Skip to content

Repository files navigation

Markdown

npm versionLicense: MITReact 18+

Production-grade, extensible Markdown rendering and editing for React — CommonMark & GFM, math, Mermaid, Shiki highlighting, CodeMirror editor, themes, and i18n in one package.

English · 简体中文 · Repository · npm


Markdown Editor — toolbar, split preview with syntax highlighting, math, tables, task lists, alerts, and TOC sidebar

Table of Contents

Features

  • CommonMark & GFM — Tables, task lists, strikethrough, footnotes, autolinks
  • Syntax Highlighting — 30+ languages via Shiki (VS Code–quality themes)
  • Math — Inline and block KaTeX ($...$, $$...$$)
  • Mermaid — Flowcharts, sequence, Gantt, class diagrams (lazy client render)
  • SVG Preview — Fenced ```svg or ```xml with SVG content renders as a sanitized inline preview (copy / download when not streaming)
  • Rich Editor — CodeMirror 6, toolbar, split/tabs layouts, image & file paste/drop/upload, auto-save, shortcuts
  • Code Block UX — Copy, download (language-based extension; optional file: / comment meta for filename), HTML sandbox preview; optional Apply (onApplyCode for coding agents)
  • Chat presets — optional CHAT_PROCESSOR_OPTIONS / createStreamingProcessor (does not change library defaults)
  • GFM Alerts & Containers> [!NOTE] / > [!WARNING] and :::tip / :::warning directives
  • TOC Sidebar — Auto-generated outline with active heading tracking (MarkdownRenderer)
  • Front Matter — YAML (and TOML) metadata via remark-frontmatter
  • Emoji:smile: shortcodes (remark-emoji)
  • Security — rehype-sanitize schema, URL handling, XSS-oriented defaults
  • Accessibility — rehype a11y helpers, ARIA-oriented output
  • Streamingstreaming prop for live SSE/chunked content (short debounce coalesce, skip Shiki/Mermaid while streaming, full highlight on stream end, cursor affordance)
  • Themes — Light / Dark / Auto mode + ThemeVariant skin system (Default / Angus / GitHub / Claude); CSS variables throughout
  • i18nen-US and zh-CN built-in
  • Dual Build — ESM + CJS, TypeScript declarations, subpath entries (/viewer, /renderer, /editor)

Quick Start

Installation

npm install @xcan-cloud/markdown

Peer dependencies:

npm install react react-dom

Import styles once in your app:

import'@xcan-cloud/markdown/styles';

This single import includes both the renderer and editor styles. Optional theme presets are imported separately (see Customization).

For viewer / renderer only (no editor chrome), prefer:

import'@xcan-cloud/markdown/styles/renderer';// or editor (+ renderer):import'@xcan-cloud/markdown/styles/editor';

Subpath entries (smaller bundles)

EntryUse caseCodeMirror
@xcan-cloud/markdownFull surface (backward compatible)In dependency graph
@xcan-cloud/markdown/viewerLightweight preview / SSRNo
@xcan-cloud/markdown/rendererStreaming, TOC, Mermaid, code actionsNo
@xcan-cloud/markdown/editorRich editing (includes preview)Yes
// Chat / read-only — does not pull CodeMirrorimport{MarkdownRenderer,MarkdownProvider}from'@xcan-cloud/markdown/renderer';import'@xcan-cloud/markdown/styles/renderer';

Chat bubble (recommended)

import{MarkdownRenderer,CHAT_PROCESSOR_OPTIONS,CHAT_RENDERER_DEFAULTS,typeCodeBlockInfo,}from'@xcan-cloud/markdown/renderer';import'@xcan-cloud/markdown/styles/renderer';functionChatBubble({ md, streaming, onApply }: {md: string;streaming: boolean;onApply?: (info: CodeBlockInfo)=>void;}){return(<MarkdownRenderersource={md}streaming={streaming}options={CHAT_PROCESSOR_OPTIONS}{...CHAT_RENDERER_DEFAULTS}onApplyCode={onApply}/>);}

CHAT_* presets are opt-in; they do not change createProcessor / showToc library defaults.

Basic Rendering

import{MarkdownRenderer}from'@xcan-cloud/markdown';import'@xcan-cloud/markdown/styles';functionApp(){return<MarkdownRenderersource="# Hello\n\nThis is **Markdown**."/>;}

Editor (split view)

import{MarkdownEditor}from'@xcan-cloud/markdown';import'@xcan-cloud/markdown/styles';functionApp(){return(<MarkdownEditorinitialValue="# Start editing…"layout="split"onChange={(value)=>console.log(value)}/>);}

Theme & Locale Provider

import{MarkdownProvider,MarkdownEditor,ThemeSwitcher,LocaleSwitcher,}from'@xcan-cloud/markdown';import'@xcan-cloud/markdown/styles';functionApp(){return(// defaultVariant="claude" — Claude skin CSS is bundled in @xcan-cloud/markdown/styles<MarkdownProviderdefaultTheme="auto"defaultVariant="claude"defaultLocale="en-US"><divstyle={{display: 'flex',gap: 8,marginBottom: 16}}><ThemeSwitcher/>{/* switches light / dark / auto */}<LocaleSwitcher/></div><MarkdownEditorinitialValue="# Hello"layout="split"/></MarkdownProvider>);}

SSR-Friendly Viewer (no CodeMirror)

import{MarkdownViewer}from'@xcan-cloud/markdown';import'@xcan-cloud/markdown/styles';functionPage({ markdown }: {markdown: string}){return<MarkdownViewersource={markdown}theme="light"/>;}

API Reference

<MarkdownRenderer />

Full-featured renderer: TOC, Mermaid/SVG post-processing, code actions, streaming.

PropTypeDefaultDescription
sourcestringMarkdown source
optionsProcessorOptionsUnified pipeline options
classNamestring''Root element class
theme'light' | 'dark' | 'auto'from context / 'auto'Color mode
showTocbooleantrueShow TOC sidebar
tocPosition'left' | 'right''right'TOC placement
debounceMsnumber150Render debounce; still applied while streaming, floored at 50ms to coalesce tokens
onRendered(info: { html: string; toc: TocItem[] }) => voidAfter successful render
onLinkClick(href: string, event: MouseEvent) => voidLink click hook
onImageClick(src: string, alt: string, event: MouseEvent) => voidImage click hook
componentsPartial<Record<string, ComponentType<any>>>Deprecated / no-op: HTML pipeline cannot map React components; value is ignored
onApplyCode(info: CodeBlockInfo) => voidWhen set, shows an Apply button on fenced code blocks (injected after streaming ends)
applyLabelstringi18n applyCodeApply button label
streamingbooleanfalseLive stream: disables Shiki/Mermaid fence transforms for cheap updates; full re-render when streaming ends
onStreamEnd() => voidFired when streaming goes truefalse
heightstringFixed height of the renderer container
minHeightstringMinimum height of the renderer container
maxHeightstringMaximum height of the renderer container

<MarkdownEditor />

Extends renderer props exceptsource is replaced by editor value APIs.

PropTypeDefaultDescription
initialValuestring''Initial markdown
valuestringControlled value
onChange(value: string) => voidContent change
layoutLayoutMode'split'split | tabs | editor-only | preview-only
layoutModesLayoutMode[]['split', 'tabs', 'editor-only', 'preview-only']Controls which layout buttons are shown and the order of layout toolbar cycling
minHeight / maxHeightstringEditor area sizing
toolbarToolbarConfigdefault setfalse to hide, or item list
readOnlybooleanfalseRead-only editor
onImageUpload(file: File) => Promise<string>Return URL for pasted/dropped/selected images. See File & Image Upload.
onImageUploadSettled(r: { success: true; url: string; file: File } | { success: false; error: unknown; file: File }) => voidFired after each image upload resolves or rejects (for toast / logging)
onFileUpload(file: File) => Promise<string>Return URL for pasted/dropped/selected non-image files; inserts a [name](url) link. See File & Image Upload.
onFileUploadSettled(r: { success: true; url: string; file: File } | { success: false; error: unknown; file: File }) => voidFired after each file upload resolves or rejects (for toast / logging)
acceptFileTypesstringaccept filter for the toolbar File button's native picker (e.g. .pdf,.zip)
mixedPastePolicy'image-first' | 'text-first' | 'image-and-text''image-first'Strategy when the clipboard contains both an image and text
onPaste(payload: ClipboardPayload, event: ClipboardEvent) => boolean | voidCustom paste hook; return true to skip the default flow
onAutoSave(value: string) => voidPeriodic save callback
autoSaveIntervalnumber30000Auto-save interval (ms)
extensionsExtension[][]Extra CodeMirror extensions
shortcutsShortcutMapCustom keymap handlers
maxLengthnumberHard limit + counter UI
placeholderstringi18n defaultEditor placeholder text

All MarkdownRenderer props except source also apply to the preview pane (e.g. options, theme, showToc).

<MarkdownViewer />

Lightweight viewer using useMarkdown (no CodeMirror).

PropTypeDefaultDescription
sourcestringMarkdown source
optionsProcessorOptionsPipeline options
classNamestring''Root class
theme'light' | 'dark' | 'auto'from contextTheme
onRendered(info: { html: string; toc: TocItem[] }) => voidNote: toc is [] in viewer
heightstringFixed height of the viewer container
minHeightstringMinimum height of the viewer container
maxHeightstringMaximum height of the viewer container

<MarkdownProvider />

PropTypeDefaultDescription
childrenReactNodeApp subtree
defaultTheme'light' | 'dark' | 'auto''auto'Light/dark mode
defaultVariant'default' | 'angus' | 'github' | 'claude''claude'Visual skin
defaultLocale'en-US' | 'zh-CN''en-US'Initial locale

<ThemeSwitcher /> / <LocaleSwitcher />

Optional controls; read/write theme and locale via useTheme() / useLocale().

TypeScript (core props)

interfaceCodeBlockInfo{code: string;language?: string;filename?: string;}interfaceMarkdownRendererProps{source: string;options?: ProcessorOptions;className?: string;theme?: 'light'|'dark'|'auto';showToc?: boolean;tocPosition?: 'left'|'right';debounceMs?: number;onRendered?: (info: {html: string;toc: TocItem[]})=>void;onLinkClick?: (href: string,event: React.MouseEvent)=>void;onImageClick?: (src: string,alt: string,event: React.MouseEvent)=>void;/** @deprecated ignored — HTML pipeline cannot map React components */components?: Partial<Record<string,React.ComponentType<any>>>;onApplyCode?: (info: CodeBlockInfo)=>void;applyLabel?: string;streaming?: boolean;onStreamEnd?: ()=>void;height?: string;minHeight?: string;maxHeight?: string;}

ProcessorOptions

OptionTypeDefaultDescription
gfmbooleantrueGitHub Flavored Markdown
mathbooleantrueKaTeX
mermaidbooleantrueMermaid code blocks (false skips mermaid-container rewrite)
frontmatterbooleantrueYAML/TOML front matter
emojibooleantrueEmoji shortcodes
tocbooleanfalse[[toc]] / [toc] replacement
sanitizebooleantrueHTML sanitization
sanitizeSchemaSchemainternalCustom rehype-sanitize schema
codeThemestring'github-dark'Shiki theme
highlightbooleantrueShiki highlighting (async pipeline)
allowHtmlbooleantrueRaw HTML path through remark-rehype
remarkPluginsPlugin[][]Extra remark plugins
rehypePluginsPlugin[][]Extra rehype plugins

Exported Utilities

ExportDescription
createProcessor, renderMarkdown, renderMarkdownSync, parseToAst, resolveProcessorOptionsForRender, processorOptionsCacheKeyCore unified pipeline and render helpers
CHAT_PROCESSOR_OPTIONS, STREAMING_PROCESSOR_OPTIONS, CHAT_RENDERER_DEFAULTS, createChatProcessor, createStreamingProcessorChat / streaming presets (does not change createProcessor defaults)
ProcessorOptionsPipeline configuration type
rehypeHighlightCodeShiki highlighting rehype plugin
renderMermaidDiagram, initMermaidClient Mermaid helpers
extractToc, remarkToc, TocItemTOC extraction / remark plugin
remarkAlert, remarkContainer, remarkCodeMetaAlert, container, code-meta remark plugins
parseCodeMeta, extractCodeBlocks, CodeBlockMetaFence meta parsing
sanitizeUrl, processExternalLinks, escapeHtmlSecurity helpers
rehypeA11yAccessibility rehype plugin
MarkdownWorkerRenderer, RenderCache, sharedRenderCache, splitHtmlBlocksWorker / cache utilities
copyToClipboardClipboard helper
slug, resetSluggerHeading slug utilities
performImageUpload, performFileUpload, createImageUploadLifecycle, createFileUploadLifecycle, encodeMarkdownUrl, sanitizeAltText, isImageFile, collectImageFiles, generateUploadIdImage & file paste/drop/upload helpers (see File & Image Upload)
setLocale, getLocale, t, getMessagesi18n API
ThemeVariant, resolveThemeClassSkin type and CSS-class resolver

Hooks

HookDescription
useMarkdown(source, options?)Returns { html, toc, isLoading, error, refresh }
useDebouncedValue(value, delay)Debounced value
useScrollSync(editorRef, previewRef)Bi-directional scroll sync

Component Architecture

┌─────────────────────────────────────────────────────────────┐
│ MarkdownProvider │
│ (theme / locale context) │
└───────────────────────────┬─────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ MarkdownEditor│ │MarkdownRenderer│ │ MarkdownViewer │
│ ┌──────────┐ │ │ • unified + │ │ • useMarkdown │
│ │ CodeMirror│ │ │ Shiki/KaTeX │ │ • no CM dep │
│ │ + Toolbar │ │ │ • TOC sidebar │ └──────────────────┘
│ └──────────┘ │ │ • Mermaid/SVG │
│ ┌──────────┐ │ │ • code actions │
│ │ Preview │◄┼──┤ (copy/…) │
│ │ (Renderer)│ │ └────────────────┘
│ └──────────┘ │
└──────────────┘

Sub-Projects

PathDescription
website/Vite dev playground / demo app for local development
src/styles/Base markdown-renderer.css and theme presets (themes/github.css, themes/angus.css, themes/claude.css)

Inline HTML

Raw HTML is supported end-to-end (parse → sanitize → render) when the default allowHtml: true is active. The sanitize schema explicitly permits class, style, id and data-* on every element, so sized <img> tags survive the pipeline:

<img src="diagrams/svg/02-lifecycle.svg"
alt="Request lifecycle"
style="max-width:1024px;width:100%;height:auto;" />

Security invariants that are still enforced:

  • <script>, <iframe>, <object>, <embed> are stripped.
  • href / src with javascript: or vbscript: schemes are dropped.
  • data: URLs are only allowed for images.
  • Unknown tags fall through rehype-sanitize's allowlist.

To further tighten or loosen the policy, pass a custom sanitizeSchema via ProcessorOptions.

File & Image Upload

MarkdownEditor supports uploading both images and arbitrary files (pdf, zip, docx, …) through three entry points: the toolbar Image / File buttons (native file picker), clipboard paste, and drag-and-drop. Provide the matching uploader(s) and the editor handles everything else — inserting a unique placeholder, swapping in the final Markdown on success, and replacing the placeholder with an HTML comment on failure.

  • onImageUpload inserts an image node: ![alt](url).
  • onFileUpload inserts a link: [name](url).

Each callback is independent: wire onImageUpload only, onFileUpload only, or both. The corresponding toolbar button (and paste/drop routing) activates only when its callback is provided.

import{MarkdownEditor}from'@xcan-cloud/markdown';asyncfunctionuploadToCdn(file: File): Promise<string>{constfd=newFormData();fd.append('file',file);constres=awaitfetch('/api/upload',{method: 'POST',body: fd});if(!res.ok)thrownewError(`upload failed: ${res.status}`);const{ url }=awaitres.json();returnurl;}<MarkdownEditoronImageUpload={uploadToCdn}// images → ![alt](url)onFileUpload={uploadToCdn}// other files → [name](url)acceptFileTypes=".pdf,.zip,.docx"onImageUploadSettled={(r)=>{if(r.success)toast.success(`Uploaded ${r.file.name}`);elsetoast.error(`Upload failed: ${String(r.error)}`);}}onFileUploadSettled={(r)=>{if(r.success)toast.success(`Uploaded ${r.file.name}`);elsetoast.error(`Upload failed: ${String(r.error)}`);}}/>

Behavioral guarantees:

  • Toolbar pickers. The Image button opens an image/* picker; the File button opens a picker filtered by acceptFileTypes. Both support multi-select.
  • Unique placeholders. Each upload gets a random id so concurrent uploads never overwrite each other's insertion point.
  • Error resilience. A rejected uploader replaces the placeholder with <!-- Upload failed: <reason> --> (not rendered, visible in source).
  • Multi-file drop. Dropping multiple files uploads them in parallel at the drop point.
  • Read-only safe. When readOnly is set, paste and drop never write to the document (uploads are skipped).
  • Localization. Placeholder text uses the active locale (editor.uploading, editor.uploadFailed).
  • URL safety. URLs are percent-encoded for whitespace and ( ) so returned CDN URLs containing spaces or parentheses do not break the ![alt](url) / [name](url) syntax.

Paste & drop classification

The editor routes text vs. image vs. file pastes separately so typing and rich-text paste are never intercepted unnecessarily:

Clipboard / drop contentsDefault behavior
Plain text / HTML onlyBrowser default (no interception)
Image file(s) onlyUpload each image via onImageUpload, insert ![alt](url)
Image + text (e.g. Windows screenshot)Controlled by mixedPastePolicy
Non-image files only (pdf, zip, …)Upload each via onFileUpload, insert [name](url)
Non-image files + textControlled by mixedPastePolicy (same semantics as image + text)

When the relevant callback is not provided, that payload falls through to the browser default (e.g. without onFileUpload, pasting a PDF is not intercepted).

<MarkdownEditoronImageUpload={uploadToCdn}mixedPastePolicy="image-and-text"// upload screenshot AND keep caption textonPaste={(payload)=>{if(payload.otherFiles.some(f=>f.type==='application/pdf')){toast.warn('PDF paste ignored');returntrue;// handled — skip default flow}}}/>

The classifyClipboard(transfer) helper that powers this (returns { images, otherFiles, text, html, uriList, hasImages, hasText, ... }) is exported for custom integrations, alongside performImageUpload, performFileUpload, createImageUploadLifecycle, createFileUploadLifecycle, encodeMarkdownUrl, isImageFile, collectImageFiles.

Customization

Themes

The theme system has two orthogonal dimensions:

  • defaultTheme — brightness mode: 'light', 'dark', 'auto' (follows prefers-color-scheme)
  • defaultVariant — visual skin: 'default', 'angus', 'github', 'claude'

The combination maps to a single CSS class on the root container:

variant \ modelightdark
defaultmarkdown-theme-lightmarkdown-theme-dark
angusmarkdown-theme-angusmarkdown-theme-angus-dark
githubmarkdown-theme-githubmarkdown-theme-github-dark
claudemarkdown-theme-claudemarkdown-theme-claude-dark

Primary Color Inheritance

The Claude theme supports inheriting the brand color from the host application. If the host defines --primary on :root, then --md-accent and --md-link automatically inherit that value; otherwise they fall back to Claude’s default brand color.

/* Host application global styles */:root {
--primary:#3b82f6; /* blue brand */
}
/* Markdown links and accent colors automatically become blue */

Default skin (light / dark toggle)

import'@xcan-cloud/markdown/styles';import{MarkdownProvider,MarkdownRenderer}from'@xcan-cloud/markdown';functionApp(){return(<MarkdownProviderdefaultTheme="auto"><MarkdownRenderersource="# Hello"/></MarkdownProvider>);}

Angus skin

The Angus skin CSS is bundled inside@xcan-cloud/markdown/styles — no extra import needed.

import'@xcan-cloud/markdown/styles';import{MarkdownProvider,MarkdownEditor,ThemeSwitcher}from'@xcan-cloud/markdown';functionApp(){return(// defaultVariant="angus": light mode → markdown-theme-angus// dark mode → markdown-theme-angus-dark<MarkdownProviderdefaultTheme="auto"defaultVariant="angus"><ThemeSwitcher/><MarkdownEditorinitialValue="# Hello"layout="split"/></MarkdownProvider>);}

GitHub skin

The GitHub skin requires an additional CSS import:

import'@xcan-cloud/markdown/styles';import'@xcan-cloud/markdown/themes/github.css';// ← extra import requiredimport{MarkdownProvider,MarkdownRenderer}from'@xcan-cloud/markdown';functionApp(){return(// defaultVariant="github": light mode → markdown-theme-github// dark mode → markdown-theme-github-dark<MarkdownProviderdefaultTheme="light"defaultVariant="github"><MarkdownRenderersource="# Hello"/></MarkdownProvider>);}

Switching variant at runtime

import{useTheme}from'@xcan-cloud/markdown';functionVariantSwitcher(){const{ variant, setVariant }=useTheme();return(<selectvalue={variant}onChange={(e)=>setVariant(e.target.valueasany)}><optionvalue="default">Default</option><optionvalue="angus">Angus</option><optionvalue="github">GitHub</option></select>);}

CSS variables

Override tokens on .markdown-renderer (see stylesheet for --md-* variables).

i18n

<MarkdownProviderdefaultLocale="zh-CN"><MarkdownEditorinitialValue="# 你好"/></MarkdownProvider>
import{setLocale,t}from'@xcan-cloud/markdown';setLocale('zh-CN');

Toolbar

<MarkdownEditortoolbar={false}/><MarkdownEditortoolbar={['bold','italic','|','code']}/>

In layout="tabs", when toolbar={false}, a minimal built-in switcher (Editor / Preview) is still rendered to keep tabs mode operable.

Layout and layoutModes

LayoutMode is publicly exported and can be used in app-side TypeScript:

import{MarkdownEditor,typeLayoutMode}from'@xcan-cloud/markdown';

layout controls the currently active layout mode:

  • split: editor and preview shown side by side
  • tabs: one pane at a time (Editor / Preview), switchable by toolbar preview action or built-in tabs switcher
  • editor-only: editor pane only
  • preview-only: preview pane only

layoutModes controls which modes are available in the layout UI and the cycle order of the layout toolbar action.

  • Default: ['split', 'tabs', 'editor-only', 'preview-only']
  • Empty array falls back to the default list
  • If current layout is not included in layoutModes, it falls back to layoutModes[0]

Examples:

// Restrict to edit/preview full-page switching only<MarkdownEditorlayout="editor-only"layoutModes={['editor-only','preview-only']}/>// Keep split + tabs only<MarkdownEditorlayout="tabs"layoutModes={['tabs','split']}/>

Height

// Fixed height<MarkdownRenderersource={md}height="600px"/><MarkdownViewersource={md}height="400px"/>// Min / max height<MarkdownRenderersource={md}minHeight="200px"maxHeight="80vh"/>// Editor CodeMirror pane height<MarkdownEditorminHeight="300px"maxHeight="700px"/>

Code fence meta

```python filename=hello.py
print("hi")
```

Use parseCodeMeta / extractCodeBlocks for external tooling.

Technology Stack

CategoryTechnologies
FrameworkReact 18+, TypeScript
Markdownunified, remark, rehype, remark-gfm, remark-math, …
HighlightingShiki
DiagramsMermaid (client), KaTeX
EditorCodeMirror 6
Iconslucide-react
BuildVite, vite-plugin-dts

Browser Support

Modern evergreen browsers (Chrome, Firefox, Safari, Edge — last 2 major versions). Features like fetch streams / Workers follow browser capabilities.

Development

npm install
npm run dev # website demo
npm run build # library dist
npm test
npm run lint # tsc --noEmit

Contributing

  1. Fork the repository.
  2. Create a branch: git checkout -b feat/your-feature.
  3. Commit with clear messages.
  4. Push and open a Pull Request.

Please ensure npm run lint and npm run build pass before submitting.

License

MIT © Markdown package contributors

About

Production-grade, extensible, high-performance Markdown rendering & editing component for React.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages