Building ASTs

Build markdown documents programmatically using Node constructors instead of parsing text. This is useful for:

  • Generating documents from data: Create reports, documentation, or content from databases, APIs, or computation results
  • Template systems: Build document structures that get filled with dynamic content
  • Document transformation: Modify parsed documents or create new ones based on existing ASTs
  • Testing: Create specific AST structures for unit tests
import CommonMark as CM
Tip

Use import CommonMark as CM to reduce verbosity in code that builds ASTs.

How It Works

The AST is a tree of Node objects. Each node has:

  • A container type (e.g., Document, Paragraph, Strong) that defines what kind of element it is
  • Children for container nodes (blocks contain blocks or inlines; inlines contain inlines or text)
  • A literal string for leaf nodes like Text, Code, and CodeBlock
  • Optional metadata for attributes, IDs, and classes

Nodes are created with Node(Type, children...) where strings automatically become Text nodes:

doc = CM.Node(CM.Document,
    CM.Node(CM.Heading, 1, "Hello World"),
    CM.Node(CM.Paragraph, "Welcome to ", CM.Node(CM.Strong, "CommonMark"), "!")
)
CM.html(doc)
"<h1>Hello World</h1>\n<p>Welcome to <strong>CommonMark</strong>!</p>\n"

The constructed AST can be rendered to any output format: html(), latex(), markdown(), term(), typst(), or json().

Block Containers

Block elements form the document structure: paragraphs, headings, lists, etc. They occupy their own vertical space in the rendered output.

Document

The root container. Every AST must have a Document as its root node. It can contain any block elements except list items.

doc = CM.Node(CM.Document,
    CM.Node(CM.Paragraph, "First paragraph."),
    CM.Node(CM.Paragraph, "Second paragraph.")
)
CM.html(doc)
"<p>First paragraph.</p>\n<p>Second paragraph.</p>\n"

Paragraph

The basic text container. Paragraphs hold inline content: text, emphasis, links, code spans, etc. Most text content lives inside paragraphs.

p = CM.Node(CM.Paragraph,
    "Plain text, ",
    CM.Node(CM.Emph, "italic"),
    ", and ",
    CM.Node(CM.Strong, "bold"),
    "."
)
doc = CM.Node(CM.Document, p)
CM.html(doc)
"<p>Plain text, <em>italic</em>, and <strong>bold</strong>.</p>\n"

Heading

Section headings with levels 1-6. The first argument is the level (1 = h1, 2 = h2, etc.), followed by the heading content.

doc = CM.Node(CM.Document,
    CM.Node(CM.Heading, 1, "Main Title"),
    CM.Node(CM.Paragraph, "Introduction paragraph."),
    CM.Node(CM.Heading, 2, "First Section"),
    CM.Node(CM.Paragraph, "Section content."),
    CM.Node(CM.Heading, 3, "Subsection")
)
CM.html(doc)
"<h1>Main Title</h1>\n<p>Introduction paragraph.</p>\n<h2>First Section</h2>\n<p>Section content.</p>\n<h3>Subsection</h3>\n"

Headings can contain inline formatting:

doc = CM.Node(CM.Document,
    CM.Node(CM.Heading, 2, "The ", CM.Node(CM.Code, "Node"), " API")
)
CM.html(doc)
"<h2>The <code>Node</code> API</h2>\n"

BlockQuote

Quoted content, typically rendered with indentation or a vertical bar. Block quotes can contain any block elements including nested quotes.

doc = CM.Node(CM.Document,
    CM.Node(CM.BlockQuote,
        CM.Node(CM.Paragraph, "To be or not to be."),
        CM.Node(CM.Paragraph, "— Shakespeare")
    )
)
CM.html(doc)
"<blockquote>\n<p>To be or not to be.</p>\n<p>— Shakespeare</p>\n</blockquote>\n"

Nested block quotes:

doc = CM.Node(CM.Document,
    CM.Node(CM.BlockQuote,
        CM.Node(CM.Paragraph, "Outer quote"),
        CM.Node(CM.BlockQuote,
            CM.Node(CM.Paragraph, "Inner quote")
        )
    )
)
CM.html(doc)
"<blockquote>\n<p>Outer quote</p>\n<blockquote>\n<p>Inner quote</p>\n</blockquote>\n</blockquote>\n"

List and Item

Lists contain Item nodes. By default, lists are unordered (bullet points). Use keyword arguments to customize:

KeywordTypeDefaultDescription
orderedBoolfalseNumbered list instead of bullets
startInt1Starting number for ordered lists
tightBooltrueTight spacing (no <p> tags around items)

Unordered list:

doc = CM.Node(CM.Document,
    CM.Node(CM.List,
        CM.Node(CM.Item, CM.Node(CM.Paragraph, "First item")),
        CM.Node(CM.Item, CM.Node(CM.Paragraph, "Second item")),
        CM.Node(CM.Item, CM.Node(CM.Paragraph, "Third item"))
    )
)
CM.html(doc)
"<ul>\n<li>First item</li>\n<li>Second item</li>\n<li>Third item</li>\n</ul>\n"

Ordered list starting at 5:

doc = CM.Node(CM.Document,
    CM.Node(CM.List,
        CM.Node(CM.Item, CM.Node(CM.Paragraph, "Fifth")),
        CM.Node(CM.Item, CM.Node(CM.Paragraph, "Sixth"));
        ordered=true, start=5
    )
)
CM.html(doc)
"<ol start=\"5\">\n<li>Fifth</li>\n<li>Sixth</li>\n</ol>\n"

Nested lists:

doc = CM.Node(CM.Document,
    CM.Node(CM.List,
        CM.Node(CM.Item,
            CM.Node(CM.Paragraph, "Outer item"),
            CM.Node(CM.List,
                CM.Node(CM.Item, CM.Node(CM.Paragraph, "Nested item 1")),
                CM.Node(CM.Item, CM.Node(CM.Paragraph, "Nested item 2"))
            )
        )
    )
)
CM.html(doc)
"<ul>\n<li>Outer item\n<ul>\n<li>Nested item 1</li>\n<li>Nested item 2</li>\n</ul>\n</li>\n</ul>\n"

CodeBlock

Fenced code blocks for displaying source code. The info keyword specifies the language for syntax highlighting.

doc = CM.Node(CM.Document,
    CM.Node(CM.CodeBlock, "function greet(name)\n    println(\"Hello, \$name!\")\nend"; info="julia")
)
CM.html(doc)
"<pre><code class=\"language-julia\">function greet(name)\n    println(&quot;Hello, \$name!&quot;)\nend</code></pre>\n"

Without a language:

doc = CM.Node(CM.Document,
    CM.Node(CM.CodeBlock, "Plain text code block\nNo syntax highlighting")
)
CM.html(doc)
"<pre><code>Plain text code block\nNo syntax highlighting</code></pre>\n"

ThematicBreak

A horizontal rule that separates sections. Takes no children or arguments.

doc = CM.Node(CM.Document,
    CM.Node(CM.Paragraph, "Content above the break."),
    CM.Node(CM.ThematicBreak),
    CM.Node(CM.Paragraph, "Content below the break.")
)
CM.html(doc)
"<p>Content above the break.</p>\n<hr />\n<p>Content below the break.</p>\n"

HtmlBlock

Raw HTML that passes through unchanged. Use for content that can't be expressed in markdown, like complex layouts or embedded widgets.

doc = CM.Node(CM.Document,
    CM.Node(CM.HtmlBlock, "<div class=\"warning\">\n  <strong>Warning:</strong> Custom HTML content.\n</div>")
)
CM.html(doc)
"<div class=\"warning\">\n  <strong>Warning:</strong> Custom HTML content.\n</div>\n"

Inline Containers

Inline elements flow within text: emphasis, links, code spans, etc. They don't create line breaks by themselves.

Emph and Strong

Emphasis (<em>, typically italic) and strong emphasis (<strong>, typically bold). Both are containers that can hold other inline content.

doc = CM.Node(CM.Document,
    CM.Node(CM.Paragraph,
        "This is ",
        CM.Node(CM.Emph, "emphasized"),
        " and this is ",
        CM.Node(CM.Strong, "strongly emphasized"),
        "."
    )
)
CM.html(doc)
"<p>This is <em>emphasized</em> and this is <strong>strongly emphasized</strong>.</p>\n"

Nested emphasis:

doc = CM.Node(CM.Document,
    CM.Node(CM.Paragraph,
        CM.Node(CM.Strong, "Bold with ", CM.Node(CM.Emph, "italic"), " inside")
    )
)
CM.html(doc)
"<p><strong>Bold with <em>italic</em> inside</strong></p>\n"

Code

Inline code spans for mentioning code within text. The argument is the literal code string.

doc = CM.Node(CM.Document,
    CM.Node(CM.Paragraph,
        "Call ",
        CM.Node(CM.Code, "process(data)"),
        " to transform the input."
    )
)
CM.html(doc)
"<p>Call <code>process(data)</code> to transform the input.</p>\n"

Hyperlinks with a destination URL and optional title. Links are containers—their children become the link text.

KeywordTypeRequiredDescription
destStringYesURL or path
titleStringNoTooltip text
doc = CM.Node(CM.Document,
    CM.Node(CM.Paragraph,
        "Visit the ",
        CM.Node(CM.Link, "official website"; dest="https://example.com", title="Example Site"),
        " for more information."
    )
)
CM.html(doc)
"<p>Visit the <a href=\"https://example.com\" title=\"Example Site\">official website</a> for more information.</p>\n"

Links can contain formatting:

doc = CM.Node(CM.Document,
    CM.Node(CM.Paragraph,
        CM.Node(CM.Link,
            "Click ",
            CM.Node(CM.Strong, "here"),
            " to continue";
            dest="/next"
        )
    )
)
CM.html(doc)
"<p><a href=\"/next\">Click <strong>here</strong> to continue</a></p>\n"

Image

Images with source URL and alt text. Unlike links, images don't contain children— the alt text is specified as a keyword argument.

KeywordTypeRequiredDescription
destStringYesImage URL or path
altStringNoAlternative text
titleStringNoTooltip text
doc = CM.Node(CM.Document,
    CM.Node(CM.Paragraph,
        "Here's a diagram: ",
        CM.Node(CM.Image; dest="diagram.png", alt="Architecture diagram")
    )
)
CM.html(doc)
"<p>Here's a diagram: <img src=\"diagram.png\" alt=\"\" /></p>\n"

SoftBreak and LineBreak

SoftBreak represents a line break in the source that becomes a space or newline depending on output format. LineBreak is a hard break (<br> in HTML).

doc = CM.Node(CM.Document,
    CM.Node(CM.Paragraph,
        "First line",
        CM.Node(CM.SoftBreak),
        "continues here (soft break becomes space)."
    ),
    CM.Node(CM.Paragraph,
        "Line one",
        CM.Node(CM.LineBreak),
        "Line two (hard break)"
    )
)
CM.html(doc)
"<p>First line\ncontinues here (soft break becomes space).</p>\n<p>Line one<br />\nLine two (hard break)</p>\n"

HtmlInline

Raw inline HTML for special formatting not available in markdown. Use paired tags for spans or single tags for elements like <br>.

doc = CM.Node(CM.Document,
    CM.Node(CM.Paragraph,
        "Text with ",
        CM.Node(CM.HtmlInline, "<mark>"),
        "highlighted",
        CM.Node(CM.HtmlInline, "</mark>"),
        " content."
    )
)
CM.html(doc)
"<p>Text with <mark>highlighted</mark> content.</p>\n"

Extension Types

Extension nodes represent syntax beyond standard CommonMark. They can be built programmatically regardless of whether the corresponding parser rule is enabled.

Math

Inline math (Math) and display math blocks (DisplayMath) for LaTeX-style equations. The argument is the LaTeX expression without delimiters.

doc = CM.Node(CM.Document,
    CM.Node(CM.Paragraph,
        "Einstein's famous equation: ",
        CM.Node(CM.Math, "E = mc^2"),
        "."
    ),
    CM.Node(CM.DisplayMath, "\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}")
)
CM.html(doc)
"<p>Einstein's famous equation: <span class=\"math tex\">\\(E = mc^2\\)</span>.</p>\n<div class=\"display-math tex\">\\[\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}\\]</div>"

Strikethrough, Mark, Subscript, Superscript

Text formatting for deleted text, highlighted text, subscripts, and superscripts. All are containers that hold inline content.

doc = CM.Node(CM.Document,
    CM.Node(CM.Paragraph,
        CM.Node(CM.Strikethrough, "removed"),
        ", ",
        CM.Node(CM.Mark, "highlighted"),
        ", H",
        CM.Node(CM.Subscript, "2"),
        "O, x",
        CM.Node(CM.Superscript, "2")
    )
)
CM.html(doc)
"<p><del>removed</del>, <mark>highlighted</mark>, H<sub>2</sub>O, x<sup>2</sup></p>\n"

Admonition

Callout boxes for notes, warnings, tips, and other highlighted content. Takes a category, title, and block children.

doc = CM.Node(CM.Document,
    CM.Node(CM.Admonition, "warning", "Important",
        CM.Node(CM.Paragraph, "This operation cannot be undone.")
    ),
    CM.Node(CM.Admonition, "tip", "Pro Tip",
        CM.Node(CM.Paragraph, "Use keyboard shortcuts for efficiency.")
    )
)
CM.html(doc)
"<div class=\"admonition warning\"><p class=\"admonition-title\">Important</p>\n<p>This operation cannot be undone.</p>\n</div><div class=\"admonition tip\"><p class=\"admonition-title\">Pro Tip</p>\n<p>Use keyboard shortcuts for efficiency.</p>\n</div>"

FencedDiv

Generic container divs with CSS classes and IDs. Useful for custom styling or semantic grouping.

KeywordTypeDescription
classString or Vector{String}CSS class(es)
idStringElement ID
doc = CM.Node(CM.Document,
    CM.Node(CM.FencedDiv,
        CM.Node(CM.Paragraph, "Important content here.");
        class=["highlight", "important"], id="section-1"
    )
)
CM.html(doc)
"<div class=\"highlight important\" id=\"section-1\">\n<p>Important content here.</p>\n</div>\n"

Footnotes

Footnote definitions and references. When building a Document, footnote links are automatically connected to their definitions.

doc = CM.Node(CM.Document,
    CM.Node(CM.Paragraph,
        "This claim needs a citation",
        CM.Node(CM.FootnoteLink, "1"),
        "."
    ),
    CM.Node(CM.FootnoteDefinition, "1",
        CM.Node(CM.Paragraph, "Source: Journal of Examples, 2024.")
    )
)
CM.html(doc)
"<p>This claim needs a citation<a href=\"#footnote-1\" class=\"footnote\">1</a>.</p>\n<div class=\"footnote\" id=\"footnote-1\"><p class=\"footnote-title\">1</p>\n<p>Source: Journal of Examples, 2024.</p>\n</div>"

GitHubAlert

GitHub-flavored alert blocks. Categories: note, tip, important, warning, caution. The title defaults to the capitalized category.

doc = CM.Node(CM.Document,
    CM.Node(CM.GitHubAlert, "note",
        CM.Node(CM.Paragraph, "This is informational.")
    ),
    CM.Node(CM.GitHubAlert, "warning",
        CM.Node(CM.Paragraph, "Proceed with caution!"); title="Danger Zone"
    )
)
CM.html(doc)
"<div class=\"github-alert note\"><p class=\"github-alert-title\">Note</p>\n<p>This is informational.</p>\n</div>\n<div class=\"github-alert warning\"><p class=\"github-alert-title\">Danger Zone</p>\n<p>Proceed with caution!</p>\n</div>\n"

TaskItem

Checkbox list items for task lists. Use checked=true for completed items. TaskItems go inside a List just like regular Item nodes.

doc = CM.Node(CM.Document,
    CM.Node(CM.Heading, 2, "Todo"),
    CM.Node(CM.List,
        CM.Node(CM.TaskItem, CM.Node(CM.Paragraph, "Write documentation")),
        CM.Node(CM.TaskItem, CM.Node(CM.Paragraph, "Add tests"); checked=true),
        CM.Node(CM.TaskItem, CM.Node(CM.Paragraph, "Release"))
    )
)
CM.html(doc)
"<h2>Todo</h2>\n<ul>\n<li><input type=\"checkbox\" disabled> Write documentation</li>\n<li><input type=\"checkbox\" disabled checked> Add tests</li>\n<li><input type=\"checkbox\" disabled> Release</li>\n</ul>\n"

Table

Tables with headers and data rows. Build from TableHeader, TableBody, TableRow, and TableCell components.

The align keyword on Table sets column alignments: :left, :center, or :right.

header = CM.Node(CM.TableHeader,
    CM.Node(CM.TableRow,
        CM.Node(CM.TableCell, "Name"),
        CM.Node(CM.TableCell, "Value"),
        CM.Node(CM.TableCell, "Status")
    )
)
row1 = CM.Node(CM.TableRow,
    CM.Node(CM.TableCell, "Alpha"),
    CM.Node(CM.TableCell, "100"),
    CM.Node(CM.TableCell, CM.Node(CM.Strong, "Active"))
)
row2 = CM.Node(CM.TableRow,
    CM.Node(CM.TableCell, "Beta"),
    CM.Node(CM.TableCell, "200"),
    CM.Node(CM.TableCell, "Pending")
)
doc = CM.Node(CM.Document,
    CM.Node(CM.Table, header, row1, row2; align=[:left, :right, :center])
)
CM.html(doc)
"<table><thead><tr><td align=\"left\">Name</td><td align=\"left\">Value</td><td align=\"left\">Status</td></tr></thead><tbody><tr><td align=\"left\">Alpha</td><td align=\"left\">100</td><td align=\"left\"><strong>Active</strong></td></tr><tr><td align=\"left\">Beta</td><td align=\"left\">200</td><td align=\"left\">Pending</td></tr></tbody></table>"

Citation

Academic citation references. Requires bibliography configuration for proper rendering.

doc = CM.Node(CM.Document,
    CM.Node(CM.Paragraph,
        "According to ",
        CM.Node(CM.Citation, "smith2020"),
        ", this approach is effective."
    )
)
CM.html(doc)
"<p>According to <span class=\"citation\"><a href=\"#ref-smith2020\">@smith2020</a></span>, this approach is effective.</p>\n"

Raw Content

Format-specific content that passes through only to matching output formats. Available types: LaTeXInline, LaTeXBlock, TypstInline, TypstBlock.

doc = CM.Node(CM.Document,
    CM.Node(CM.Paragraph,
        "This ",
        CM.Node(CM.LaTeXInline, "\\textbf{bold}"),
        " only appears in LaTeX output."
    )
)
CM.latex(doc)
"This \\textbf{bold} only appears in LaTeX output.\\par\n"

Reference-style links preserve the original syntax for roundtripping. These nodes are produced when parsing with ReferenceLinkRule enabled.

doc = CM.Node(CM.Document,
    CM.Node(CM.Paragraph,
        "See the ",
        CM.Node(CM.ReferenceLink, "docs"; dest="https://example.com", label="docs"),
        " or ",
        CM.Node(CM.ReferenceLink, "click here"; dest="https://example.com", label="docs", style=:full),
        "."
    ),
    CM.Node(CM.ReferenceDefinition; label="docs", dest="https://example.com")
)
CM.markdown(doc)
"See the [docs][docs] or [click here][docs].\n\n[docs]: https://example.com\n"
TypeChildrenKeyword Args
ReferenceLinklink textdest, label, title="", style=:full
ReferenceImagealt textdest, label, title="", style=:full
ReferenceDefinitionlabel, dest, title=""
UnresolvedReferencelink textlabel, style=:shortcut, image=false

The style can be :full, :collapsed, or :shortcut.

Tree Manipulation

After constructing nodes, you can modify the tree structure using these functions:

FunctionDescription
append_child(parent, child)Add child as last child of parent
prepend_child(parent, child)Add child as first child of parent
insert_after(node, sibling)Insert sibling immediately after node
insert_before(node, sibling)Insert sibling immediately before node
unlink(node)Remove node from its parent
isnull(node)Check if node is the null node (empty reference)
text(string)Create a Text node with the given content

Building Incrementally

Instead of nesting everything in the constructor, you can build nodes step by step:

# Create empty containers
doc = CM.Node(CM.Document)
para = CM.Node(CM.Paragraph)

# Add content
CM.append_child(para, CM.text("Hello "))
CM.append_child(para, CM.Node(CM.Strong, "world"))
CM.append_child(para, CM.text("!"))

# Attach to document
CM.append_child(doc, para)

CM.html(doc)
"<p>Hello <strong>world</strong>!</p>\n"

Modifying Existing Trees

You can also modify trees created by the parser:

# Parse a document
parser = CM.Parser()
doc = parser("# Original Title\n\nSome content.")

# Find and modify nodes
for (node, entering) in doc
    if entering && node.t isa CM.Heading
        # Prepend "Chapter: " to all headings
        CM.prepend_child(node, CM.text("Chapter: "))
    end
end

CM.html(doc)
"<h1>Chapter: Original Title</h1>\n<p>Some content.</p>\n"

Complete Example

A realistic document combining multiple element types:

doc = CM.Node(CM.Document,
    CM.Node(CM.Heading, 1, "CommonMark.jl User Guide"),

    CM.Node(CM.Paragraph,
        "CommonMark.jl is a ",
        CM.Node(CM.Strong, "fast"),
        " and ",
        CM.Node(CM.Strong, "spec-compliant"),
        " markdown parser for Julia."
    ),

    CM.Node(CM.Admonition, "tip", "Quick Start",
        CM.Node(CM.Paragraph,
            "Install with ",
            CM.Node(CM.Code, "Pkg.add(\"CommonMark\")"),
            "."
        )
    ),

    CM.Node(CM.Heading, 2, "Features"),

    CM.Node(CM.List,
        CM.Node(CM.Item, CM.Node(CM.Paragraph,
            CM.Node(CM.Link, "Multiple output formats"; dest="#outputs"),
            ": HTML, LaTeX, Typst, terminal"
        )),
        CM.Node(CM.Item, CM.Node(CM.Paragraph,
            "Extensible parser with ",
            CM.Node(CM.Code, "enable!"),
            " and ",
            CM.Node(CM.Code, "disable!")
        )),
        CM.Node(CM.Item, CM.Node(CM.Paragraph, "Full CommonMark specification support"))
    ),

    CM.Node(CM.Heading, 2, "Example"),

    CM.Node(CM.CodeBlock, """
using CommonMark

parser = Parser()
enable!(parser, TableRule())

ast = parser("| A | B |\\n|---|---|\\n| 1 | 2 |")
html(ast)"""; info="julia"),

    CM.Node(CM.Paragraph,
        "See the ",
        CM.Node(CM.Link, "API Reference"; dest="api.html"),
        " for complete documentation."
    )
)

CM.html(doc)
"<h1>CommonMark.jl User Guide</h1>\n<p>CommonMark.jl is a <strong>fast</strong> and <strong>spec-compliant</strong> markdown parser for Julia.</p>\n<div class=\"admonition tip\"><p class=\"admonition-title\">Quick Start</p>\n<p>Install with <code>Pkg.add(&quot;CommonMark&quot;)</co" ⋯ 255 bytes ⋯ ">\n<h2>Example</h2>\n<pre><code class=\"language-julia\">using CommonMark\n\nparser = Parser()\nenable!(parser, TableRule())\n\nast = parser(&quot;| A | B |\\n|---|---|\\n| 1 | 2 |&quot;)\nhtml(ast)</code></pre>\n<p>See the <a href=\"api.html\">API Reference</a> for complete documentation.</p>\n"

Converting from Julia Markdown

CommonMark.jl can convert Julia's stdlib Markdown.MD AST to CommonMark's Node AST. This enables migration of existing documentation or integration with tools that produce stdlib Markdown.

using Markdown  # Load first to trigger extension
using CommonMark
import CommonMark: Node

# Parse with Julia's stdlib
md = Markdown.parse("# Hello World\n\nThis is **bold** and *italic* text.")

# Convert to CommonMark AST
ast = Node(md)

# Render to any format
CommonMark.html(ast)
"<h1>Hello World</h1>\n<p>This is <strong>bold</strong> and <em>italic</em> text.</p>\n"

The conversion handles all stdlib element types:

Stdlib TypeCommonMark Type
MDDocument
ParagraphParagraph
Header{N}Heading (level N)
BoldStrong
ItalicEmph
Code (inline)Code
Code (block)CodeBlock
BlockQuoteBlockQuote
ListList + Item
HorizontalRuleThematicBreak
LinkLink
ImageImage
LineBreakLineBreak
TableTable hierarchy
AdmonitionAdmonition
FootnoteFootnoteDefinition / FootnoteLink
LaTeXMath

Metadata from the stdlib MD object is preserved:

md = Markdown.MD([Markdown.Paragraph(["Content"])])
md.meta[:title] = "My Document"
md.meta[:author] = "Author Name"

ast = Node(md)
ast.meta["title"], ast.meta["author"]
("My Document", "Author Name")

The converted AST works with all CommonMark.jl output formats:

md = Markdown.parse("Visit [Julia](https://julialang.org).")
ast = Node(md)

println("HTML:     ", CommonMark.html(ast))
println("LaTeX:    ", CommonMark.latex(ast))
println("Markdown: ", CommonMark.markdown(ast))
HTML:     <p>Visit <a href="https://julialang.org">Julia</a>.</p>

LaTeX:    Visit \href{https://julialang.org}{Julia}.\par

Markdown: Visit [Julia](https://julialang.org).

Converting to/from MarkdownAST.jl

CommonMark.jl supports bidirectional conversion with MarkdownAST.jl, enabling interoperability between the two AST representations.

CommonMark → MarkdownAST

using MarkdownAST
using CommonMark

cm = CommonMark.Parser()("# Hello **world**")
mast = MarkdownAST.Node(cm)
@ast MarkdownAST.Document() do
  MarkdownAST.Heading(1) do
    MarkdownAST.Text("Hello ")
    MarkdownAST.Strong() do
      MarkdownAST.Text("world")
    end
  end
end

MarkdownAST → CommonMark

using MarkdownAST: @ast

mast = @ast MarkdownAST.Document() do
    MarkdownAST.Heading(1) do
        "Hello"
    end
    MarkdownAST.Paragraph() do
        "Some "
        MarkdownAST.Strong() do
            "bold"
        end
        " text."
    end
end

cm = CommonMark.Node(mast)
CommonMark.html(cm)
"<h1>Hello</h1>\n<p>Some <strong>bold</strong> text.</p>\n"

Supported Type Mappings

CommonMarkMarkdownAST
DocumentDocument
ParagraphParagraph
HeadingHeading
BlockQuoteBlockQuote
ListList
ItemItem
CodeBlockCodeBlock
ThematicBreakThematicBreak
HtmlBlockHTMLBlock
TextText
SoftBreakSoftBreak
LineBreakLineBreak
CodeCode
EmphEmph
StrongStrong
LinkLink
ImageImage
HtmlInlineHTMLInline
BackslashBackslash
Table, TableHeader, TableBody, TableRow, TableCellTable hierarchy
AdmonitionAdmonition
MathInlineMath
DisplayMathDisplayMath
FootnoteDefinitionFootnoteDefinition
FootnoteLinkFootnoteLink
JuliaValue, JuliaExpressionJuliaValue

Unsupported types generate a warning and are skipped during conversion.

Pandoc JSON Round-Trip

CommonMark.jl can convert ASTs to and from Pandoc's JSON format, enabling interoperability with Pandoc's ecosystem and lossless round-tripping.

Export to Dict

Use json(Dict, ast) to get the Pandoc AST as a dictionary without JSON string serialization:

import CommonMark as CM

parser = CM.Parser()
ast = parser("# Hello\n\nWorld with **bold**.")

d = CM.json(Dict, ast)
keys(d)
KeySet for a Dict{String, Any} with 3 entries. Keys:
  "pandoc-api-version"
  "meta"
  "blocks"

The dictionary contains Pandoc's standard keys:

d["pandoc-api-version"]
3-element Vector{Int64}:
  1
 23
  1

Import from Dict

Convert a Pandoc AST dictionary back to a CommonMark Node:

ast2 = CM.Node(d)
CM.html(ast2)
"<h1>Hello</h1>\n<p>World with <strong>bold</strong>.</p>\n"

Round-Trip Example

This enables lossless round-tripping through the Pandoc format:

original = parser("A [link](url.md) and `code`.")
roundtrip = CM.Node(CM.json(Dict, original))

CM.markdown(original) == CM.markdown(roundtrip)
true

Deterministic Output

For deterministic key ordering (useful for diffs and tests), pass an ordered dict type:

using OrderedCollections
d = json(OrderedDict, ast)  # Keys in consistent order

Use Cases

  • Pandoc integration: Modify AST between CommonMark parsing and Pandoc output
  • Serialization: Store ASTs in databases or send over networks
  • Testing: Compare AST structures programmatically
  • Migration: Convert from other tools that export Pandoc JSON