AI-generated Markdown#
Parse, filter, and stream Markdown from LLMs before rendering it into your application.
LLMs often return Markdown because it is compact, readable, and easy to display. Do not render generated Markdown directly in a production application. You may need to:
restrict the allowed Markdown syntax;
remove images, raw HTML, or unknown extension nodes;
validate link and image URLs;
stream a preview before the complete answer is available;
store AST data for search, citations, or analytics.
Wenmode supports this workflow because parsing, AST inspection, filtering, and rendering are separate steps.
Choose a rule set#
Use the streaming preset to render the response as tokens arrive. It supports
common block and inline syntax, tables,
strikethrough, direct links, and direct images, but disables features that need
the complete document, such as reference-style links and footnotes.
from wenmode import Wenmode
from wenmode.presets import streaming
wen = Wenmode(streaming)
If the answer uses reference-style links, footnotes, or document-wide
transforms, parse the complete answer with commonmark, github, or a custom
rule list before you render it.
Filter nodes before rendering#
Use Parser.parse_iter() with a streaming-compatible preset when each completed
top-level block can be filtered and rendered independently.
from collections.abc import Iterable, Iterator
from wenmode import HTMLRenderer, Wenmode
from wenmode.nodes import Node
from wenmode.presets import streaming
ALLOWED_NODE_TYPES = {
'blockquote',
'break',
'code',
'delete',
'emphasis',
'heading',
'inlineCode',
'link',
'list',
'listItem',
'paragraph',
'root',
'strong',
'table',
'tableCell',
'tableRow',
'text',
'thematicBreak',
}
wen = Wenmode(streaming, renderer=HTMLRenderer())
def filter_node(node: Node) -> Node | None:
if node.type not in ALLOWED_NODE_TYPES:
return None
children = getattr(node, 'children', None)
if isinstance(children, list):
children[:] = [
child
for child in (filter_node(child) for child in children)
if child is not None
]
return node
def iter_filtered_nodes(source: str | Iterable[str]) -> Iterator[Node]:
for node in wen.parser.parse_iter(source):
filtered = filter_node(node)
if filtered is not None:
yield filtered
def render_ai_markdown(source: str | Iterable[str]) -> Iterator[str]:
yield from wen.renderer.render_iter(iter_filtered_nodes(source))
The allowlist above removes images and raw HTML because image and html are
not included in ALLOWED_NODE_TYPES.
Render streamed output#
The function accepts a string, a line iterator, or another iterable that yields Markdown chunks.
markdown = '''
# Answer
Use **Markdown** safely.

<script>alert(1)</script>
'''
html = ''.join(render_ai_markdown(markdown))
assert '<h1>Answer</h1>' in html
assert '<strong>Markdown</strong>' in html
assert '<img' not in html
assert '<script' not in html
Keep the default HTMLRenderer() safety settings. The renderer removes unsafe
URLs from links that pass the node filter. Node filtering and renderer safety
have different roles: filtering selects the allowed Markdown constructs, while
the renderer converts the remaining nodes to HTML.
Parse the full answer when needed#
Some Markdown features require the complete document. For those cases, parse the whole generated answer, filter or transform the root node, then render it.
from wenmode import HTMLRenderer, Wenmode
from wenmode.presets import github
wen = Wenmode(github, renderer=HTMLRenderer())
markdown = '''
# Answer
A [reference][id].
[id]: https://example.com
'''
root = wen.parse(markdown)
filtered = filter_node(root)
html = wen.render_node(filtered) if filtered is not None else ''
Use the full-document path when you need footnotes, reference-style links, heading ID transforms, table-of-contents generation, or AST JSON for storage and indexing.
Store structured output#
If another service needs to inspect the generated answer, serialize the AST instead of scraping rendered HTML.
import json
from wenmode import Wenmode
root = Wenmode().parse('A [link](https://example.com).')
payload = json.dumps(root.to_ast(), ensure_ascii=False)
assert '"type": "root"' in payload
assert '"url": "https://example.com"' in payload
For production renderer policy, read Security.