Renderers
Dos renderers vienen en la caja: MarkdownRenderer y HTMLRenderer. Ambas son clases Python subclasificables — sobrescribe handlers individuales para personalizar la salida.
MarkdownRenderer
Hace roundtrip de un AST de regreso a Markdown. Útil para:
- Renderizar AST guardado en BD de regreso a un textarea / editor.
- Re-emitir Markdown normalizado después de correr transformaciones.
- Debugging —
print(doc.to_markdown())es más legible que el JSON.
from markast import MarkdownRenderer, parse
doc = parse("# Hola\n\nUn párrafo.")
print(MarkdownRenderer().render(doc.root))
Por conveniencia, Document.to_markdown() hace lo mismo.
Estabilidad del roundtrip
parse(text).to_markdown() reproduce la fuente para cada construcción Markdown que markast soporta, con estas normalizaciones:
- Líneas en blanco en exceso colapsan a una sola.
- Headings setext (subrayado
====) se vuelven ATX (# título). - Marcadores de listas sueltas se normalizan a la izquierda.
- Indentación de listas se normaliza a 2 espacios por nivel.
El roundtrip es estable: parse(parse(text).to_markdown()).to_markdown() equivale a parse(text).to_markdown() para cada construcción soportada.
HTMLRenderer
HTML conservador, sin opiniones. No emite clases CSS por defecto más allá de las estructurales que los widgets añaden explícitamente.
from markast import HTMLRenderer, parse
doc = parse("# Hola\n\n:::tip\nUn tip\n:::")
print(HTMLRenderer().render(doc.root))
Pasa wrap_root=True para envolver la salida en <article class="markast">:
HTMLRenderer(wrap_root=True).render(doc.root)
# <article class="markast">...</article>
Caracteres especiales
Los nodos text se escapan: <, >, & se vuelven <, >, &. Los bloques de código se escapan igual. Los atributos HTML se escapan incluyendo ".
Los nodos html_block (HTML crudo en la fuente Markdown) se emiten verbatim — el parser ya no validó nada de su contenido. Trátalo como opt-in: si tu fuente permite HTML crudo, confías en los autores.
Subclasificar
Ambos renderers despachan por nombre de método: _block_<type> para nodos block, _inline_<type> para inline. Sobrescribe solo lo que te importa:
from markast import MarkdownRenderer
class FlushDividers(MarkdownRenderer):
"""Usa una sintaxis de divider más fuerte."""
def _block_divider(self, node):
return "* * *"
print(FlushDividers().render(doc.root))
HTMLRenderer sigue la misma convención. Sobrescribe por ejemplo _block_heading para emitir headings con ancla:
class AnchoredHTMLRenderer(HTMLRenderer):
def _block_heading(self, node):
lvl = max(1, min(6, node.get("level", 1)))
slug = node.get("id", "")
anchor = f'<a href="#{slug}">#</a>' if slug else ""
body = self._inline(node.get("children", []))
return f'<h{lvl} id="{slug}">{body}{anchor}</h{lvl}>'
Combínalo con la transformación slugify para que cada heading lleve un id.
Renderers personalizados
Si quieres un tercer formato (terminal? otro modelo de documento?), la ruta más fácil es extender el mismo patrón de despacho:
class AnsiRenderer:
def render(self, doc):
return "".join(self._block(c) for c in doc.get("children", []))
def _block(self, node):
method = getattr(self, f"_block_{node.get('type')}", None)
return method(node) if method else ""
def _block_heading(self, node):
text = "".join(c.get("value", "") for c in node["children"])
return f"\033[1m{'#' * node['level']} {text}\033[0m\n\n"
def _block_paragraph(self, node):
text = "".join(c.get("value", "") for c in node["children"])
return text + "\n\n"
Mantén la convención _block_<type> / _inline_<type> para que tus usuarios puedan extender tu renderer igual que extienden los builtin.