Parsing and AST

multimark exposes the full abstract syntax tree (AST) that cmark-gfm builds during parsing. This lets you inspect document structure, extract content, transform nodes, and render subtrees. This is all backed by the same C library that powers the renderers.

Parsing to an AST

The parse() function takes a Markdown string and returns the root Node of the parsed tree.

from multimark import parse, NodeType

doc = parse("# Hello\n\nA paragraph with **bold** text.\n")
doc
<Node document>

The root node is always a DOCUMENT. Its children are the top-level block elements.

for child in doc.children:
    print(child)
<Node heading level=1>
<Node paragraph>

Walking the Tree

The walk() method performs a depth-first traversal, yielding (event, node) pairs. Container nodes produce both "enter" and "exit" events; leaf nodes produce only "enter".

for event, node in doc.walk():
    if event == "enter":
        print(f"{node.type_string:>15}  {node.literal or ''}")
       document  
        heading  
           text  Hello
      paragraph  
           text  A paragraph with 
         strong  
           text  bold
           text   text.

This is the primary way to scan or transform a document. For example, extracting all links:

md = "See [Python](https://python.org) and [Rust](https://rust-lang.org).\n"
doc = parse(md)

links = [
    (node.url, node.first_child.literal)
    for event, node in doc.walk()
    if event == "enter" and node.type == NodeType.LINK
]
links
[('https://python.org', 'Python'), ('https://rust-lang.org', 'Rust')]

Node Properties

Each node type exposes relevant properties. Headings have a heading_level, links have url and title, code blocks have fence_info, and text nodes have literal.

doc = parse("## Installation\n\nRun `pip install multimark`.\n")

for event, node in doc.walk():
    if event == "enter" and node.type == NodeType.HEADING:
        print(f"Level {node.heading_level}: ", end="")
        print(node.first_child.literal)
Level 2: Installation

Source positions are available on every node:

doc = parse("First paragraph.\n\nSecond paragraph.\n")

for child in doc.children:
    print(f"Lines {child.start_line}{child.end_line}: {child.type_string}")
Lines 1–1: paragraph
Lines 3–3: paragraph

Mutating the Tree

Nodes support in-place mutation. You can change properties, add or remove nodes, and then render the modified tree.

from multimark import Node

doc = parse("Visit [example](http://example.com) for details.\n")

for event, node in doc.walk():
    if node.type == NodeType.LINK and node.url.startswith("http://"):
        node.url = node.url.replace("http://", "https://", 1)

print(doc.render_html())
<p>Visit <a href="https://example.com">example</a> for details.</p>

You can also build nodes from scratch:

doc = parse("Some content.\n")

heading = Node.new(NodeType.HEADING)
heading.heading_level = 2
text = Node.new(NodeType.TEXT)
text.literal = "Table of Contents"
heading.append_child(text)
doc.prepend_child(heading)

print(doc.render_html())
<h2>Table of Contents</h2>
<p>Some content.</p>

Rendering Subtrees

Any node can render itself independently, which is useful for extracting fragments.

doc = parse("# Title\n\nFirst paragraph.\n\nSecond paragraph.\n")

second_para = list(doc.children)[2]
print(second_para.render_html())
<p>Second paragraph.</p>

All five output formats are available: render_html(), render_xml(), render_latex(), render_man(), and render_commonmark().

Streaming Parser

For large documents or streamed input, the Parser class provides incremental feeding.

from multimark import Parser

with Parser(smart=True) as p:
    p.feed("# Chapter 1\n\n")
    p.feed("Some text with \"quotes\" and -- dashes.\n")
    print(p.render_html())
<h1>Chapter 1</h1>
<p>Some text with “quotes” and – dashes.</p>

The Parser also works as a context manager for automatic resource cleanup, and its finish() method returns the root Node for AST access:

with Parser(extensions=["table"]) as p:
    p.feed("| A | B |\n|---|---|\n| 1 | 2 |\n")
    doc = p.finish()
    print(doc.render_html())
<table>
<thead>
<tr>
<th>A</th>
<th>B</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>2</td>
</tr>
</tbody>
</table>

GFM Extensions with the AST

Extensions like tables, strikethrough, and task lists produce their own node types in the tree. These work with both parse() and Parser.

doc = parse(
    "- [x] Done\n- [ ] Pending\n",
    extensions=["tasklist"],
)

for event, node in doc.walk():
    if event == "enter":
        print(f"{node.type_string}: {node.literal or ''}")
document: 
list: 
tasklist: 
paragraph: 
text: Done
tasklist: 
paragraph: 
text: Pending

Common Patterns

Extract all headings for a table of contents

doc = parse("# Intro\n\n## Setup\n\n### Details\n\n## Usage\n")

toc = []
for event, node in doc.walk():
    if event == "enter" and node.type == NodeType.HEADING:
        level = node.heading_level
        text = node.first_child.literal if node.first_child else ""
        toc.append((level, text))

toc
[(1, 'Intro'), (2, 'Setup'), (3, 'Details'), (2, 'Usage')]

Strip all formatting and extract plain text

doc = parse("Hello **world**, visit [here](https://x.com).\n")

texts = [
    node.literal
    for event, node in doc.walk()
    if event == "enter" and node.type == NodeType.TEXT
]
" ".join(texts)
'Hello  world , visit  here .'