markdown

Incremental Markdown parser and compiler

markdown
parser
cst
incremental
gfm
moon add mizchi/markdown@0.7.4
Download zip
Author
Version
0.7.4
License
MIT
Last updated
18 days ago
Downloads
13K
README

#@mizchi/markdown

CST-based incremental Markdown parser for JavaScript/MoonBit.

A cross-platform (JS/WASM/native) Markdown compiler optimized for real-time editing with incremental parsing.

#Features

  • Fast: Edit-position based incremental updates inspired by CRDTs Go Brrr. Optimized for speed over edge-case correctness CommonMark 207/542
  • Lossless CST: Preserves all whitespace, markers, and formatting
  • Incremental parsing: Re-parses only changed blocks (up to 42x faster)
  • GFM: GitHub Flavored Markdown support (tables, task lists, strikethrough)
  • Cross-platform: Works on JS, WASM-GC, and native targets
  • HTML rendering: Built-in HTML renderer with remark-html compatible output
  • mdast compatible: AST follows mdast specification


#JavaScript API

npm install @mizchi/markdown

#Usage

import { parse, toHtml, toMarkdown } from "@mizchi/markdown"; // Parse to AST const ast = parse("# Hello\n\n**Bold** text"); console.log(ast.children[0].type); // "heading" // Convert to HTML const html = toHtml("# Hello\n\n**Bold** text"); // => "<h1>Hello</h1>\n<p><strong>Bold</strong> text</p>\n" // Normalize markdown const normalized = toMarkdown("# Hello\n\n\n\nWorld"); // => "# Hello\n\nWorld\n"

#Incremental Parsing

For real-time editing scenarios:

import { createDocument, insertEdit } from "@mizchi/markdown"; // Create document handle const doc = createDocument("# Hello"); // Access AST, HTML, or Markdown console.log(doc.ast); // Parsed AST console.log(doc.toHtml()); // "<h1>Hello</h1>\n" console.log(doc.toMarkdown()); // "# Hello\n" // Incremental update (faster than full re-parse) const edit = insertEdit(7, 6); // Insert 6 chars at position 7 const newDoc = doc.update("# Hello World", edit); // Free resources when done doc.dispose(); newDoc.dispose();

#TypeScript Support

Full TypeScript definitions are included:

import { parse, Document, Block, Inline } from "@mizchi/markdown"; const ast: Document = parse("# Hello"); const heading = ast.children[0] as HeadingBlock; console.log(heading.level); // 1


#MoonBit API

#Installation

moon add mizchi/markdown

#Usage

// Parse markdown
let result = @markdown.parse("# Hello\n\nWorld")
let doc = result.document

// Serialize back (lossless)
let output = @markdown.serialize(doc)

// Render to HTML
let html = @markdown.render_html(doc)

// Or use convenience function
let html = @markdown.md_to_html("# Hello\n\nWorld")

#Incremental Parsing

// Initial parse
let result = @markdown.parse(source)
let doc = result.document

// Create edit info
let edit = @markdown.EditInfo::replace(
change_start, // Start position
old_length, // Length of replaced text
new_length // Length of new text
)

// Incremental update (reuses unchanged blocks)
let inc_result = @markdown.parse_incremental(doc, old_source, new_source, edit)
let new_doc = inc_result.document


#Playground

pnpm install moon build --target js pnpm exec vite

#Performance

DocumentFull ParseIncrementalSpeedup
10 paragraphs68.89µs7.36µs9.4x
50 paragraphs327.99µs8.67µs37.8x
100 paragraphs651.14µs15.25µs42.7x

#Documentation

See docs/markdown.md for detailed architecture and design.

#CommonMark Compatibility

This parser handles most common Markdown syntax correctly and works well for typical use cases like documentation, blog posts, and notes.

However, some edge cases (deeply nested structures, unusual delimiter combinations) are not fully CommonMark compliant. If you need strict CommonMark compliance, consider using cmark.mbt or other fully compliant parsers.

#License

MIT

#
AgentId

pub(all) type AgentId Int

Agent識別子 (簡易版: Int)
impl Eq for AgentId
impl Show for AgentId

#
AgentId::inner

#deprecated("Use `struct T(A)` to declare a newtype and use `.0` access the underlying type instead.")
fn AgentId::inner(self : AgentId) -> Int
Convert newtype to its underlying type, automatically derived.

#
Block

pub(all) enum Block {
ThematicBreak(Char, Int, Span, Trivia, Trivia)
Heading(Int, HeadingStyle, Array[Inline], Int, Span, Trivia, Trivia)
Paragraph(Array[Inline], Span, Trivia, Trivia)
FencedCode(FenceMarker, Int, String, String, Int, Span, Trivia, Trivia)
IndentedCode(String, Span, Trivia, Trivia)
Blockquote(Array[Block], Span, Trivia, Trivia)
BulletList(BulletMarker, Bool, Array[ListItem], Span, Trivia, Trivia)
OrderedList(Int, OrderedDelimiter, Bool, Array[ListItem], Span, Trivia, Trivia)
HtmlBlock(String, Span, Trivia, Trivia)
Table(Array[TableCell], Array[TableAlign], Array[Array[TableCell]], Span, Trivia, Trivia)
BlankLines(Int, Span)
FootnoteDefinition(String, Array[Block], Span, Trivia, Trivia)
}

Block-level nodes

#
BulletMarker

pub(all) enum BulletMarker {
Dash
Asterisk
Plus
}

List marker for unordered lists: -, *, +
impl Eq for BulletMarker

#
CodeBlockInfo

pub struct CodeBlockInfo {
lang : String
filename : String
meta : String
}

"ts:index.ts {highlight=[1,3]}" → (lang="ts", filename="index.ts", meta="{highlight=[1,3]}")

#
CrdtDocument

pub(all) struct CrdtDocument {
runs : Array[TextRun]
gen : IdGenerator
}

実験用ドキュメント構造

#
CrdtDocument::active_count

fn CrdtDocument::active_count(self : CrdtDocument) -> Int

アクティブなRun数

#
CrdtDocument::delete

fn CrdtDocument::delete(self : CrdtDocument, idx : Int) -> Unit

削除(Tombstone化)

#
CrdtDocument::get_text

fn CrdtDocument::get_text(self : CrdtDocument) -> String

アクティブなテキストを取得

#
CrdtDocument::insert

fn CrdtDocument::insert(self : CrdtDocument, text : String) -> LogicalId

テキスト挿入

#
CrdtDocument::new

fn CrdtDocument::new(agent : AgentId) -> CrdtDocument

#
CrdtDocument::total_count

fn CrdtDocument::total_count(self : CrdtDocument) -> Int

全Run数(Tombstone含む)

#
CrdtSpan

pub(all) struct CrdtSpan {
start_id : LogicalId
end_id : LogicalId
cached_from : Int
cached_to : Int
}

CRDT対応Span (キャッシュ付き)

#
CrdtSpan::new

fn CrdtSpan::new(start_id : LogicalId, end_id : LogicalId, from : Int, to : Int) -> CrdtSpan

#
Document

pub(all) struct Document {
frontmatter : Frontmatter?
children : Array[Block]
span : Span
}

Document root

#
EditInfo

pub(all) struct EditInfo {
offset : Int
old_len : Int
new_len : Int
}

Using #valtype to avoid heap allocation for this small struct

#
EditInfo::delete

fn EditInfo::delete(offset : Int, len : Int) -> EditInfo

Create an edit info for deletion

#
EditInfo::insert

fn EditInfo::insert(offset : Int, len : Int) -> EditInfo

Create an edit info for insertion

#
EditInfo::replace

fn EditInfo::replace(offset : Int, old_len : Int, new_len : Int) -> EditInfo

Create an edit info for replacement

#
EmphasisMarker

pub(all) enum EmphasisMarker {
Asterisk
Underscore
}

Emphasis marker: * or _

#
FenceMarker

pub(all) enum FenceMarker {
Backtick
Tilde
}

Fence marker for code blocks: ``` or ~~~
impl Eq for FenceMarker
impl Show for FenceMarker

#
Frontmatter

pub(all) struct Frontmatter {
raw : String
entries : Array[(String, String)]
span : Span
}

Frontmatter (YAML)

#
HardBreakStyle

pub(all) enum HardBreakStyle {
TwoSpaces
Backslash
}

Hard break style

#
HeadingStyle

pub(all) enum HeadingStyle {
Atx
Setext
}

Heading style
impl Eq for HeadingStyle

#
IdGenerator

pub(all) struct IdGenerator {
agent : AgentId
next_seq : Int
}

LogicalId生成器

#
IdGenerator::new

fn IdGenerator::new(agent : AgentId) -> IdGenerator

#
IdGenerator::next

fn IdGenerator::next(self : IdGenerator) -> LogicalId

#
IncrementalResult

pub(all) struct IncrementalResult {
document : Document
reused_before : Int
reparsed : Int
reused_after : Int
}

Result of incremental parsing

#
Inline

pub(all) enum Inline {
Text(String, Span)
SoftBreak(Span)
HardBreak(HardBreakStyle, Span)
Emphasis(EmphasisMarker, Array[Inline], Span)
Strong(EmphasisMarker, Array[Inline], Span)
Strikethrough(Array[Inline], Span)
Code(String, Int, Span)
Link(Array[Inline], String, String, Span)
RefLink(Array[Inline], String, Span)
Autolink(String, Bool, Span)
Image(String, String, String, Span)
RefImage(String, String, Span)
HtmlInline(String, Span)
FootnoteReference(String, Span)
}

Inline-level nodes

#
LinkDefinition

pub(all) struct LinkDefinition {
label : String
url : String
title : String
span : Span
}

Link reference definition [label]: url "title"

#
ListItem

pub(all) struct ListItem {
children : Array[Block]
checked : Bool?
marker_offset : Int
content_offset : Int
span : Span
}

List item

#
LogicalId

pub(all) struct LogicalId {
agent : AgentId
seq : Int
}

論理ID: (agent, seq) ペア
impl Eq for LogicalId
impl Show for LogicalId

#
LogicalId::compare

fn LogicalId::compare(self : LogicalId, other : LogicalId) -> Int

LogicalId の順序比較 (Lamport順序)

#
LogicalId::le

fn LogicalId::le(self : LogicalId, other : LogicalId) -> Bool

#
LogicalId::lt

fn LogicalId::lt(self : LogicalId, other : LogicalId) -> Bool

#
OrderedDelimiter

pub(all) enum OrderedDelimiter {
Dot
Paren
}

Ordered list delimiter: . or )

#
ParseResult

pub(all) struct ParseResult {
document : Document
definitions : Array[LinkDefinition]
}

Parse result

#
RenderOptions

pub struct RenderOptions {
code_highlighter : (CodeBlockInfo, String) -> String?
}

Render options with plugin support

#
RenderOptions::default

fn RenderOptions::default() -> RenderOptions

Create default render options

#
RenderOptions::with_highlighter

fn RenderOptions::with_highlighter(highlighter : (CodeBlockInfo, String) -> String) -> RenderOptions

Create render options with code highlighter

#
RenderOptions::with_simple_highlighter

fn RenderOptions::with_simple_highlighter(highlighter : (String, String) -> String) -> RenderOptions

Create render options with simple highlighter (lang, code) -> String

#
Scanner

pub(all) struct Scanner {
source : String
chars : Array[Char]
pos : Int
len : Int
utf16_offsets : Array[Int]?
}

Scanner state - uses Array[Char] for O(1) character access Handles Unicode correctly by tracking UTF-16 offsets for non-BMP characters

#
Scanner::advance

fn Scanner::advance(self : Scanner, n : Int) -> Unit

Advance position by n characters

#
Scanner::consume

fn Scanner::consume(self : Scanner) -> Char?

Consume and return current character (O(1))

#
Scanner::consume_str

fn Scanner::consume_str(self : Scanner, s : String) -> Bool

Match and consume a string

#
Scanner::count_char

fn Scanner::count_char(self : Scanner, c : Char) -> Int

Count consecutive occurrences of a character from current position - O(1) per char

#
Scanner::count_leading_spaces

fn Scanner::count_leading_spaces(self : Scanner) -> Int

Count leading spaces (without advancing) - O(1) per char

#
Scanner::is_blank_line

fn Scanner::is_blank_line(self : Scanner) -> Bool

Check if current line is blank (only whitespace) - O(1) per char

#
Scanner::is_eof

fn Scanner::is_eof(self : Scanner) -> Bool

Check if at end of input

#
Scanner::matches

fn Scanner::matches(self : Scanner, s : String) -> Bool

Match a string at current position - optimized with Array[Char]

#
Scanner::new

fn Scanner::new(source : String) -> Scanner

Create a new scanner

#
Scanner::peek

fn Scanner::peek(self : Scanner) -> Char?

Peek current character (O(1) with Array[Char])

#
Scanner::peek_at

fn Scanner::peek_at(self : Scanner, offset : Int) -> Char?

Peek character at offset from current position (O(1))

#
Scanner::read_line

fn Scanner::read_line(self : Scanner) -> String

Read until end of line (not consuming newline) - O(1) per char

#
Scanner::remaining

fn Scanner::remaining(self : Scanner) -> String

Get remaining substring from current position

#
Scanner::restore

fn Scanner::restore(self : Scanner, pos : Int) -> Unit

Restore to saved position

#
Scanner::save

fn Scanner::save(self : Scanner) -> Int

Save current position

#
Scanner::skip_line

fn Scanner::skip_line(self : Scanner) -> Unit

Skip to next line (consuming newline if present) - O(1) per char

#
Scanner::skip_spaces

fn Scanner::skip_spaces(self : Scanner) -> Int

Skip whitespace (space and tab only) - O(1) per char

#
Scanner::substring

fn Scanner::substring(self : Scanner, start : Int, end : Int) -> String

Get substring from start to end position (code point indices)

#
Span

pub(all) struct Span {
from : Int
to : Int
}

Using #valtype to avoid heap allocation for this frequently-used small struct
impl Eq for Span
impl Show for Span

#
Span::empty

fn Span::empty() -> Span

Empty span (for synthetic nodes)

#
Span::new

fn Span::new(from : Int, to : Int) -> Span

Create a span

#
TableAlign

pub(all) enum TableAlign {
Left
Center
Right
None
}

Table alignment
impl Eq for TableAlign
impl Show for TableAlign

#
TableCell

pub(all) struct TableCell {
children : Array[Inline]
span : Span
}

Table cell

#
TextRun

pub(all) struct TextRun {
id : LogicalId
content : String
deleted : Bool
}

TextRun: 連続テキストの圧縮表現

#
Trivia

pub(all) struct Trivia {
content : String
}

Trivia represents non-semantic characters that should be preserved
impl Eq for Trivia
impl Show for Trivia

#
Trivia::empty

fn Trivia::empty() -> Trivia

#
Trivia::new

fn Trivia::new(content : String) -> Trivia

#
char_is

fn char_is(opt : Char?, c : Char) -> Bool

Check if char option matches specific char

#
char_is_digit

fn char_is_digit(opt : Char?) -> Bool

Check if char option is a digit

#
is_alphanumeric

fn is_alphanumeric(c : Char) -> Bool

Check if character is alphanumeric

#
is_digit

fn is_digit(c : Char) -> Bool

Check if character is a digit

#
is_letter

fn is_letter(c : Char) -> Bool

Check if character is ASCII letter

#
is_punctuation

fn is_punctuation(c : Char) -> Bool

Check if character is a punctuation mark

#
is_whitespace

fn is_whitespace(c : Char) -> Bool

Check if character is ASCII whitespace

#
md_free

fn md_free(handle : Int) -> Unit

Free a document handle to release memory

#
md_parse

fn md_parse(source : String) -> Int

Returns 0 on error

#
md_parse_and_render

fn md_parse_and_render(source : String, strict? : Bool) -> String

When strict=true, uses full CommonMark compliance (slower)

#
md_parse_incremental

fn md_parse_incremental(handle : Int, new_source : String, change_start : Int, old_end : Int, new_end : Int) -> Int

Returns new document handle, or 0 on error

#
md_render_to_html

fn md_render_to_html(handle : Int) -> String

Returns empty string if handle is invalid

#
md_render_to_string

fn md_render_to_string(handle : Int) -> String

Returns empty string if handle is invalid

#
md_to_html

fn md_to_html(source : String) -> String

Parse markdown and render to HTML

#
md_to_html_strict

fn md_to_html_strict(source : String) -> String

Parse markdown and render to HTML (strict mode)

#
parse

fn parse(source : String, strict? : Bool) -> ParseResult

When strict=false (default), uses fast single-pass parser

#
parse_code_block_info

fn parse_code_block_info(info : String) -> CodeBlockInfo

Parse code block info string into components

#
parse_incremental

fn parse_incremental(old_doc : Document, old_source : String, new_source : String, edit : EditInfo) -> IncrementalResult

Parse incrementally using edit hint

#
parse_inlines

fn parse_inlines(text : String, strict? : Bool) -> Array[Inline]

When strict=false (default), uses fast single-pass parser

#
parse_inlines_multipass

fn parse_inlines_multipass(text : String) -> Array[Inline]

Parse all inlines using multi-pass approach

#
render_html

fn render_html(doc : Document) -> String

Render a document to HTML

#
serialize

fn serialize(doc : Document) -> String

Serialize document to markdown string

#
serialize_definitions

fn serialize_definitions(defs : Array[LinkDefinition]) -> String

Serialize link definitions