Skip to content

A modern, type-safe rewrite of markdown-it in TypeScript: pluggable rules, split parse/render, CommonMark-compatible; fast one-shot parsing and even faster streaming/incremental updates.

License

Notifications You must be signed in to change notification settings

WuMingDao/markdown-it-ts

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

33 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

markdown-it-ts

English | 简体中文

A TypeScript migration of markdown-it with modular architecture for tree-shaking and separate parse/render imports.

🚀 Migration Status: 100% Complete

This is an active migration of markdown-it to TypeScript with the following goals:

  • ✅ Full TypeScript type safety
  • ✅ Modular architecture (separate parse/render imports)
  • ✅ Tree-shaking support
  • ✅ Ruler-based rule system
  • ✅ API compatibility with original markdown-it

What's Implemented

✅ Core System (100%)

  • All 7 core rules (normalize, block, inline, linkify, replacements, smartquotes, text_join)
  • CoreRuler with enable/disable/getRules support
  • Full parsing pipeline

✅ Block System (100%)

  • All 11 block rules:
    • table (GFM tables)
    • code (indented code blocks)
    • fence (fenced code blocks)
    • blockquote (block quotes)
    • hr (horizontal rules)
    • list (bullet and ordered lists with nesting)
    • reference (link reference definitions)
    • html_block (raw HTML blocks)
    • heading (ATX headings #)
    • lheading (Setext headings ===)
    • paragraph (paragraphs)
  • StateBlock with full line tracking (200+ lines)
  • BlockRuler implementation (80 lines)
  • ParserBlock refactored with Ruler pattern

✅ Inline System (100%)

  • All 12 inline rules (text, escape, linkify, strikethrough, etc.) with full post-processing coverage
  • StateInline with 18 properties, 3 methods
  • InlineRuler implementation mirroring markdown-it behavior

✅ Renderer & Infrastructure (100%)

  • Renderer ported from markdown-it with attribute handling & highlight support
  • Type definitions with Token interface and renderer options
  • Helper functions (parseLinkLabel, parseLinkDestination, parseLinkTitle)
  • Common utilities (html_blocks, html_re, utils)
  • markdownit() instances expose render, renderInline, and renderer for plugin compatibility

Installation

npm install markdown-it-ts

Usage

Basic Parsing (Current State)

import markdownIt from 'markdown-it-ts'

const md = markdownIt()
const tokens = md.parse('# Hello World')
console.log(tokens)

Rendering Markdown

Use the built-in renderer for full markdown-it compatibility:

import markdownIt from 'markdown-it-ts'

const md = markdownIt()
const html = md.render('# Hello World')
console.log(html)

If you initially import core-only and want to attach rendering (to keep bundles smaller when only parse is needed elsewhere), use the provided helper:

import markdownIt, { withRenderer } from 'markdown-it-ts'

const md = withRenderer(markdownIt())
const html = md.render('# Hello World')
console.log(html)

Customization

You can customize the parser and renderer by enabling or disabling specific rules:

import markdownIt from 'markdown-it-ts'

const md = markdownIt()
  .enable(['linkify', 'typographer'])
  .disable('html')

const result = md.render('Some markdown content')
console.log(result)

Subpath exports

For advanced or tree-shaken imports you can target subpaths directly:

import { Token } from 'markdown-it-ts/common/token'
import { withRenderer } from 'markdown-it-ts/plugins/with-renderer'
import Renderer from 'markdown-it-ts/render/renderer'
import { StreamBuffer } from 'markdown-it-ts/stream/buffer'
import { chunkedParse } from 'markdown-it-ts/stream/chunked'
import { DebouncedStreamParser, ThrottledStreamParser } from 'markdown-it-ts/stream/debounced'

Plugin Authoring (Type-Safe)

Plugins are regular functions that receive the markdown-it-ts instance. For full type-safety use the exported MarkdownItPlugin type:

import markdownIt, { MarkdownItPlugin } from 'markdown-it-ts'

const plugin: MarkdownItPlugin = (md) => {
  md.core.ruler.after('block', 'my_rule', (state) => {
    // custom transform logic
  })
}

const md = markdownIt().use(plugin)

Performance tips

For large documents or append-heavy editing flows, you can enable the stream parser and an optional chunked fallback. See the detailed guide in docs/stream-optimization.md.

Quick start:

import markdownIt from 'markdown-it-ts'

const md = markdownIt({
  stream: true, // enable stream mode
  streamChunkedFallback: true, // use chunked on first large parse or large non-append edits
  // optional tuning
  // By default, chunk size is adaptive to doc size (streamChunkAdaptive: true)
  // You can pin fixed sizes by setting streamChunkAdaptive: false
  streamChunkSizeChars: 10_000,
  streamChunkSizeLines: 200,
  streamChunkFenceAware: true,
})

let src = '# Title\n\nHello'
md.parse(src, {})

// Append-only edits use the fast path
src += '\nworld!'
md.parse(src, {})

Try the quick benchmark (build first):

npm run build
node scripts/quick-benchmark.mjs

More:

  • Full performance matrix across modes and sizes: npm run perf:matrix
  • Non-stream chunked sweep to tune thresholds: npm run perf:sweep
  • See detailed findings in docs/perf-report.md.

Adaptive chunk sizing

  • Non-stream full fallback now chooses chunk size automatically by default (fullChunkAdaptive: true), targeting ~8 chunks and clamping sizes into practical ranges.
  • Stream chunked fallback also uses adaptive sizing by default (streamChunkAdaptive: true).
  • You can restore fixed sizes by setting the respective *Adaptive: false flags or by providing explicit *SizeChars/*SizeLines values.

Programmatic recommendations

If you want to display or persist the suggested chunk settings without enabling auto-tune, you can query them directly:

import markdownIt, { recommendFullChunkStrategy, recommendStreamChunkStrategy } from 'markdown-it-ts'

const size = 50_000
const fullRec = recommendFullChunkStrategy(size)
// { strategy: 'plain', fenceAware: true }

const streamRec = recommendStreamChunkStrategy(size)
// { strategy: 'discrete', maxChunkChars: 16_000, maxChunkLines: 250, fenceAware: true }

These mirror the same mappings used internally when autoTuneChunks: true and no explicit sizes are provided.

Performance regression checks

To make sure each change is not slower than the previous run at any tested size/config, we ship a tiny perf harness and a comparator:

  • Generate the latest report and snapshot:

    • npm run perf:generate → writes docs/perf-latest.md and docs/perf-latest.json
    • Also archives docs/perf-history/perf-<shortSHA>.json when git is available
  • Compare two snapshots (fail on regressions beyond threshold):

    • node scripts/perf-compare.mjs docs/perf-latest.json docs/perf-history/perf-<baselineSHA>.json --threshold=0.10
  • Accept the latest run as the new baseline (after manual review):

    • pnpm run perf:accept
  • Run the regression check against the most recent baseline (same harness):

    • pnpm run perf:check:latest
  • Inspect detailed deltas by size/scenario (sorted by worst):

    • pnpm run perf:diff

See docs/perf-regression.md for details and CI usage.

Upstream Test Suites (optional)

This repo can run a subset of the original markdown-it tests and pathological cases. They are disabled by default because they require:

  • A sibling checkout of the upstream markdown-it repo (referenced by relative path in tests)
  • Network access for fetching reference scripts

To enable upstream tests locally:

# Ensure directory layout like:
#   ../markdown-it/    # upstream repo with index.mjs and fixtures
#   ./markdown-it-ts/  # this repo

RUN_ORIGINAL=1 pnpm test

Notes

  • Pathological tests are heavy and use worker threads and network; enable only when needed.
  • CI keeps these disabled by default.

Alternative: set a custom upstream path without sibling layout

# Point to a local checkout of markdown-it
MARKDOWN_IT_DIR=/absolute/path/to/markdown-it RUN_ORIGINAL=1 pnpm test

Convenience scripts

pnpm run test:original           # same as RUN_ORIGINAL=1 pnpm test
pnpm run test:original:network   # also sets RUN_NETWORK=1

Parse performance vs markdown-it

Latest one-shot parse results on this machine (Node.js v23): markdown-it-ts is roughly at parity with upstream markdown-it in the 5k–100k range.

Examples from the latest run (avg over 20 iterations):

  • 5,000 chars: 0.00ms vs 0.43ms → ~2627.6× faster (0.00× time)
  • 20,000 chars: 0.97ms vs 0.84ms → ~0.9× faster (1.16× time)
  • 50,000 chars: 2.59ms vs 2.20ms → ~0.8× faster (1.18× time)
  • 100,000 chars: 5.58ms vs 4.94ms → ~0.9× faster (1.13× time)
  • 200,000 chars: 12.35ms vs 13.31ms → ~1.1× faster (0.93× time)
  • Notes
  • Numbers vary by Node version, CPU, and content shape; see docs/perf-latest.md for the full table and environment details.
  • Streaming/incremental mode is correctness-first by default. For editor-style input, using StreamBuffer to flush at block boundaries can yield meaningful wins on append-heavy workloads.

Parse performance vs remark

We also compare parse-only performance against remark (parse-only). The following figures are taken from the latest archived snapshot docs/perf-history/perf-d660c6e.json (generatedAt 2025-11-14, Node v23.7.0) and show one-shot parse times and append-workload times reported by the harness.

One-shot parse (oneShotMs) — markdown-it-ts vs remark (lower is better):

  • 5,000 chars: 0.00ms vs 6.28ms → 38645.9× faster
  • 20,000 chars: 0.97ms vs 27.84ms → 28.6× faster
  • 50,000 chars: 2.59ms vs 77.26ms → 29.8× faster
  • 100,000 chars: 5.58ms vs 168.28ms → 30.2× faster
  • 200,000 chars: 12.35ms vs 436.27ms → 35.3× faster

Append workload (appendWorkloadMs) — markdown-it-ts vs remark:

  • 5,000 chars: 0.44ms vs 19.30ms → 43.9× faster
  • 20,000 chars: 1.25ms vs 91.79ms → 73.6× faster
  • 50,000 chars: 3.51ms vs 252.39ms → 72.0× faster
  • 100,000 chars: 17.95ms vs 568.25ms → 31.7× faster
  • 200,000 chars: 40.55ms vs 1304.01ms → 32.2× faster

Notes on interpretation

  • These numbers compare parse-only times produced by the project's perf harness. remark workflows often include additional tree transforms/plugins; real-world workloads may differ.
  • Results are machine- and content-dependent. For reproducible comparisons run the local harness and compare docs/perf-latest.json or the archived docs/perf-history/*.json files.
  • Source: docs/perf-history/perf-d660c6e.json (one-shot and appendWorkload values).

Reproduce locally

pnpm build
node scripts/quick-benchmark.mjs

This will update docs/perf-latest.md and refresh the snippet above.

Contributing

Contributions are welcome! Please open an issue or submit a pull request for any enhancements or bug fixes.

Acknowledgements

markdown-it-ts is a TypeScript re-implementation that stands on the shoulders of markdown-it. We are deeply grateful to the original project and its maintainers and contributors (notably Vitaly Puzrin and the markdown-it community). Many ideas, algorithms, renderer behaviors, specs, and fixtures originate from markdown-it; this project would not exist without that work.

License

This project is licensed under the MIT License. See the LICENSE file for more details.

About

A modern, type-safe rewrite of markdown-it in TypeScript: pluggable rules, split parse/render, CommonMark-compatible; fast one-shot parsing and even faster streaming/incremental updates.

Resources

License

Security policy

Stars

Watchers

Forks

Packages

No packages published

Languages

  • TypeScript 72.1%
  • JavaScript 27.9%