markdocpy.transform.transformer

  1from __future__ import annotations
  2
  3from typing import Any, Dict, List
  4
  5from ..ast.node import Node
  6from ..ast.tag import Tag
  7from ..schema.nodes import nodes as default_nodes
  8from ..schema.functions import functions as default_functions
  9from ..schema.tags import tags as default_tags
 10from ..schema_types import ClassType, IdType
 11
 12
 13global_attributes = {
 14    "class": {"type": ClassType, "render": True},
 15    "id": {"type": IdType, "render": True},
 16}
 17
 18
 19def merge_config(config: Dict[str, Any] | None = None) -> Dict[str, Any]:
 20    """Merge user config with default nodes/tags."""
 21    config = config or {}
 22    return {
 23        **config,
 24        "nodes": {**default_nodes, **config.get("nodes", {})},
 25        "tags": {**default_tags, **config.get("tags", {})},
 26        "functions": {**default_functions, **config.get("functions", {})},
 27        "global_attributes": {**global_attributes, **config.get("global_attributes", {})},
 28    }
 29
 30
 31def transform(node: Node | List[Node], config: Dict[str, Any] | None = None):
 32    """Transform AST nodes into a renderable tree."""
 33    cfg = merge_config(config)
 34    if isinstance(node, list):
 35        return [transform(child, cfg) for child in node]
 36    if node.type == "document":
 37        schema = _find_schema(node, cfg)
 38        if schema and schema.get("render"):
 39            return _make_tag(
 40                schema.get("render"),
 41                _render_attributes(node, schema, cfg),
 42                _transform_children(node, cfg),
 43                schema,
 44            )
 45        return [transform(child, cfg) for child in node.children]
 46    if node.type == "text":
 47        return node.content or ""
 48    if node.type == "code_inline":
 49        return Tag("code", {}, [node.content or ""])
 50    if node.type in ("variable", "function"):
 51        return node.attributes.get("value") if node.attributes else None
 52    if node.type == "softbreak":
 53        return " "
 54    if node.type == "code":
 55        return _render_code(node, None, fenced=False)
 56    if node.type == "fence":
 57        return _render_code(node, node.attributes.get("language"), fenced=True)
 58
 59    schema = _find_schema(node, cfg)
 60    if schema and callable(schema.get("transform")):
 61        return schema["transform"](node, cfg)
 62
 63    if node.type == "list":
 64        name = "ol" if node.attributes.get("ordered") else "ul"
 65        return _make_tag(name, {}, _transform_children(node, cfg), schema)
 66    if node.type == "item":
 67        children = node.children
 68        if children and isinstance(children[0], Node) and children[0].type == "paragraph":
 69            if len(children) == 1:
 70                return _make_tag(
 71                    "li",
 72                    _render_attributes(node, schema, cfg),
 73                    _transform_children(children[0], cfg),
 74                    schema,
 75                )
 76            flattened = [
 77                *(_transform_children(children[0], cfg)),
 78                *[transform(child, cfg) for child in children[1:]],
 79            ]
 80            return _make_tag("li", _render_attributes(node, schema, cfg), flattened, schema)
 81        return _make_tag(
 82            "li", _render_attributes(node, schema, cfg), _transform_children(node, cfg), schema
 83        )
 84
 85    if schema is None:
 86        if node.type == "tag":
 87            return _transform_children(node, cfg)
 88        return ""
 89
 90    render = schema.get("render")
 91    if render is False:
 92        return _transform_children(node, cfg)
 93    if render is None:
 94        return _transform_children(node, cfg)
 95
 96    if isinstance(render, str):
 97        name = render.format(**node.attributes) if "{" in render else render
 98        return _make_tag(name, _render_attributes(node, schema, cfg), _transform_children(node, cfg), schema)
 99
100    return ""
101
102
103def _transform_children(node: Node, config: Dict[str, Any]) -> List[Any]:
104    return [transform(child, config) for child in node.children]
105
106
107def _find_schema(node: Node, config: Dict[str, Any]) -> Dict[str, Any] | None:
108    if node.type == "tag":
109        return config.get("tags", {}).get(node.tag)
110    return config.get("nodes", {}).get(node.type)
111
112
113def _render_attributes(
114    node: Node, schema: Dict[str, Any] | None, config: Dict[str, Any]
115) -> Dict[str, Any]:
116    if not schema:
117        return dict(node.attributes)
118    rendered: Dict[str, Any] = {}
119    schema_attrs = schema.get("attributes", {}) if schema else {}
120    attrs = {**global_attributes, **schema_attrs} if isinstance(schema_attrs, dict) else dict(global_attributes)
121
122    for key, attr in attrs.items():
123        if isinstance(attr, dict) and attr.get("render") is False:
124            continue
125        render_as = attr.get("render", True) if isinstance(attr, dict) else True
126        name = render_as if isinstance(render_as, str) else key
127        value = node.attributes.get(key)
128        if value is None and isinstance(attr, dict) and "default" in attr:
129            value = attr.get("default")
130        if value is None:
131            continue
132        type_cls = attr.get("type") if isinstance(attr, dict) else None
133        if isinstance(type_cls, type):
134            instance = type_cls()
135            if hasattr(instance, "transform"):
136                value = instance.transform(value)
137        rendered[name] = value
138
139    if schema.get("slots") and node.slots:
140        for key, slot in schema["slots"].items():
141            if isinstance(slot, dict) and slot.get("render") is False:
142                continue
143            name = slot.get("render") if isinstance(slot, dict) else key
144            if isinstance(name, str) and key in node.slots:
145                rendered[name] = transform(node.slots[key], config)
146
147    return rendered
148
149
150def _render_code(node: Node, language: str | None, *, fenced: bool):
151    """Render fenced or indented code blocks."""
152    if fenced:
153        attrs: Dict[str, Any] = {}
154        if language:
155            attrs["data-language"] = language
156        return Tag("pre", attrs, [node.content or ""])
157    return Tag("pre", {}, [node.content or ""])
158
159
160def _make_tag(
161    name: str | None, attributes: Dict[str, Any], children: List[Any], schema: Dict[str, Any] | None
162) -> Tag:
163    self_closing = bool(schema.get("self_closing")) if schema else False
164    return Tag(name, attributes, children, self_closing=self_closing)
global_attributes = {'class': {'type': <class 'markdocpy.schema_types.class_type.ClassType'>, 'render': True}, 'id': {'type': <class 'markdocpy.schema_types.id_type.IdType'>, 'render': True}}
def merge_config(config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
20def merge_config(config: Dict[str, Any] | None = None) -> Dict[str, Any]:
21    """Merge user config with default nodes/tags."""
22    config = config or {}
23    return {
24        **config,
25        "nodes": {**default_nodes, **config.get("nodes", {})},
26        "tags": {**default_tags, **config.get("tags", {})},
27        "functions": {**default_functions, **config.get("functions", {})},
28        "global_attributes": {**global_attributes, **config.get("global_attributes", {})},
29    }

Merge user config with default nodes/tags.

def transform( node: Union[markdocpy.Node, List[markdocpy.Node]], config: Optional[Dict[str, Any]] = None):
 32def transform(node: Node | List[Node], config: Dict[str, Any] | None = None):
 33    """Transform AST nodes into a renderable tree."""
 34    cfg = merge_config(config)
 35    if isinstance(node, list):
 36        return [transform(child, cfg) for child in node]
 37    if node.type == "document":
 38        schema = _find_schema(node, cfg)
 39        if schema and schema.get("render"):
 40            return _make_tag(
 41                schema.get("render"),
 42                _render_attributes(node, schema, cfg),
 43                _transform_children(node, cfg),
 44                schema,
 45            )
 46        return [transform(child, cfg) for child in node.children]
 47    if node.type == "text":
 48        return node.content or ""
 49    if node.type == "code_inline":
 50        return Tag("code", {}, [node.content or ""])
 51    if node.type in ("variable", "function"):
 52        return node.attributes.get("value") if node.attributes else None
 53    if node.type == "softbreak":
 54        return " "
 55    if node.type == "code":
 56        return _render_code(node, None, fenced=False)
 57    if node.type == "fence":
 58        return _render_code(node, node.attributes.get("language"), fenced=True)
 59
 60    schema = _find_schema(node, cfg)
 61    if schema and callable(schema.get("transform")):
 62        return schema["transform"](node, cfg)
 63
 64    if node.type == "list":
 65        name = "ol" if node.attributes.get("ordered") else "ul"
 66        return _make_tag(name, {}, _transform_children(node, cfg), schema)
 67    if node.type == "item":
 68        children = node.children
 69        if children and isinstance(children[0], Node) and children[0].type == "paragraph":
 70            if len(children) == 1:
 71                return _make_tag(
 72                    "li",
 73                    _render_attributes(node, schema, cfg),
 74                    _transform_children(children[0], cfg),
 75                    schema,
 76                )
 77            flattened = [
 78                *(_transform_children(children[0], cfg)),
 79                *[transform(child, cfg) for child in children[1:]],
 80            ]
 81            return _make_tag("li", _render_attributes(node, schema, cfg), flattened, schema)
 82        return _make_tag(
 83            "li", _render_attributes(node, schema, cfg), _transform_children(node, cfg), schema
 84        )
 85
 86    if schema is None:
 87        if node.type == "tag":
 88            return _transform_children(node, cfg)
 89        return ""
 90
 91    render = schema.get("render")
 92    if render is False:
 93        return _transform_children(node, cfg)
 94    if render is None:
 95        return _transform_children(node, cfg)
 96
 97    if isinstance(render, str):
 98        name = render.format(**node.attributes) if "{" in render else render
 99        return _make_tag(name, _render_attributes(node, schema, cfg), _transform_children(node, cfg), schema)
100
101    return ""

Transform AST nodes into a renderable tree.