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 / TypedDict
  • json-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

InvariantePor qué importa
Cada nodo tiene un type stringEl cliente puede hacer switch
heading.children nunca contiene imágenes ni nodos blockLos headings se renderizan solo inline
table_cell.children nunca contiene nodos blockLas 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)