Renderers¶
A renderer turns a Document into a string. Two renderers ship with the package and register themselves when their module is imported:
| Registry name | Class | Module | Output |
|---|---|---|---|
markdown |
MarkdownRenderer |
marktripy.renderers.markdown |
Markdown |
html |
HTMLRenderer |
marktripy.renderers.html |
HTML fragment (no <html> or <body>) |
from marktripy.renderers.base import RendererRegistry
from marktripy.renderers import html, markdown # registers "html" and "markdown"
renderer = RendererRegistry.create("markdown", {"bullet_char": "*"})
Constructing the class directly is equivalent: MarkdownRenderer({"bullet_char": "*"}).
How rendering works¶
render(ast) resets the render context and calls render_node(ast). render_node looks up a method named render_<node.type> on the renderer and calls it. Each method renders its children by calling render_node again, so the whole tree is visited depth first.
If no method matches, render_unknown runs:
MarkdownRendererlogs a warning and outputs nothing for that node.HTMLRendererlogs a warning and outputs<!-- Unknown node type: <type> -->.
This lookup is also how extensions add output for new node types: they attach a render_<type> function to the renderer instance. You can do the same:
from marktripy.core.ast import ASTNode, Document
from marktripy.renderers.html import HTMLRenderer
html = HTMLRenderer()
html.render_callout = lambda node: f'<aside>{html.render_children(node)}</aside>\n'
doc = Document()
doc.add_child(ASTNode(type="callout", children=[]))
print(html.render(doc)) # <aside></aside>
MarkdownRenderer¶
Options¶
| Key | Default | Effect |
|---|---|---|
heading_style |
"atx" |
"setext" writes level 1 and 2 headings with === / --- underlines. Levels 3–6 always use #. |
emphasis_char |
"*" |
Delimiter for emphasis nodes, for example "_". |
strong_char |
"**" |
Delimiter for strong nodes, for example "__". |
bullet_char |
"-" |
Marker for unordered list items. |
code_fence |
"`" |
Character repeated three times to fence code blocks. "~" gives ~~~. |
line_width |
0 |
If greater than 0, paragraph text is re-wrapped at this width. 0 means no wrapping. |
preserve_format |
False |
Stored on the renderer but not used by any method. |
indent |
" " |
Used by get_indent(). List rendering uses a fixed three-space continuation indent instead. |
Example with non-default options:
from marktripy.core.parser import ParserRegistry
from marktripy.parsers import markdown_it
from marktripy.renderers.markdown import MarkdownRenderer
doc = ParserRegistry.create("markdown-it").parse("# Title\n\n*it* and\n\n- a\n")
r = MarkdownRenderer({"heading_style": "setext", "emphasis_char": "_", "bullet_char": "*"})
print(r.render(doc))
Output:
Output rules¶
- Blocks in the document are separated by one blank line. The output always ends with exactly one newline.
- Ordered lists are numbered from
List.start(or 1) and counted up, so3. x/4. yround-trips. - Code blocks use a fence of three
code_fencecharacters, or four if the content already contains three. The language follows the opening fence. - Inline code uses enough backticks to enclose any backticks in the content.
- A
Linkwith areference_idattribute is written as[text][id]. Otherwise it is written inline, with the title in quotes if set. - Tables are written with padded columns and a separator row that shows alignment (
:--,--:,:-:). The first row is treated as the header if all its cells haveheader=True. HorizontalRuleis always written as---.
Escaping¶
render_text escapes these characters with a backslash in every text node: \ * _ ` [ ] ( ) # + - ! | { }. It does this without looking at context, so plain prose picks up escapes that a person would not write:
doc = ParserRegistry.create("markdown-it").parse("Use the well-known foo_bar (see #1).\n")
print(MarkdownRenderer().render(doc))
# Use the well\-known foo\_bar \(see \#1\).
The escaped Markdown renders to the same text, but it is not byte-for-byte what you parsed. Text inside inline_code and code_block nodes is not escaped.
HTMLRenderer¶
Options¶
The option names follow markdown-it conventions.
| Key | Default | Effect |
|---|---|---|
xhtml |
False |
Writes <hr /> and <img … /> instead of <hr> and <img …>. |
breaks |
False |
Replaces each newline inside a text node with <br> plus a newline. |
langPrefix |
"language-" |
Prefix for the class added to code blocks with a language. |
typographer |
False |
Replaces -- with an em dash (—) and ... with an ellipsis (…) in text nodes. Quote replacement is not implemented. |
quotes |
'""' + "''" |
Stored but unused, because quote replacement is not implemented. |
linkify |
False |
Stored but unused. |
Example:
from marktripy.renderers.html import HTMLRenderer
doc = ParserRegistry.create("markdown-it").parse("a -- b...\nline2\n\n---\n")
print(HTMLRenderer({"xhtml": True, "typographer": True, "breaks": True}).render(doc))
Output:
Output rules¶
- Text, code, link
href/titleand imagesrc/alt/titleare escaped withhtml.escape(…, quote=True). - Every entry in a node's
attrsbecomes an HTML attribute.Truewrites the bare attribute name;FalseandNoneare skipped; other values are converted to strings and escaped. Heading IDs therefore appear as<h2 id="setup">. - Because
CodeBlockstores its language inattrstoo, a fenced block produces both alanguageattribute and a class:<pre><code language="python" class="language-python">. - Ordered lists with a start other than 1 get a
startattribute. - In tight lists, a list item that renders as a single
<p>…</p>has the paragraph tags removed. - Table cells with an alignment get both
align="…"(fromattrs) andstyle="text-align: …;". - Empty paragraphs produce no output.
Write your own renderer¶
Subclass Renderer and implement render() and render_node(). The base class provides render_children(), escape() (returns the text unchanged) and get_indent().
from marktripy.core.ast import ASTNode, Document
from marktripy.renderers.base import Renderer, RendererRegistry
class PlainTextRenderer(Renderer):
"""Output only the text content, one block per line."""
def render(self, ast: Document) -> str:
return "\n".join(self.render_node(child) for child in ast.children) + "\n"
def render_node(self, node: ASTNode) -> str:
if node.content is not None:
return node.content
return self.render_children(node)
RendererRegistry.register("plain", PlainTextRenderer)
RenderContext (in marktripy.renderers.base) holds per-render state that the shipped renderers use: list_depth, tight_list, in_table, in_code_block, plus set_data() / get_data() for your own values. Both shipped renderers create a fresh context at the start of every render() call and store it in self.context.