Walker y utilidades

El paquete markast.ast expone primitivas de recorrido para que inspecciones o reescribas el árbol sin escribir recursión contra cada forma de contenedor.

walk(node) — generador

from markast import parse, walk

doc = parse("# Hola\n\nUna palabra **negrita**.")
for node in walk(doc.root):
    print(node["type"])
# document
# heading
# text
# paragraph
# text
# bold
# text

El walker visita los nodos en orden de documento (depth-first, pre-order). Sigue cada clave contenedora (children, slots, head, body, rows, cells), así que widgets y tablas no se saltan.

walk(node, include_root=False) salta el nodo inicial y solo emite descendientes.

find(node, type_) y find_all(node, type_)

from markast import find, find_all, parse

doc = parse("# A\n\n## B\n\n## C")
print(find(doc.root, "heading")["children"][0]["value"])      # A
print([h["children"][0]["value"] for h in find_all(doc.root, "heading")])
# ["A", "B", "C"]

Ambas aceptan un tipo string o una lista:

find_all(doc.root, ["link", "inline_image"])

Visitor — despacho por clase

from markast import Visitor, parse


class HeadingCollector(Visitor):
    def __init__(self):
        self.headings = []

    def visit_heading(self, node):
        self.headings.append(node)


v = HeadingCollector()
v.run(parse("# A\n\n## B"))
print(len(v.headings))  # 2

Sobrescribe visit_<node_type> para cualquier tipo de nodo al que quieras reaccionar. Los métodos pueden devolver un valor, que run() colecta en una lista.

replace(node, fn) — reescritura funcional

replace recorre el árbol y aplica fn a cada nodo, sustituyendo cada uno por lo que fn retorne. Devolver None elimina el nodo:

from markast import NodeType, parse, replace

doc = parse("# Título\n\n---\n\nCuerpo.")
trimmed = replace(doc.root, lambda n: None if n.get("type") == NodeType.DIVIDER else n)

Devolver un nodo nuevo está bien — el walker continúa por los hijos del reemplazo, no del original. Esto hace que reescrituras de una pasada sean triviales:

def lowercase_text(node):
    if node.get("type") == "text":
        return {"type": "text", "value": node["value"].lower()}
    return node


lowered = replace(doc.root, lowercase_text)

Pasa in_place=True para mutar el árbol existente si necesitas preservar identidad de objetos. Por defecto devuelve un árbol nuevo.

extract_text(node)

Atajo para "dame la proyección de texto plano de este subárbol":

from markast import extract_text, parse

doc = parse("# Hola **mundo**")
print(extract_text(doc.children[0]))
# "Hola mundo"

Recorre children, slots, y rows/cells de tablas, así que widgets y tablas también aportan texto.

count_nodes(node)

from markast import count_nodes, parse

doc = parse("# A\n\n## B\n\n- x\n- y")
print(count_nodes(doc.root))
# {'document': 1, 'heading': 2, 'text': 4, 'list': 1, 'list_item': 2, ...}

Cheat-sheet

ObjetivoUsa
Iterar cada nodo, hacer algowalk
Encontrar el primero / todos de un tipofind / find_all
Extraer datos en una listaVisitor
Mutar una copia del árbolreplace
Mutar en sitioreplace(..., in_place=True)
Obtener texto planoextract_text
Contar tipos de nodoscount_nodes

Modelo mental: el AST es solo dicts. El walker es la única parte no trivial; todo lo demás es manipulación de diccionarios.