Custom Templates with Jinja2
all2md includes a powerful Jinja2-based template renderer that allows you to create custom output formats without writing any Python code. Simply write a Jinja2 template and all2md will handle the rest.
Overview
The Jinja2 renderer provides a flexible way to transform documents into any text-based format. Whether you need XML, YAML, custom markup, or even terminal output with ANSI codes, you can achieve it by writing a template.
Key capabilities:
Full access to the document AST (Abstract Syntax Tree)
Pre-computed collections (headings, links, images, footnotes)
Node rendering filters for recursive rendering
Format-specific escape filters (XML, HTML, LaTeX, YAML, Markdown)
AST traversal helper functions
Custom context injection
Configurable escaping strategies
Quick Start
1. Create a template file (e.g., outline.txt.jinja2):
# {{ metadata.title or "Document" }}
{% for h in headings -%}
{{ " " * (h.level - 1) }}{{ loop.index }}. {{ h.text }}
{% endfor %}
2. Use it from Python:
from all2md import convert
from all2md.options import JinjaRendererOptions
options = JinjaRendererOptions(
template_file="outline.txt.jinja2"
)
convert("document.pdf", "outline.txt",
target_format="jinja",
renderer_options=options)
3. Or from the CLI:
all2md document.pdf --output-format jinja \
--jinja-renderer-template-file outline.txt.jinja2 \
--out outline.txt
Template Context Reference
Every template receives a rich context with access to the document structure, metadata, and helper utilities.
Core Variables
documentThe root AST node (
Documentobject). Contains the full document tree with achildrenlist.astDictionary representation of the entire AST. Useful for debugging or simple property access.
metadataDocument metadata object with attributes:
title(str or None) - Document titleauthor(str or None) - Document authordate(str or None) - Document datedescription(str or None) - Document descriptionkeywords(list[str]) - Document keywordslanguage(str or None) - Document language
titleQuick access to
metadata.title(commonly used).
Pre-computed Collections
These collections are only available if enable_traversal_helpers=True (enabled by default).
headingsList of all headings. Each entry is a dict with:
level(int) - Heading level (1-6)text(str) - Plain text content (extracted automatically)node(Heading) - The underlying AST node (usenode.contentfor its inline nodes)
Example:
{% for h in headings %} {{ " " * (h.level - 1) }}{{ h.text }} {% endfor %}
linksList of all links. Each entry is a dict with:
url(str) - Link destinationtitle(str or None) - Optional link titletext(str) - Plain text of link contentnode(Link) - The underlying AST node
Example:
Links in this document: {% for link in links %} - {{ link.text }}: {{ link.url }} {% endfor %}
imagesList of all images. Each entry is a dict with:
url(str) - Image source URL or pathalt_text(str or None) - Alternative texttitle(str or None) - Optional image titlewidth/height(int or None) - Optional dimensionsnode(Image) - The underlying AST node
Example:
Images: {{ images|length }} {% for img in images %} - {{ img.alt_text or "Untitled" }} {% endfor %}
footnotesList of all footnote definition nodes. Each entry is a dict with:
identifier(str) - Footnote identifiernode(FootnoteDefinition) - The underlying AST node (usenode.contentfor its blocks)
Example:
Footnotes: {% for fn in footnotes %} [{{ fn.identifier }}]: {{ fn.node.content|map('render')|join('') }} {% endfor %}
Filter Reference
Node Rendering Filters
These filters are only available if enable_render_filter=True (enabled by default).
renderRecursively renders a node (or list of nodes) to the configured output format. The filter takes no arguments — the output format is fixed by the renderer options (
default_render_format, which defaults to"markdown", or theescape_strategywhen one is set).Example:
{# Render a node using the configured format #} {{ node|render }} {# Render a list of inline nodes (e.g. a heading's content) #} {{ heading.node.content|map('render')|join('') }}
render_inlineRenders inline content (list of inline nodes) to plain text.
Example:
{{ paragraph.content|render_inline }}
Escape Filters
These filters are only available if enable_escape_filters=True (enabled by default).
escape_xmlEscapes text for XML output (
&,<,>,",').<title>{{ metadata.title|escape_xml }}</title>
escape_htmlEscapes text for HTML output (
&,<,>,",').<h1>{{ heading.text|escape_html }}</h1>
escape_latexEscapes text for LaTeX output (
&,%,$,#,_,{,},~,^,\).\section{{{ heading.text|escape_latex }}}
escape_yamlEscapes text for YAML output (handles special characters and multiline strings).
title: {{ metadata.title|escape_yaml }}
escape_markdownEscapes text for Markdown output (
*,_,[,],(,), etc.).# {{ heading.text|escape_markdown }}
Function Reference
Helper Functions
These functions are only available if enable_traversal_helpers=True (enabled by default).
find_nodes(document, node_type)Finds all nodes of a specific type in the document.
documentis available in the template context.Parameters:
document- The document being rendered (thedocumentcontext variable)node_type(str) - Node type name (e.g., “Heading”, “Link”, “Image”)
Returns: List of matching nodes
Example:
{# Find all code blocks #} {% set code_blocks = find_nodes(document, "CodeBlock") %} Code blocks: {{ code_blocks|length }}
node_typeTest filter that returns the type name of a node.
Example:
{% if node|node_type == "Heading" %} # {{ node.content|map('render')|join('') }} {% endif %}
Template Creation Walkthrough
Let’s walk through creating a custom DocBook XML template.
Step 1: Plan the structure
DocBook uses specific XML tags like <article>, <section>, <para>, etc. We need to map AST nodes to these tags.
Step 2: Create the template file (docbook.xml.jinja2):
<?xml version="1.0" encoding="UTF-8"?>
<article xmlns="http://docbook.org/ns/docbook" version="5.0">
<info>
<title>{{ metadata.title|escape_xml or "Untitled Document" }}</title>
{% if metadata.author -%}
<author><personname>{{ metadata.author|escape_xml }}</personname></author>
{% endif -%}
{% if metadata.date -%}
<date>{{ metadata.date|escape_xml }}</date>
{% endif -%}
</info>
{% for node in document.children -%}
{%- if node|node_type == "Heading" -%}
<section>
<title>{{ node.content|map('render')|join('')|escape_xml }}</title>
</section>
{%- elif node|node_type == "Paragraph" -%}
<para>{{ node.content|map('render')|join('')|escape_xml }}</para>
{%- elif node|node_type == "CodeBlock" -%}
<programlisting{% if node.language %} language="{{ node.language }}"{% endif %}>{{ node.content|escape_xml }}</programlisting>
{%- endif -%}
{% endfor -%}
</article>
Step 3: Configure the renderer
from all2md import convert
from all2md.options import JinjaRendererOptions
options = JinjaRendererOptions(
template_file="docbook.xml.jinja2",
escape_strategy="xml", # Auto-escape for XML
enable_render_filter=True, # Enable node rendering
enable_escape_filters=True, # Enable escape filters
enable_traversal_helpers=True # Enable find functions
)
convert("document.pdf", "output.xml",
target_format="jinja",
renderer_options=options)
Step 4: Test and refine
Run the conversion and check the output. Adjust the template as needed for edge cases, proper indentation, and complete coverage of node types.
Example Templates Gallery
all2md includes several example templates in the examples/templates/jinja-templates/ directory:
DocBook XML
File: docbook.xml.jinja2
Produces valid DocBook 5.0 XML suitable for technical documentation systems.
Features:
Full XML escaping
Metadata mapping to DocBook
<info>elementSection hierarchy
Code blocks with language attributes
Use case: Technical documentation, book authoring, documentation toolchains
YAML Metadata
File: metadata.yaml.jinja2
Extracts document structure and metadata to YAML format.
Features:
Document statistics (heading count, link count, image count)
Table of contents with heading hierarchy
Link and image inventories
Use case: Document analysis, metadata extraction, content auditing
ANSI Terminal
File: ansi-terminal.txt.jinja2
Creates colorful, formatted terminal output with Unicode box drawing characters.
Features:
ANSI color codes for syntax highlighting
Unicode box characters for borders and structure
Styled headings (█ level 1, ▓ level 2, ▒ level 3)
Code blocks in bordered frames
Document statistics footer
Use case: Terminal documentation viewers, CLI help systems, README rendering
Custom Outline
File: custom-outline.txt.jinja2
Generates a human-readable document outline.
Features:
Hierarchical heading list with indentation
Document statistics summary
Plain text format
Use case: Document navigation, quick previews, outline generation
Best Practices
Performance Optimization
Disable unused features: Set
enable_render_filter=False,enable_escape_filters=False, andenable_traversal_helpers=Falseif you don’t need them.Use pre-computed collections: Access
headings,links,images, andfootnotesinstead of callingfind_nodes()repeatedly.Cache expensive operations: Use Jinja2’s
{% set %}to cache computed values.{% set total_links = links|length %} {% set total_images = images|length %}
Escaping and Security
Always escape user content: Use appropriate escape filters for your output format.
{# Good - escaped #} <title>{{ metadata.title|escape_xml }}</title> {# Bad - unescaped #} <title>{{ metadata.title }}</title>
Use escape_strategy: Set a default escape strategy in options to avoid forgetting.
JinjaRendererOptions( template_file="template.xml.jinja2", escape_strategy="xml" )
Mark safe content carefully: Only use
|safewhen you’re absolutely certain the content is safe.
Template Organization
Break complex templates into macros:
{% macro render_heading(heading) -%} <h{{ heading.level }}>{{ heading.content|map('render')|join('') }}</h{{ heading.level }}> {%- endmacro %} {% for node in document.children %} {%- if node|node_type == "Heading" -%} {{ render_heading(node) }} {%- endif -%} {% endfor %}
Use template inheritance: Create a base template with common structure.
{# base.xml.jinja2 #} <?xml version="1.0"?> <document> {% block content %}{% endblock %} </document> {# custom.xml.jinja2 #} {% extends "base.xml.jinja2" %} {% block content %} {{ document.children|map('render')|join('') }} {% endblock %}
Add comments: Document complex logic and template sections.
{# Table of Contents Section #} {% if headings and headings|length > 0 %} <nav> {# Render headings hierarchically #} {% for h in headings %} <a href="#{{ h.node_id }}">{{ h.text }}</a> {% endfor %} </nav> {% endif %}
Error Handling
Check for None values:
{% if metadata.title %} <title>{{ metadata.title }}</title> {% else %} <title>Untitled</title> {% endif %}
Use default filter:
<title>{{ metadata.title|default("Untitled") }}</title>
Relax strict_undefined for lenient rendering:
strict_undefineddefaults toTrue, so undefined variables raise an error (this is why the pre-computed collections above must use their real keys). Set it toFalseto render undefined variables as empty strings instead:JinjaRendererOptions( template_file="template.jinja2", strict_undefined=False # Render undefined variables as empty instead of erroring )
Advanced Techniques
Inline Template Strings
For simple templates, use template_string instead of template_file:
from all2md import convert
from all2md.options import JinjaRendererOptions
template = """
# {{ metadata.title or "Document" }}
{% for h in headings %}
{{ " " * (h.level - 1) }}- {{ h.text }}
{% endfor %}
"""
options = JinjaRendererOptions(
template_string=template,
enable_traversal_helpers=True
)
convert("doc.pdf", "outline.txt",
target_format="jinja",
renderer_options=options)
Custom Escape Functions
Provide your own escape function for specialized formats:
def escape_csv(text: str) -> str:
"""Escape text for CSV format."""
if '"' in text or ',' in text or '\n' in text:
return '"' + text.replace('"', '""') + '"'
return text
options = JinjaRendererOptions(
template_file="export.csv.jinja2",
escape_strategy="custom",
custom_escape_function=escape_csv
)
Extra Context Variables
Inject additional context variables into templates:
options = JinjaRendererOptions(
template_file="report.html.jinja2",
extra_context={
"company_name": "Acme Corp",
"report_date": "2025-01-24",
"logo_url": "https://example.com/logo.png"
}
)
Then use in template:
<header>
<img src="{{ logo_url }}" alt="{{ company_name }}">
<span>Report Date: {{ report_date }}</span>
</header>
CLI Integration
All template options are available from the command line:
# Basic usage
all2md doc.pdf --output-format jinja --jinja-renderer-template-file template.xml.jinja2
# With escape strategy
all2md doc.pdf --output-format jinja \
--jinja-renderer-template-file template.xml.jinja2 \
--jinja-renderer-escape-strategy xml
# The render filter, escape filters, and traversal helpers are enabled by
# default; pass the negative flags to turn any of them off
all2md doc.pdf --output-format jinja \
--jinja-renderer-template-file template.jinja2 \
--jinja-renderer-no-enable-render-filter \
--jinja-renderer-no-enable-escape-filters \
--jinja-renderer-no-enable-traversal-helpers
# With inline template string
all2md doc.pdf --output-format jinja \
--jinja-renderer-template-string "# {{ title }}"
See Also
Python API Workflows - Overview of bidirectional conversion including templates
Recipes and Cookbook - Step-by-step template creation tutorial
Working with the AST - Understanding the AST structure for template authoring
examples/templates/jinja-templates/- Gallery of example templatesexamples/templates/jinja_template_demo.py- Python API examples