npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xcan-cloud/markdown

v1.6.0

Published

Production-grade, extensible, high-performance Markdown rendering and editing component

Readme

Markdown

npm version License: MIT React 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


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 paste/drop, auto-save, shortcuts
  • Code Block UX — Copy, download (language-based extension; optional file: / comment meta for filename), HTML sandbox preview
  • 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 (debounce bypass, cursor affordance)
  • Themes — Light / Dark / Auto mode + ThemeVariant skin system (Default / Angus / GitHub); CSS variables throughout
  • i18nen-US and zh-CN built-in
  • Dual Build — ESM + CJS, TypeScript declarations, tree-shakeable entry

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).

Basic Rendering

import { MarkdownRenderer } from '@xcan-cloud/markdown';
import '@xcan-cloud/markdown/styles';

function App() {
  return <MarkdownRenderer source="# Hello\n\nThis is **Markdown**." />;
}

Editor (split view)

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

function App() {
  return (
    <MarkdownEditor
      initialValue="# 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';

function App() {
  return (
    // defaultVariant="angus" — angus.css is already bundled inside @xcan-cloud/markdown/styles
    <MarkdownProvider defaultTheme="auto" defaultVariant="angus" defaultLocale="en-US">
      <div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
        <ThemeSwitcher />   {/* switches light / dark / auto */}
        <LocaleSwitcher />
      </div>
      <MarkdownEditor initialValue="# Hello" layout="split" />
    </MarkdownProvider>
  );
}

SSR-Friendly Viewer (no CodeMirror)

import { MarkdownViewer } from '@xcan-cloud/markdown';
import '@xcan-cloud/markdown/styles';

function Page({ markdown }: { markdown: string }) {
  return <MarkdownViewer source={markdown} theme="light" />;
}

API Reference

<MarkdownRenderer />

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

| Prop | Type | Default | Description | | --- | --- | --- | --- | | source | string | — | Markdown source | | options | ProcessorOptions | — | Unified pipeline options | | className | string | '' | Root element class | | theme | 'light' \| 'dark' \| 'auto' | from context / 'auto' | Color mode | | showToc | boolean | true | Show TOC sidebar | | tocPosition | 'left' \| 'right' | 'right' | TOC placement | | debounceMs | number | 150 | Render debounce (disabled while streaming) | | onRendered | (info: { html: string; toc: TocItem[] }) => void | — | After successful render | | onLinkClick | (href: string, event: MouseEvent) => void | — | Link click hook | | onImageClick | (src: string, alt: string, event: MouseEvent) => void | — | Image click hook | | components | Partial<Record<string, ComponentType<any>>> | — | Custom HTML tag mapping | | streaming | boolean | false | Live stream mode | | onStreamEnd | () => void | — | Fired when streaming goes truefalse | | height | string | — | Fixed height of the renderer container | | minHeight | string | — | Minimum height of the renderer container | | maxHeight | string | — | Maximum height of the renderer container |

<MarkdownEditor />

Extends renderer props except source is replaced by editor value APIs.

| Prop | Type | Default | Description | | --- | --- | --- | --- | | initialValue | string | '' | Initial markdown | | value | string | — | Controlled value | | onChange | (value: string) => void | — | Content change | | layout | LayoutMode | 'split' | split | tabs | editor-only | preview-only | | layoutModes | LayoutMode[] | ['split', 'tabs', 'editor-only', 'preview-only'] | Controls which layout buttons are shown and the order of layout toolbar cycling | | minHeight / maxHeight | string | — | Editor area sizing | | toolbar | ToolbarConfig | default set | false to hide, or item list | | readOnly | boolean | false | Read-only editor | | onImageUpload | (file: File) => Promise<string> | — | Return URL for pasted/dropped images. See Image Paste Upload. | | onImageUploadSettled | (r: { success: true; url: string; file: File } \| { success: false; error: unknown; file: File }) => void | — | Fired after each upload resolves or rejects (for toast / logging) | | 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 \| void | — | Custom paste hook; return true to skip the default flow | | onAutoSave | (value: string) => void | — | Periodic save callback | | autoSaveInterval | number | 30000 | Auto-save interval (ms) | | extensions | Extension[] | [] | Extra CodeMirror extensions | | shortcuts | ShortcutMap | — | Custom keymap handlers | | maxLength | number | — | Hard limit + counter UI | | placeholder | string | i18n default | Editor 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).

| Prop | Type | Default | Description | | --- | --- | --- | --- | | source | string | — | Markdown source | | options | ProcessorOptions | — | Pipeline options | | className | string | '' | Root class | | theme | 'light' \| 'dark' \| 'auto' | from context | Theme | | onRendered | (info: { html: string; toc: TocItem[] }) => void | — | Note: toc is [] in viewer | | height | string | — | Fixed height of the viewer container | | minHeight | string | — | Minimum height of the viewer container | | maxHeight | string | — | Maximum height of the viewer container |

<MarkdownProvider />

| Prop | Type | Default | Description | | --- | --- | --- | --- | | children | ReactNode | — | App subtree | | defaultTheme | 'light' \| 'dark' \| 'auto' | 'auto' | Light/dark mode | | defaultVariant | 'default' \| 'angus' \| 'github' | 'angus' | 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)

interface MarkdownRendererProps {
  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;
  components?: Partial<Record<string, React.ComponentType<any>>>;
  streaming?: boolean;
  onStreamEnd?: () => void;
  height?: string;
  minHeight?: string;
  maxHeight?: string;
}

ProcessorOptions

| Option | Type | Default | Description | | --- | --- | --- | --- | | gfm | boolean | true | GitHub Flavored Markdown | | math | boolean | true | KaTeX | | mermaid | boolean | true | Mermaid code blocks | | frontmatter | boolean | true | YAML/TOML front matter | | emoji | boolean | true | Emoji shortcodes | | toc | boolean | false | [[toc]] / [toc] replacement | | sanitize | boolean | true | HTML sanitization | | sanitizeSchema | Schema | internal | Custom rehype-sanitize schema | | codeTheme | string | 'github-dark' | Shiki theme | | highlight | boolean | true | Shiki highlighting (async pipeline) | | allowHtml | boolean | true | Raw HTML path through remark-rehype | | remarkPlugins | Plugin[] | [] | Extra remark plugins | | rehypePlugins | Plugin[] | [] | Extra rehype plugins |

Exported Utilities

| Export | Description | | --- | --- | | createProcessor, renderMarkdown, renderMarkdownSync, parseToAst | Core unified pipeline | | ProcessorOptions | Pipeline configuration type | | rehypeHighlightCode | Shiki highlighting rehype plugin | | renderMermaidDiagram, initMermaid | Client Mermaid helpers | | extractToc, remarkToc, TocItem | TOC extraction / remark plugin | | remarkAlert, remarkContainer, remarkCodeMeta | Alert, container, code-meta remark plugins | | parseCodeMeta, extractCodeBlocks, CodeBlockMeta | Fence meta parsing | | sanitizeUrl, processExternalLinks, escapeHtml | Security helpers | | rehypeA11y | Accessibility rehype plugin | | MarkdownWorkerRenderer, RenderCache, splitHtmlBlocks | Worker / cache utilities | | copyToClipboard | Clipboard helper | | slug, resetSlugger | Heading slug utilities | | performImageUpload, createImageUploadLifecycle, encodeMarkdownUrl, sanitizeAltText, isImageFile, collectImageFiles, generateUploadId | Image paste/drop upload helpers (see Image Paste Upload) | | setLocale, getLocale, t, getMessages | i18n API | | ThemeVariant, resolveThemeClass | Skin type and CSS-class resolver |

Hooks

| Hook | Description | | --- | --- | | 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

| Path | Description | | --- | --- | | website/ | Vite dev playground / demo app for local development | | src/styles/ | Base markdown-renderer.css and theme presets (themes/github.css, themes/angus.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.

Image Paste Upload

MarkdownEditor supports pasting from the clipboard and drag-and-drop for images. Provide an uploader that returns the final URL and the editor handles everything else — inserting a unique placeholder, swapping in the final ![alt](url) on success, and replacing the placeholder with an HTML comment on failure.

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

async function uploadToCdn(file: File): Promise<string> {
  const fd = new FormData();
  fd.append('file', file);
  const res = await fetch('/api/upload', { method: 'POST', body: fd });
  if (!res.ok) throw new Error(`upload failed: ${res.status}`);
  const { url } = await res.json();
  return url;
}

<MarkdownEditor
  onImageUpload={uploadToCdn}
  onImageUploadSettled={(r) => {
    if (r.success) toast.success(`Uploaded ${r.file.name}`);
    else toast.error(`Upload failed: ${String(r.error)}`);
  }}
/>

Behavioral guarantees:

  • Unique placeholders. Each upload gets a random id so concurrent pastes 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 images uploads them in parallel at the drop point.
  • 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) syntax.

Paste classification

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

| Clipboard contents | Default behavior | | --- | --- | | Plain text / HTML only | Browser default (no interception) | | Image file(s) only | Upload each image, insert ![alt](url) | | Image + text (e.g. Windows screenshot) | Controlled by mixedPastePolicy | | Non-image files only (pdf, zip, …) | Browser default (not uploaded) |

<MarkdownEditor
  onImageUpload={uploadToCdn}
  mixedPastePolicy="image-and-text"   // upload screenshot AND keep caption text
  onPaste={(payload) => {
    if (payload.otherFiles.some(f => f.type === 'application/pdf')) {
      toast.warn('PDF paste ignored');
      return true; // 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, createImageUploadLifecycle, 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'

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

| variant \ mode | light | dark | | --- | --- | --- | | default | markdown-theme-light | markdown-theme-dark | | angus | markdown-theme-angus | markdown-theme-angus-dark | | github | markdown-theme-github | markdown-theme-github-dark |

Default skin (light / dark toggle)

import '@xcan-cloud/markdown/styles';
import { MarkdownProvider, MarkdownRenderer } from '@xcan-cloud/markdown';

function App() {
  return (
    <MarkdownProvider defaultTheme="auto">
      <MarkdownRenderer source="# 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';

function App() {
  return (
    // defaultVariant="angus": light mode → markdown-theme-angus
    //                          dark mode  → markdown-theme-angus-dark
    <MarkdownProvider defaultTheme="auto" defaultVariant="angus">
      <ThemeSwitcher />
      <MarkdownEditor initialValue="# 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 required
import { MarkdownProvider, MarkdownRenderer } from '@xcan-cloud/markdown';

function App() {
  return (
    // defaultVariant="github": light mode → markdown-theme-github
    //                           dark mode  → markdown-theme-github-dark
    <MarkdownProvider defaultTheme="light" defaultVariant="github">
      <MarkdownRenderer source="# Hello" />
    </MarkdownProvider>
  );
}

Switching variant at runtime

import { useTheme } from '@xcan-cloud/markdown';

function VariantSwitcher() {
  const { variant, setVariant } = useTheme();
  return (
    <select value={variant} onChange={(e) => setVariant(e.target.value as any)}>
      <option value="default">Default</option>
      <option value="angus">Angus</option>
      <option value="github">GitHub</option>
    </select>
  );
}

CSS variables

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

i18n

<MarkdownProvider defaultLocale="zh-CN">
  <MarkdownEditor initialValue="# 你好" />
</MarkdownProvider>
import { setLocale, t } from '@xcan-cloud/markdown';

setLocale('zh-CN');

Toolbar

<MarkdownEditor toolbar={false} />
<MarkdownEditor toolbar={['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, type LayoutMode } 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
<MarkdownEditor
  layout="editor-only"
  layoutModes={['editor-only', 'preview-only']}
/>

// Keep split + tabs only
<MarkdownEditor
  layout="tabs"
  layoutModes={['tabs', 'split']}
/>

Height

// Fixed height
<MarkdownRenderer source={md} height="600px" />
<MarkdownViewer source={md} height="400px" />

// Min / max height
<MarkdownRenderer source={md} minHeight="200px" maxHeight="80vh" />

// Editor CodeMirror pane height
<MarkdownEditor minHeight="300px" maxHeight="700px" />

Code fence meta

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

Use parseCodeMeta / extractCodeBlocks for external tooling.

Technology Stack

| Category | Technologies | | --- | --- | | Framework | React 18+, TypeScript | | Markdown | unified, remark, rehype, remark-gfm, remark-math, … | | Highlighting | Shiki | | Diagrams | Mermaid (client), KaTeX | | Editor | CodeMirror 6 | | Icons | lucide-react | | Build | Vite, 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