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.8.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 & 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
  • Streaming — streaming 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
  • i18n — en-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)

| Entry | Use case | CodeMirror | | --- | --- | --- | | @xcan-cloud/markdown | Full surface (backward compatible) | In dependency graph | | @xcan-cloud/markdown/viewer | Lightweight preview / SSR | No | | @xcan-cloud/markdown/renderer | Streaming, TOC, Mermaid, code actions | No | | @xcan-cloud/markdown/editor | Rich editing (includes preview) | Yes |

// Chat / read-only — does not pull CodeMirror
import { MarkdownRenderer, MarkdownProvider } from '@xcan-cloud/markdown/renderer';
import '@xcan-cloud/markdown/styles/renderer';

Chat bubble (recommended)

import {
  MarkdownRenderer,
  CHAT_PROCESSOR_OPTIONS,
  CHAT_RENDERER_DEFAULTS,
  type CodeBlockInfo,
} from '@xcan-cloud/markdown/renderer';
import '@xcan-cloud/markdown/styles/renderer';

function ChatBubble({ md, streaming, onApply }: {
  md: string;
  streaming: boolean;
  onApply?: (info: CodeBlockInfo) => void;
}) {
  return (
    <MarkdownRenderer
      source={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';

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="claude" — Claude skin CSS is bundled in @xcan-cloud/markdown/styles
    <MarkdownProvider defaultTheme="auto" defaultVariant="claude" 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; still applied while streaming, floored at 50ms to coalesce tokens | | 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>>> | — | Deprecated / no-op: HTML pipeline cannot map React components; value is ignored | | onApplyCode | (info: CodeBlockInfo) => void | — | When set, shows an Apply button on fenced code blocks (injected after streaming ends) | | applyLabel | string | i18n applyCode | Apply button label | | streaming | boolean | false | Live stream: disables Shiki/Mermaid fence transforms for cheap updates; full re-render when streaming ends | | onStreamEnd | () => void | — | Fired when streaming goes true → false | | 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/selected images. See File & Image Upload. | | onImageUploadSettled | (r: { success: true; url: string; file: File } \| { success: false; error: unknown; file: File }) => void | — | Fired 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 }) => void | — | Fired after each file upload resolves or rejects (for toast / logging) | | acceptFileTypes | string | — | accept 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 \| 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' \| '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)

interface CodeBlockInfo {
  code: string;
  language?: string;
  filename?: string;
}

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;
  /** @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

| Option | Type | Default | Description | | --- | --- | --- | --- | | gfm | boolean | true | GitHub Flavored Markdown | | math | boolean | true | KaTeX | | mermaid | boolean | true | Mermaid code blocks (false skips mermaid-container rewrite) | | 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, resolveProcessorOptionsForRender, processorOptionsCacheKey | Core unified pipeline and render helpers | | CHAT_PROCESSOR_OPTIONS, STREAMING_PROCESSOR_OPTIONS, CHAT_RENDERER_DEFAULTS, createChatProcessor, createStreamingProcessor | Chat / streaming presets (does not change createProcessor defaults) | | 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, sharedRenderCache, splitHtmlBlocks | Worker / cache utilities | | copyToClipboard | Clipboard helper | | slug, resetSlugger | Heading slug utilities | | performImageUpload, performFileUpload, createImageUploadLifecycle, createFileUploadLifecycle, encodeMarkdownUrl, sanitizeAltText, isImageFile, collectImageFiles, generateUploadId | Image & file paste/drop/upload helpers (see File & Image 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, 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';

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}     // images → ![alt](url)
  onFileUpload={uploadToCdn}      // other files → [name](url)
  acceptFileTypes=".pdf,.zip,.docx"
  onImageUploadSettled={(r) => {
    if (r.success) toast.success(`Uploaded ${r.file.name}`);
    else toast.error(`Upload failed: ${String(r.error)}`);
  }}
  onFileUploadSettled={(r) => {
    if (r.success) toast.success(`Uploaded ${r.file.name}`);
    else toast.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 contents | Default behavior | | --- | --- | | Plain text / HTML only | Browser default (no interception) | | Image file(s) only | Upload 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 + text | Controlled 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).

<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, 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 \ 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 | | claude | markdown-theme-claude | markdown-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';

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