Working with the AST

all2md provides a powerful Abstract Syntax Tree (AST) API that enables advanced programmatic document manipulation. The AST separates document parsing from rendering, allowing you to analyze, transform, and render documents in different ways.

Why Use the AST?

The AST API is useful when you need to:

  • Analyze document structure: Extract headings, count tables, find all links

  • Transform documents: Change heading levels, rewrite links, modify content

  • Custom rendering: Generate different Markdown flavors from the same document

  • Programmatic manipulation: Build, modify, and combine documents programmatically

  • Persistent storage: Save/load document structure as JSON

The AST provides separation between:

  1. Parsing: Converting source documents to AST

  2. Manipulation: Working with structured document tree

  3. Rendering: Converting AST to Markdown (or other formats)

Getting Started

Converting to AST

Use to_ast() to convert any supported document to an AST:

from all2md import to_ast

# Convert PDF to AST
ast_doc = to_ast("document.pdf")

# Convert HTML to AST
ast_doc = to_ast("webpage.html")

# Convert DOCX to AST
ast_doc = to_ast("report.docx")

# With options (same as to_markdown)
from all2md.options import PdfOptions
ast_doc = to_ast("document.pdf", parser_options=PdfOptions(pages=[1, 2, 3]))

The AST Document Structure

Every AST starts with a Document node containing child nodes:

from all2md import to_ast

doc = to_ast("simple.md")

# Document properties
print(type(doc))  # <class 'all2md.ast.nodes.Document'>
print(doc.children)  # List of top-level nodes

# Access child nodes
for child in doc.children:
    print(type(child).__name__)  # Heading, Paragraph, etc.

Rendering AST to Markdown

Use MarkdownRenderer to convert AST back to Markdown:

from all2md import to_ast
from all2md.renderers.markdown import MarkdownRenderer

# Convert document to AST
doc = to_ast("document.pdf")

# Render to Markdown
renderer = MarkdownRenderer()
markdown = renderer.render_to_string(doc)
print(markdown)

# Render with specific flavor
from all2md.options import MarkdownRendererOptions

options = MarkdownRendererOptions(flavor="gfm")
renderer = MarkdownRenderer(options=options)
gfm_markdown = renderer.render_to_string(doc)

AST Node Types

Block Nodes

Block nodes represent block-level elements:

from all2md.ast import (
    Document,      # Root container
    Heading,       # Headings (h1-h6)
    Paragraph,     # Text paragraphs
    CodeBlock,     # Fenced code blocks
    BlockQuote,    # Blockquotes
    List,          # Ordered/unordered lists
    ListItem,      # List items
    Table,         # Tables
    TableRow,      # Table rows
    TableCell,     # Table cells
    ThematicBreak, # Horizontal rules
    HTMLBlock,     # Raw HTML blocks
    MathBlock,     # Display math blocks
    FootnoteDefinition,    # Footnote definitions
    DefinitionList,        # Definition lists
    DefinitionTerm,        # Definition list terms
    DefinitionDescription, # Definition list descriptions
    Comment        # Block-level comments
)

# Example: Create heading programmatically
from all2md.ast import Heading, Text

heading = Heading(
    level=1,
    content=[Text(content="Chapter 1")]
)

Inline Nodes

Inline nodes represent inline formatting:

from all2md.ast import (
    Text,          # Plain text
    Emphasis,      # Italic/emphasis
    Strong,        # Bold/strong
    Code,          # Inline code
    Link,          # Hyperlinks
    Image,         # Images
    LineBreak,     # Line breaks
    Strikethrough, # Strikethrough text
    Underline,     # Underlined text
    Mark,          # Highlighted/marked text (==text==)
    Superscript,   # Superscript
    Subscript,     # Subscript
    HTMLInline,    # Inline HTML
    MathInline,        # Inline math
    FootnoteReference, # Footnote references
    CommentInline      # Inline comments
)

# Example: Create link programmatically
from all2md.ast import Link, Text

link = Link(
    url="https://example.com",
    title="Example Site",
    content=[Text(content="Click here")]
)

Traversing the AST

Using NodeVisitor

The NodeVisitor pattern allows you to traverse and analyze the AST:

from all2md import to_ast
from all2md.ast import NodeVisitor, Heading, Link, Image

class DocumentAnalyzer(NodeVisitor):
    def __init__(self):
        self.headings = []
        self.links = []
        self.images = []

    def visit_heading(self, node: Heading):
        # Extract heading text
        text = ''.join(
            child.content for child in node.content
            if hasattr(child, 'content')
        )
        self.headings.append({
            'level': node.level,
            'text': text
        })
        # Continue visiting children
        self.generic_visit(node)

    def visit_link(self, node: Link):
        self.links.append({
            'url': node.url,
            'title': node.title
        })
        self.generic_visit(node)

    def visit_image(self, node: Image):
        self.images.append({
            'url': node.url,
            'alt': node.alt_text
        })
        self.generic_visit(node)

# Analyze document
doc = to_ast("document.pdf")
analyzer = DocumentAnalyzer()
doc.accept(analyzer)

# Print results
print(f"Found {len(analyzer.headings)} headings:")
for h in analyzer.headings:
    print(f"  {'#' * h['level']} {h['text']}")

print(f"\nFound {len(analyzer.links)} links:")
for link in analyzer.links:
    print(f"  {link['url']}")

Built-in Visitors

all2md includes useful pre-built visitors:

from all2md import to_ast
from all2md.ast import ValidationVisitor

# Validate AST structure
doc = to_ast("document.pdf")
validator = ValidationVisitor()

try:
    doc.accept(validator)
    print("AST is valid")
except ValueError as e:
    print(f"AST validation error: {e}")

Transforming the AST

Using NodeTransformer

Transform AST nodes by subclassing NodeTransformer:

from all2md import to_ast
from all2md.ast import NodeTransformer, Heading
from all2md.renderers.markdown import MarkdownRenderer

class IncreaseHeadingLevel(NodeTransformer):
    """Increase all heading levels by 1 (H1 -> H2, etc.)."""

    def visit_heading(self, node: Heading):
        # Don't exceed H6
        new_level = min(node.level + 1, 6)
        return Heading(
            level=new_level,
            content=self._transform_children(node.content)
        )

# Apply transformation
doc = to_ast("document.md")
transformer = IncreaseHeadingLevel()
transformed_doc = transformer.transform(doc)

# Render transformed document
renderer = MarkdownRenderer()
markdown = renderer.render_to_string(transformed_doc)

Built-in Transformers

all2md provides commonly-used transformers:

Heading Level Transformer:

from all2md import to_ast
from all2md.ast import HeadingLevelTransformer
from all2md.renderers.markdown import MarkdownRenderer

doc = to_ast("document.md")

# Increase all headings by 1 level
transformer = HeadingLevelTransformer(offset=1)
new_doc = transformer.transform(doc)

# Decrease all headings by 1 level
transformer = HeadingLevelTransformer(offset=-1)
new_doc = transformer.transform(doc)

Link Rewriter:

from all2md import to_ast
from all2md.ast import LinkRewriter
from all2md.renderers.markdown import MarkdownRenderer

doc = to_ast("document.md")

# Rewrite all links under /old-docs/ to /new-docs/
transformer = LinkRewriter(
    url_mapper=lambda url: url.replace('/old-docs/', '/new-docs/')
)
new_doc = transformer.transform(doc)

# Or use a named function
def rewrite_link(url: str) -> str:
    if url.startswith('http://'):
        return url.replace('http://', 'https://')
    return url

transformer = LinkRewriter(url_mapper=rewrite_link)
new_doc = transformer.transform(doc)

Text Replacer:

from all2md import to_ast
from all2md.ast import TextReplacer

doc = to_ast("document.md")

# Replace text across all text nodes
transformer = TextReplacer(
    pattern=r'\bcompany_name\b',
    replacement='Acme Corporation',
    use_regex=True
)
new_doc = transformer.transform(doc)

Filtering Nodes

Extract or remove specific node types:

from all2md import to_ast
from all2md.ast import filter_nodes, extract_nodes, Heading, Image

doc = to_ast("document.pdf")

# Remove all images
doc_without_images = filter_nodes(
    doc,
    lambda node: not isinstance(node, Image)
)

# Extract all headings
headings = extract_nodes(doc, Heading)
for heading in headings:
    print(f"Level {heading.level}: {heading.content}")

# Extract multiple types
from all2md.ast import Link
links_and_images = extract_nodes(doc, (Link, Image))

Collecting Nodes

Use extract_nodes() to gather specific node types:

from all2md import to_ast
from all2md.ast import extract_nodes, Heading, Table

doc = to_ast("report.pdf")

# Extract all headings
headings = extract_nodes(doc, Heading)
print(f"Found {len(headings)} headings")

# Extract all tables
tables = extract_nodes(doc, Table)
print(f"Found {len(tables)} tables")

For advanced filtering, use NodeCollector with a custom predicate:

from all2md import to_ast
from all2md.ast import NodeCollector, Heading

doc = to_ast("report.pdf")

# Collect headings with custom predicate
collector = NodeCollector(predicate=lambda n: isinstance(n, Heading) and n.level <= 2)
doc.accept(collector)
print(f"Found {len(collector.collected)} top-level headings")

Document Manipulation

all2md provides powerful APIs for manipulating document structure at the section level. These APIs allow you to query, extract, split, and modify documents programmatically.

Working with Sections

A section consists of a heading and all content until the next heading of equal or higher level:

from all2md import to_ast
from all2md.ast import get_all_sections

doc = to_ast("document.md")

# Get all sections in document
sections = get_all_sections(doc)

for section in sections:
    print(f"Level {section.level}: {section.get_heading_text()}")
    print(f"  Contains {len(section.content)} nodes")
    print(f"  Spans indices {section.start_index}-{section.end_index}")

Filter sections by level:

from all2md.ast import get_all_sections

# Get only top-level headings (H1)
h1_sections = get_all_sections(doc, min_level=1, max_level=1)

# Get H2 and H3 headings
subsections = get_all_sections(doc, min_level=2, max_level=3)

Querying Sections

Use query_sections() to find sections by various criteria:

from all2md import to_ast
from all2md.ast import query_sections

doc = to_ast("document.md")

# Find by heading text (case-insensitive by default)
intro = query_sections(doc, "Introduction")

# Find by index
first_section = query_sections(doc, 0)

# Find by multiple indices
specific_sections = query_sections(doc, [0, 2, 4])

# Find by predicate function
long_sections = query_sections(doc, lambda s: len(s.content) > 10)

# Find by level
all_h2 = query_sections(doc, level=2)

The query_sections() function returns a list of Section objects. Access section properties:

sections = query_sections(doc, "Methods")
if sections:
    section = sections[0]
    print(f"Heading: {section.get_heading_text()}")
    print(f"Level: {section.level}")
    print(f"Content nodes: {len(section.content)}")

    # Convert section to standalone document
    section_doc = section.to_document()

Extracting Sections

Extract sections as new documents:

from all2md import to_ast
from all2md.ast import extract_sections
from all2md.renderers.markdown import MarkdownRenderer

doc = to_ast("large_document.md")

# Extract single section
intro_doc = extract_sections(doc, "Introduction", combine=False)

# Extract multiple sections and combine them
selected = extract_sections(doc, ["Introduction", "Conclusion"])

# Extract by index
first_three = extract_sections(doc, [0, 1, 2])

# Extract using range notation
chapters = extract_sections(doc, "#:0-5")  # Sections 0 through 5

# Render extracted content
renderer = MarkdownRenderer()
markdown = renderer.render_to_string(intro_doc)

Wildcard Matching:

# Extract all sections starting with "Chapter"
chapters = extract_sections(doc, "Chapter*")

# Extract all "API" sections
api_sections = extract_sections(doc, "*API*")

Manipulating Sections

The Document class provides methods for adding, removing, and replacing sections:

Adding Sections:

from all2md import to_ast
from all2md.ast import Document, Heading, Paragraph, Text

doc = to_ast("document.md")

# Create new section
new_section = Document(children=[
    Heading(level=2, content=[Text(content="New Section")]),
    Paragraph(content=[Text(content="This is new content.")])
])

# Add after existing section
updated_doc = doc.add_section_after("Introduction", new_section)

# Add before existing section
updated_doc = doc.add_section_before("Conclusion", new_section)

# Add using section index
updated_doc = doc.add_section_after(0, new_section)

Removing Sections:

# Remove section by heading text
doc_without_intro = doc.remove_section("Introduction")

# Remove by index
doc_without_first = doc.remove_section(0)

# Remove multiple sections
for section_name in ["Appendix A", "Appendix B"]:
    try:
        doc = doc.remove_section(section_name)
    except ValueError:
        print(f"Section '{section_name}' not found")

Replacing Sections:

# Create replacement content
replacement = Document(children=[
    Heading(level=2, content=[Text(content="Updated Introduction")]),
    Paragraph(content=[Text(content="This replaces the old introduction.")])
])

# Replace section
updated_doc = doc.replace_section("Introduction", replacement)

Inserting Content into Sections:

from all2md.ast import Paragraph, Text

# Create new content
new_para = Paragraph(content=[
    Text(content="This paragraph is inserted into an existing section.")
])

# Insert at end of section (default)
doc = doc.insert_into_section("Methods", new_para, position="end")

# Insert at start of section
doc = doc.insert_into_section("Methods", new_para, position="start")

# Insert right after heading
doc = doc.insert_into_section("Methods", new_para, position="after_heading")

Generating Table of Contents

Generate a table of contents from document headings:

from all2md import to_ast
from all2md.ast import generate_toc, insert_toc
from all2md.renderers.markdown import MarkdownRenderer

doc = to_ast("document.md")

# Generate TOC as markdown string
toc_markdown = generate_toc(doc, max_level=3, style="markdown")
print(toc_markdown)

# Generate TOC as AST List node
toc_list = generate_toc(doc, max_level=3, style="list")

# Generate nested TOC (hierarchical structure)
nested_toc = generate_toc(doc, max_level=3, style="nested")

# Insert TOC into document
doc_with_toc = insert_toc(doc, position="start", max_level=3)

# Insert after first heading
doc_with_toc = insert_toc(doc, position="after_first_heading", max_level=3)

# Render document with TOC
renderer = MarkdownRenderer()
markdown = renderer.render_to_string(doc_with_toc)

Splitting Documents

Split documents into multiple parts using various strategies:

from all2md import to_ast
from all2md.ast import DocumentSplitter
from all2md.renderers.markdown import MarkdownRenderer

doc = to_ast("large_document.md")

# Split by sections (one document per section)
results = DocumentSplitter.split_by_sections(doc, include_preamble=True)

for result in results:
    print(f"Title: {result.title}")
    print(f"Words: {result.word_count}")
    print(f"Index: {result.index}")

    # Render each part
    renderer = MarkdownRenderer()
    markdown = renderer.render_to_string(result.document)

Split by Heading Level:

# Split at H1 boundaries (chapters)
chapters = DocumentSplitter.split_by_heading_level(doc, level=1)

# Split at H2 boundaries (sections)
sections = DocumentSplitter.split_by_heading_level(doc, level=2, include_preamble=True)

Split by Word Count:

# Split into chunks of approximately 500 words
chunks = DocumentSplitter.split_by_word_count(
    doc,
    target_words=500   # Approximate words per chunk (section boundaries preserved)
)

for i, chunk in enumerate(chunks, 1):
    print(f"Chunk {i}: {chunk.word_count} words")

Split into N Parts:

# Split document into 5 equal parts
parts = DocumentSplitter.split_by_parts(doc, num_parts=5)

for i, part in enumerate(parts, 1):
    print(f"Part {i}: {part.word_count} words")

Split by Thematic Breaks:

# Split on horizontal rules (---, ***, ___)
parts = DocumentSplitter.split_by_break(doc)

Working with Preamble

The preamble is content before the first heading:

from all2md import to_ast
from all2md.ast import get_preamble

doc = to_ast("document.md")

# Get preamble nodes
preamble = get_preamble(doc)

if preamble:
    print(f"Document has {len(preamble)} preamble nodes")

    # Extract text from preamble
    from all2md.ast.utils import extract_text
    preamble_text = extract_text(preamble)
    print(f"Preamble: {preamble_text}")

Practical Example: Document Restructuring

Combine multiple manipulation operations:

from all2md import to_ast
from all2md.ast import (
    query_sections, extract_sections, generate_toc, insert_toc,
    Document, Heading, Paragraph, Text
)
from all2md.renderers.markdown import MarkdownRenderer

# Load document
doc = to_ast("technical_report.md")

# Remove confidential sections
for confidential in ["Internal Notes", "Budget Details"]:
    try:
        doc = doc.remove_section(confidential)
    except ValueError:
        pass  # Section doesn't exist

# Add disclaimer at the beginning
disclaimer = Document(children=[
    Heading(level=2, content=[Text(content="Disclaimer")]),
    Paragraph(content=[Text(content="This is a public version of the report.")])
])
doc = doc.add_section_after(0, disclaimer)  # After title

# Insert table of contents
doc = insert_toc(doc, position="after_first_heading", max_level=2)

# Extract only executive summary and conclusions
public_doc = extract_sections(
    doc,
    ["Executive Summary", "Conclusions"],
    combine=True
)

# Render final document
renderer = MarkdownRenderer()
markdown = renderer.render_to_string(public_doc)

# Save to file
from pathlib import Path
Path("public_report.md").write_text(markdown)

Building AST Programmatically

Creating Documents from Scratch

Build documents programmatically using AST nodes:

from all2md.ast import (
    Document, Heading, Paragraph, Text,
    Strong, Emphasis, Link, CodeBlock
)
from all2md.renderers.markdown import MarkdownRenderer

# Build document structure
doc = Document(children=[
    Heading(level=1, content=[Text(content="User Guide")]),

    Paragraph(content=[
        Text(content="Welcome to "),
        Strong(content=[Text(content="all2md")]),
        Text(content=", a powerful document converter.")
    ]),

    Heading(level=2, content=[Text(content="Installation")]),

    CodeBlock(
        language="bash",
        content="pip install all2md[pdf]"
    ),

    Paragraph(content=[
        Text(content="For more information, visit "),
        Link(
            url="https://all2md.readthedocs.io",
            content=[Text(content="the documentation")]
        ),
        Text(content=".")
    ])
])

# Render to Markdown
renderer = MarkdownRenderer()
markdown = renderer.render_to_string(doc)
print(markdown)

Using Document Builders

all2md provides builders for complex structures:

TableBuilder:

from all2md.ast import TableBuilder, Text
from all2md.renderers.markdown import MarkdownRenderer

# Build table programmatically
builder = TableBuilder()

# Add header row
builder.add_row(
    [Text(content="Name"), Text(content="Age"), Text(content="City")],
    is_header=True,
    alignments=[None, 'right', 'left']
)

# Add data rows
builder.add_row([
    Text(content="Alice"),
    Text(content="30"),
    Text(content="New York")
])
builder.add_row([
    Text(content="Bob"),
    Text(content="25"),
    Text(content="San Francisco")
])

# Get table
table = builder.get_table()

# Render
renderer = MarkdownRenderer()
markdown = renderer.render_to_string(table)

ListBuilder:

from all2md.ast import ListBuilder, Text

# Build nested list
builder = ListBuilder()

builder.add_item(level=1, ordered=False, content=[Text(content="Python")])
builder.add_item(level=1, ordered=False, content=[Text(content="JavaScript")])

# Add nested items (level 2)
builder.add_item(level=2, ordered=True, content=[Text(content="ES6")])
builder.add_item(level=2, ordered=True, content=[Text(content="TypeScript")])

# Back to top level
builder.add_item(level=1, ordered=False, content=[Text(content="Rust")])

doc = builder.get_document()
list_node = doc.children[0]

DocumentBuilder:

from all2md.ast import DocumentBuilder, Text

# Fluent API for building documents
builder = DocumentBuilder()

(builder
    .add_heading(1, [Text(content="Title")])
    .add_paragraph([Text(content="Introduction paragraph.")])
    .add_heading(2, [Text(content="Section 1")])
    .add_paragraph([Text(content="Content here.")])
)

doc = builder.get_document()

Merging Documents

Combine multiple documents into one:

from all2md import to_ast
from all2md.ast import merge_documents
from all2md.renderers.markdown import MarkdownRenderer

# Convert multiple documents
doc1 = to_ast("chapter1.md")
doc2 = to_ast("chapter2.md")
doc3 = to_ast("chapter3.md")

# Merge into single document
combined = merge_documents([doc1, doc2, doc3])

# Render combined document
renderer = MarkdownRenderer()
markdown = renderer.render_to_string(combined)

AST Serialization

Saving AST as JSON

Persist AST structure for later use:

from all2md import to_ast
from all2md.ast import ast_to_json, json_to_ast
from pathlib import Path

# Convert document to AST
doc = to_ast("document.pdf")

# Save as JSON
json_str = ast_to_json(doc)
Path("document.ast.json").write_text(json_str)

# Later: load from JSON
loaded_json = Path("document.ast.json").read_text()
restored_doc = json_to_ast(loaded_json)

# Render restored document
from all2md.renderers.markdown import MarkdownRenderer
renderer = MarkdownRenderer()
markdown = renderer.render_to_string(restored_doc)

Dictionary Format

Work with AST as dictionaries:

from all2md import to_ast
from all2md.ast import ast_to_dict, dict_to_ast

doc = to_ast("document.md")

# Convert to dictionary
doc_dict = ast_to_dict(doc)

# Inspect structure
print(doc_dict.keys())  # ['type', 'children', ...]

# Modify dictionary
doc_dict['metadata'] = {'author': 'Jane Doe'}

# Restore to AST
modified_doc = dict_to_ast(doc_dict)

Markdown Flavors

Rendering Different Flavors

Render the same AST in different Markdown dialects:

from all2md import to_ast
from all2md.renderers.markdown import MarkdownRenderer
from all2md.options import MarkdownRendererOptions

doc = to_ast("document.pdf")

# GitHub Flavored Markdown
gfm_renderer = MarkdownRenderer(options=MarkdownRendererOptions(flavor="gfm"))
gfm_md = gfm_renderer.render_to_string(doc)

# CommonMark (strict)
cm_renderer = MarkdownRenderer(options=MarkdownRendererOptions(flavor="commonmark"))
cm_md = cm_renderer.render_to_string(doc)

# Markdown Plus (extended features)
mdp_renderer = MarkdownRenderer(options=MarkdownRendererOptions(flavor="markdown_plus"))
mdp_md = mdp_renderer.render_to_string(doc)

Flavor Differences

Different flavors support different features:

# GFM (flavor="gfm") supports:
# - Tables
# - Strikethrough
# - Task lists
# - Automatic URL linking

# CommonMark (flavor="commonmark") supports:
# - Core Markdown only
# - No tables (rendered as HTML)
# - No strikethrough (rendered as HTML)
# - Strict spec compliance

Practical Examples

Example 1: Generate Table of Contents

Extract headings and build TOC:

from all2md import to_ast
from all2md.ast import NodeVisitor, Heading
from all2md.ast.utils import extract_text

class TOCGenerator(NodeVisitor):
    def __init__(self):
        self.toc = []

    def visit_heading(self, node: Heading):
        # Extract text from heading using official utility
        text = extract_text(node.content)

        # Create anchor from text
        anchor = text.lower().replace(' ', '-')

        # Add to TOC with indentation
        indent = '  ' * (node.level - 1)
        self.toc.append(f"{indent}- [{text}](#{anchor})")

        self.generic_visit(node)

# Generate TOC
doc = to_ast("document.pdf")
generator = TOCGenerator()
doc.accept(generator)

print("# Table of Contents\n")
print('\n'.join(generator.toc))

Example 2: Document Statistics

Analyze document content:

from all2md import to_ast
from all2md.ast import NodeVisitor, Heading, Paragraph, Table, Image, Link
from all2md.ast.utils import extract_text

class DocumentStats(NodeVisitor):
    def __init__(self):
        self.stats = {
            'headings': [],
            'paragraphs': 0,
            'tables': 0,
            'images': 0,
            'links': 0,
            'words': 0
        }

    def visit_heading(self, node: Heading):
        text = extract_text(node.content)
        self.stats['headings'].append({
            'level': node.level,
            'text': text
        })
        self.generic_visit(node)

    def visit_paragraph(self, node: Paragraph):
        self.stats['paragraphs'] += 1
        # Count words
        text = extract_text(node.content)
        self.stats['words'] += len(text.split())
        self.generic_visit(node)

    def visit_table(self, node: Table):
        self.stats['tables'] += 1
        self.generic_visit(node)

    def visit_image(self, node: Image):
        self.stats['images'] += 1
        self.generic_visit(node)

    def visit_link(self, node: Link):
        self.stats['links'] += 1
        self.generic_visit(node)

# Analyze document
doc = to_ast("report.pdf")
stats = DocumentStats()
doc.accept(stats)

print(f"Document Statistics:")
print(f"  Headings: {len(stats.stats['headings'])}")
print(f"  Paragraphs: {stats.stats['paragraphs']}")
print(f"  Tables: {stats.stats['tables']}")
print(f"  Images: {stats.stats['images']}")
print(f"  Links: {stats.stats['links']}")
print(f"  Words: {stats.stats['words']}")

Example 4: Custom Renderer

Create a custom renderer for specific output:

from all2md import to_ast
from all2md.ast import NodeVisitor, Heading, Paragraph, Strong, Emphasis, Text

class PlainTextRenderer(NodeVisitor):
    """Render AST as plain text (no formatting)."""

    def __init__(self):
        self.output = []

    def visit_heading(self, node: Heading):
        # Add heading text with level prefix
        text = self._extract_text(node.content)
        prefix = '#' * node.level
        self.output.append(f"{prefix} {text}\n")

    def visit_paragraph(self, node: Paragraph):
        text = self._extract_text(node.content)
        self.output.append(f"{text}\n\n")

    def visit_text(self, node: Text):
        # Plain text nodes handled in _extract_text
        pass

    def _extract_text(self, nodes):
        text = []
        for node in nodes:
            if isinstance(node, Text):
                text.append(node.content)
            elif isinstance(node, (Strong, Emphasis)):
                text.append(self._extract_text(node.content))
            elif hasattr(node, 'content') and isinstance(node.content, list):
                text.append(self._extract_text(node.content))
        return ''.join(text)

    def render(self, doc):
        doc.accept(self)
        return ''.join(self.output)

# Use custom renderer
doc = to_ast("document.pdf")
renderer = PlainTextRenderer()
plain_text = renderer.render(doc)
print(plain_text)

Best Practices

Performance Considerations

  1. AST Conversion: Converting to AST has overhead. Use to_markdown() directly if you don’t need AST features.

  2. Transformation: Clone nodes only when necessary. Transformers create new trees by default.

  3. Serialization: JSON serialization is best for persistence, not real-time processing.

Error Handling

from all2md import to_ast, FormatError
from all2md.ast import ValidationVisitor

try:
    doc = to_ast("document.pdf")

    # Validate structure
    validator = ValidationVisitor()
    doc.accept(validator)

except FormatError as e:
    print(f"Unsupported format: {e}")
except ValueError as e:
    print(f"Invalid AST structure: {e}")

Thread Safety

Transformers do not mutate their input — they return new trees — so a source AST can be shared safely across threads. (Node objects are plain mutable dataclasses, so avoid mutating a shared node in place.) This makes the transform-and-render pattern below safe for concurrent processing:

from concurrent.futures import ThreadPoolExecutor
from all2md import to_ast
from all2md.ast import HeadingLevelTransformer
from all2md.renderers.markdown import MarkdownRenderer

def transform_document(file_path):
    doc = to_ast(file_path)
    transformer = HeadingLevelTransformer(offset=1)
    new_doc = transformer.transform(doc)
    renderer = MarkdownRenderer()
    return renderer.render_to_string(new_doc)

# Safe concurrent processing
files = ['doc1.pdf', 'doc2.pdf', 'doc3.pdf']
with ThreadPoolExecutor() as executor:
    results = list(executor.map(transform_document, files))

For more examples, see the Recipes and Cookbook cookbook. For API reference, see all2md.ast.