Incremental Markdown parser and compiler
Dependencies
npm install @mizchi/markdownimport { 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("See https://example.com/docs\n");
// => '<p>See <a href="https://example.com/docs">https://example.com/docs</a></p>\n'
// Disable bare URL links when you need plain text output
const plain = toHtml("See https://example.com/docs\n", { autolink: false });
// => "<p>See https://example.com/docs</p>\n"
// Normalize markdown
const normalized = toMarkdown("# Hello\n\n\n\nWorld");
// => "# Hello\n\nWorld\n"import { parse, toHtml } from "@mizchi/markdown";
const ast = parse("[[MoonBit#syntax|MoonBit syntax]]", { wikilinks: true });
// ast.children[0].children[0].type === "wikiLink"
const html = toHtml("[[MoonBit|MoonBit notes]]", { wikilinks: true });
// => '<p><a href="MoonBit">MoonBit notes</a></p>\n'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();import { parse, Document, Block, Inline } from "@mizchi/markdown";
const ast: Document = parse("# Hello");
const heading = ast.children[0] as HeadingBlock;
console.log(heading.level); // 1moon add mizchi/markdown// 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")
let linked = @markdown.md_to_html("See https://example.com/docs\n")
let plain = @markdown.md_to_html("See https://example.com/docs\n", autolink=false)
// Enable the WikiLink extension explicitly
let wiki_html = @markdown.md_to_html("[[MoonBit|MoonBit notes]]", wikilinks=true)// 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.documentimport {
"mizchi/markdown",
"mizchi/markdown/x/mdx" @mdx,
}
let schema = @mdx.MdxSchema::closed([
@mdx.ComponentSchema::new(
"Fold",
[
@mdx.PropSchema::required("id", @mdx.MdxValueType::Text),
@mdx.PropSchema::required(
"kind",
@mdx.MdxValueType::OneOf(["concept", "procedure"]),
),
@mdx.PropSchema::optional("requires", @mdx.MdxValueType::TextList),
],
),
])
let source = #|<Fold id="auth.mfa" kind="procedure" requires={["auth.login"]} />
let checked = @mdx.type_check_mdx(@markdown.parse(source).document, schema)
let is_valid = checked.is_valid()pnpm install
moon build --target js
pnpm exec vitepnpm add @mizchi/markdown @luna_ui/lunaimport { SyntaxHighlightEditor } from "@mizchi/markdown/editor";
import "@mizchi/markdown/editor/style.css";
<SyntaxHighlightEditor
value={() => markdown}
onChange={(next) => setMarkdown(next)}
/>;import { loadHighlighter } from "@mizchi/markdown/highlight";
const highlightMoonBit = await loadHighlighter("moonbit");
const html = highlightMoonBit?.("fn main { println(\"hi\") }");| Document | Full Parse | Incremental | Speedup |
|---|---|---|---|
| 10 paragraphs | 68.89µs | 7.36µs | 9.4x |
| 50 paragraphs | 327.99µs | 8.67µs | 37.8x |
| 100 paragraphs | 651.14µs | 15.25µs | 42.7x |
pub(all) enum Block {
ThematicBreak(marker~ : Char, count~ : Int, span~ : Span, leading_trivia~ : Trivia, trailing_trivia~ : Trivia)
Heading(level~ : Int, style~ : HeadingStyle, children~ : Array[Inline], closing_hashes~ : Int, span~ : Span, leading_trivia~ : Trivia, trailing_trivia~ : Trivia)
Paragraph(children~ : Array[Inline], span~ : Span, leading_trivia~ : Trivia, trailing_trivia~ : Trivia)
FencedCode(fence_marker~ : FenceMarker, fence_length~ : Int, info~ : String, code~ : String, indent~ : Int, span~ : Span, leading_trivia~ : Trivia, trailing_trivia~ : Trivia)
IndentedCode(code~ : String, span~ : Span, leading_trivia~ : Trivia, trailing_trivia~ : Trivia)
Blockquote(children~ : Array[Block], span~ : Span, leading_trivia~ : Trivia, trailing_trivia~ : Trivia)
BulletList(marker~ : BulletMarker, tight~ : Bool, items~ : Array[ListItem], span~ : Span, leading_trivia~ : Trivia, trailing_trivia~ : Trivia)
OrderedList(start~ : Int, delimiter~ : OrderedDelimiter, tight~ : Bool, items~ : Array[ListItem], span~ : Span, leading_trivia~ : Trivia, trailing_trivia~ : Trivia)
HtmlBlock(html~ : String, span~ : Span, leading_trivia~ : Trivia, trailing_trivia~ : Trivia)
Table(header~ : Array[TableCell], alignments~ : Array[TableAlign], rows~ : Array[Array[TableCell]], span~ : Span, leading_trivia~ : Trivia, trailing_trivia~ : Trivia)
BlankLines(count~ : Int, span~ : Span)
FootnoteDefinition(label~ : String, children~ : Array[Block], span~ : Span, leading_trivia~ : Trivia, trailing_trivia~ : Trivia)
}impl Show for BulletMarkerpub(all) struct CodeBlockInfo {
lang : String
filename : String
meta : String
}pub(all) struct EditInfo {
offset : Int
old_len : Int
new_len : Int
}impl Show for EmphasisMarkerimpl Show for FenceMarkerimpl Show for HardBreakStyleimpl Show for HeadingStylepub(all) struct IncrementalResult {
document : Document
reused_before : Int
reparsed : Int
reused_after : Int
}pub(all) enum Inline {
Text(content~ : String, span~ : Span)
SoftBreak(span~ : Span)
HardBreak(style~ : HardBreakStyle, span~ : Span)
Emphasis(marker~ : EmphasisMarker, children~ : Array[Inline], span~ : Span)
Strong(marker~ : EmphasisMarker, children~ : Array[Inline], span~ : Span)
Strikethrough(children~ : Array[Inline], span~ : Span)
Code(content~ : String, backtick_count~ : Int, span~ : Span)
WikiLink(target~ : String, label~ : String, fragment~ : String, span~ : Span)
Link(children~ : Array[Inline], url~ : String, title~ : String, span~ : Span)
RefLink(children~ : Array[Inline], label~ : String, span~ : Span)
Autolink(url~ : String, is_email~ : Bool, span~ : Span)
Image(alt~ : String, url~ : String, title~ : String, span~ : Span)
RefImage(alt~ : String, label~ : String, span~ : Span)
HtmlInline(html~ : String, span~ : Span)
FootnoteReference(label~ : String, span~ : Span)
}impl Show for OrderedDelimiterfn RenderOptions::with_highlighter(highlighter : (CodeBlockInfo, String) -> String) -> RenderOptionsfn RenderOptions::with_simple_highlighter(highlighter : (String, String) -> String) -> RenderOptionsimpl Show for TableAlignfn is_unicode_punctuation(c : Char) -> Boolfn is_unicode_whitespace(c : Char) -> Boolfn md_parse_and_render(source : String, strict? : Bool, wikilinks? : Bool) -> Stringfn md_to_html(source : String, wikilinks? : Bool, autolink? : Bool) -> Stringfn md_to_html_literal(source : String, wikilinks? : Bool, positions? : Bool, image_preview? : Bool) -> Stringfn md_to_html_strict(source : String, wikilinks? : Bool, autolink? : Bool) -> Stringfn parse_incremental(old_doc : Document, old_source : String, new_source : String, edit : EditInfo, wikilinks? : Bool) -> IncrementalResultIncremental Markdown parser and compiler
Dependencies