Integración con clientes
El patrón estándar para enviar la salida de markast a un cliente — sin importar la tecnología. Las ideas son las mismas si renderizas con React, Vue, Svelte, widgets nativos móviles, UIs de terminal, o cualquier cosa que pueda hacer switch sobre un string.
El pipeline
┌──────────────┐ parse() ┌──────────┐ to_json() ┌───────────┐ HTTP ┌──────────────┐
│ Fuente │ ─────────────► │ AST │ ────────────► │ JSON │ ─────────► │ Renderer │
│ Markdown │ │ (dict) │ │ (string) │ │ cliente │
└──────────────┘ └──────────┘ └───────────┘ └──────────────┘
Cada paso ocurre server-side. El cliente recibe un árbol JSON tipado y lo recorre. El cliente nunca parsea Markdown.
Un handler en FastAPI
from fastapi import FastAPI
from markast import Parser
app = FastAPI()
parser = Parser(transforms=["normalize", "slugify", "toc"])
@app.get("/content/{slug}")
def get_content(slug: str):
markdown = load_from_db(slug)
doc = parser.parse(markdown)
return {
"ast": doc.to_dict(),
"warnings": doc.warnings,
"toc": doc.meta.get("toc", []),
}
Una instancia de Parser reutilizada entre requests. Parser.parse() es thread-safe para operaciones de lectura.
Cliente: switch sobre type
Pseudocódigo que se traduce limpiamente a cualquier front-end:
function render(node):
switch node.type:
case "document": render_each(node.children)
case "heading": return text_with_size(node.level, render_inline(node.children))
case "paragraph": return text(render_inline(node.children))
case "list": return list_view(node.ordered, render_each(node.children))
case "list_item": return list_item(render_each(node.children), checked=node.checked)
case "image": return image(node.src, node.alt)
case "code_block": return code(node.language, node.value, filename=node.filename)
case "table": return table(render_table(node.head, node.body))
case "blockquote": return quote(render_each(node.children))
case "divider": return divider()
case "widget": return render_widget(node)
...
function render_inline(nodes):
parts = []
for n in nodes:
switch n.type:
case "text": parts.add(plain(n.value))
case "bold": parts.add(bold(render_inline(n.children)))
case "italic": parts.add(italic(render_inline(n.children)))
case "code_inline": parts.add(code_span(n.value))
case "link": parts.add(link(n.href, render_inline(n.children)))
case "strikethrough": parts.add(strike(render_inline(n.children)))
case "softbreak": parts.add(line_break())
case "hardbreak": parts.add(line_break())
...
return parts
El código concreto en tu framework se verá casi idéntico.
Widgets en el cliente
Un nodo widget deja que tu contenido lleve componentes propios. Bifurca por nombre de widget:
function render_widget(node):
switch node.widget:
case "tip": return Callout(level="tip", title=node.props.title, body=render_each(node.slots.default))
case "warning": return Callout(level="warn", title=node.props.title, body=render_each(node.slots.default))
case "video": return Video(src=node.props.src, poster=node.props.poster, controls=node.props.controls)
case "card": return Card(title=node.props.title, header=render_each(node.slots.header or []), body=render_each(node.slots.default), footer=render_each(node.slots.footer or []))
case "code-group": return Tabs(node.slots.default.map(b => (b.filename, render(b))))
default:
// Widget desconocido — falla suave: renderiza el slot default si lo hay, o ignóralo.
return render_each(node.slots.default or [])
Generar schemas para clientes
Si tu lenguaje cliente tiene un sistema de tipos fuerte, genera definiciones desde json_schema():
import json
from markast import json_schema
with open("ast.schema.json", "w") as f:
json.dump(json_schema(), f, indent=2)
Pasa ese schema a:
quicktype— Dart, TypeScript, Go, Swift, C#, Kotlin, Rust, …datamodel-code-generator— Pydantic / TypedDictjson-schema-to-typescript— interfaces TS
Warnings: dónde mostrarlos
doc.warnings es el canal de avisos entre autor y revisión de contenido. Recomendado:
- En respuestas de producción, descártalos — los clientes los ignoran.
- En staging / dev, expónlos para que los autores vean qué falla.
- En una UI de autoría/CMS, renderízalos junto al fragmento ofensor.
warnings = doc.warnings if request.app.debug else []
return {"ast": doc.to_dict(), "warnings": warnings}
Lo que el AST garantiza
| Invariante | Por qué importa |
|---|---|
Cada nodo tiene un type string | El cliente puede hacer switch |
heading.children nunca contiene imágenes ni nodos block | Los headings se renderizan solo inline |
table_cell.children nunca contiene nodos block | Las celdas se renderizan solo inline |
widget.props es un dict; widget.slots siempre tiene la clave "default" | Siempre seguro de destructurar |
document.warnings es siempre un array (puede estar vacío) | Nunca null |
document.version es un string | Útil para chequeos de compatibilidad futura |
Tip de caché
Si tu contenido cambia poco, parsear en cada request es desperdicio. Cachea el JSON usando como clave un hash de la fuente:
import hashlib
from functools import lru_cache
@lru_cache(maxsize=1024)
def parse_cached(text_hash, text):
return parser.parse(text).to_dict()
@app.get("/content/{slug}")
def get_content(slug):
md = load_from_db(slug)
h = hashlib.sha256(md.encode("utf-8")).hexdigest()
return parse_cached(h, md)