Extender la librería
Más allá de widgets, transformaciones y subclases de renderer, el parser expone hooks más profundos para casos donde esos no alcanzan.
Reglas propias
Una regla observa el árbol que se está construyendo y reporta diagnósticos. Subclasifica markast.rules.Rule y sobrescribe los métodos que te interesen:
from markast import Parser
from markast.rules import Diagnostic, Rule, Severity
class HeadingMustBeShort(Rule):
"""Marca headings cuyo texto plano supera los 60 caracteres."""
name = "short-heading"
def check_heading_children(self, children, level):
text = "".join(c.get("value", "") for c in children
if c.get("type") == "text")
if len(text) > 60:
return [Diagnostic(
code="X100",
message=f"Heading demasiado largo ({len(text)} chars).",
context=text[:40] + "…",
severity=Severity.WARNING,
)]
return None
parser = Parser(rules=[HeadingMustBeShort])
Códigos de diagnóstico
| Código | Disparador |
|---|---|
| W001 | Imagen dentro de un heading |
| W002 | Elemento block donde se requiere inline |
| W003 | Nombre de widget desconocido |
| W004 | Valor de prop inválido (tipo incorrecto / no está en choices) |
| W005 | Prop requerido faltante |
| W006 | Imagen dentro de una celda de tabla |
| W007 | HTML crudo encontrado (informativo) |
| W008 | Referencia a footnote sin definición |
| W009 | Anidación de widgets más profunda que el límite configurado |
Para reemplazar las reglas builtin (por ejemplo en modo estricto), pasa solo las tuyas. Para extender, incluye BuiltinRules:
from markast.rules.builtin import BuiltinRules
parser = Parser(rules=[BuiltinRules(), HeadingMustBeShort()])
Ajustar la configuración del parser
from markast import Parser, ParserConfig
cfg = ParserConfig(
features=("tables", "strikethrough", "footnotes"), # sin autolinks/tasklists
diagnose_html_blocks=False,
max_widget_depth=8,
)
parser = Parser(cfg)
Usa cfg.evolve(...) para derivar nuevas configuraciones:
strict = cfg.evolve(max_widget_depth=4)
Reemplazar el tokenizer
Raro, pero posible. Parser construye un Tokenizer de forma perezosa; si necesitas otro (por ejemplo para inyectar plugins extra de markdown-it-py), asígnalo antes del primer parse:
from markast import Parser
from markast.parser.tokenizer import Tokenizer
from mdit_py_plugins.deflist import deflist_plugin
class MyTokenizer(Tokenizer):
def _build_markdown_it(self):
md = super()._build_markdown_it()
md.use(deflist_plugin)
return md
parser = Parser()
parser._tokenizer = MyTokenizer(parser.config, parser.registry)
Patrón: parser por tenant
Un servicio multi-tenant puede necesitar conjuntos distintos de widgets por tenant. Cachea parsers por id:
from functools import lru_cache
from markast import Parser
from markast.widgets import default_registry, WidgetRegistry
@lru_cache(maxsize=64)
def parser_for(tenant_id: str) -> Parser:
registry = default_registry.clone()
for cls in load_tenant_widgets(tenant_id):
registry.register(cls)
return Parser(registry=registry, transforms=["normalize", "slugify"])
def render(tenant_id, markdown):
return parser_for(tenant_id).parse(markdown)
Cada parser es independiente — mutar el registry de uno no afecta a los demás.
Patrón: CI estricto de autoría
Combina reglas propias con doc.has_errors para fallar una build de CI por contenido inválido:
from markast import Parser
from markast.rules import Diagnostic, Rule, Severity
class NoRawHtml(Rule):
name = "no-raw-html"
def check_html_block(self, value):
return [Diagnostic(
code="C001",
message="HTML crudo no permitido en este corpus.",
severity=Severity.ERROR,
)]
parser = Parser(rules=[NoRawHtml()])
doc = parser.parse(open("article.md").read())
if doc.has_errors:
for w in doc.warnings:
print(f"::error::[{w['code']}] {w['message']}")
raise SystemExit(1)
Dónde está el código fuente
markast/
├── ast/ tipos, factories, walker, exportar schema
├── parser/ tokenizer + builder (block / inline / widget / props)
├── render/ markdown + html
├── widgets/ base / registry / builtins
├── rules/ sistema de diagnósticos + reglas builtin
├── transforms/ normalize / slugify / toc / linkify / typography
├── config.py
├── document.py
├── parser_api.py
└── cli.py
Cada archivo empieza con un docstring de módulo. Si te atascas, esa es la entrada más rápida.