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.