markdocpy
markdocpy public API.
1"""markdocpy public API.""" 2 3from __future__ import annotations 4 5from dataclasses import dataclass 6from typing import Any, Dict, List 7 8from .ast.function import Function 9from .ast.node import Node 10from .ast.tag import Tag 11from .ast.variable import Variable 12from .version import __version__ 13from .parser.parser import parse as _parse_tokens 14from .parser.tokenizer import Tokenizer 15from .renderer.html import render as _render_html 16from .schema.nodes import nodes 17from .schema.tags import tags, truthy 18from .transform.transformer import global_attributes, merge_config, transform as _transform 19from .validator.validator import validate_tree 20 21 22@dataclass 23class _Renderers: 24 html = staticmethod(_render_html) 25 26 27renderers = _Renderers() 28 29 30def parse( 31 content: str, *, file: str | None = None, slots: bool = False, location: bool = False 32) -> Node: 33 _ = file, slots, location 34 tokenizer = Tokenizer() 35 tokens = tokenizer.tokenize(content) 36 return _parse_tokens(tokens, slots=slots) 37 38 39def resolve(content: Node | List[Node], config: Dict[str, Any]): 40 if isinstance(content, list): 41 return [child.resolve(config) for child in content] 42 return content.resolve(config) 43 44 45def transform(content: Node | List[Node], config: Dict[str, Any] | None = None): 46 merged = merge_config(config) 47 resolved = resolve(content, merged) 48 return _transform(resolved, merged) 49 50 51def validate(content: Node | List[Node], config: Dict[str, Any] | None = None): 52 return validate_tree(content, config) 53 54 55def create_element( 56 name: str | Dict[str, Any], attributes: Dict[str, Any] | None = None, *children: Any 57) -> Tag: 58 if isinstance(name, dict): 59 attributes = name 60 name = attributes.get("name") 61 return Tag(name, attributes or {}, list(children)) 62 63 64class Markdoc: 65 Tokenizer = Tokenizer 66 Tag = Tag 67 renderers = renderers 68 nodes = nodes 69 tags = tags 70 truthy = truthy 71 72 def __init__(self, config: Dict[str, Any]): 73 """Create a Markdoc wrapper with a fixed config.""" 74 self.config = config 75 76 def parse(self, content: str) -> Node: 77 """Parse Markdoc content into an AST.""" 78 return parse(content) 79 80 def resolve(self, content: Node | List[Node]): 81 """Resolve variables/functions using the stored config.""" 82 return resolve(content, self.config) 83 84 def transform(self, content: Node | List[Node]): 85 """Transform AST nodes into a renderable tree.""" 86 return transform(content, self.config) 87 88 def validate(self, content: Node | List[Node]): 89 """Validate AST nodes against the schema.""" 90 return validate(content, self.config) 91 92 93__all__ = [ 94 "Node", 95 "Tag", 96 "Tokenizer", 97 "Variable", 98 "Function", 99 "parse", 100 "resolve", 101 "transform", 102 "validate", 103 "create_element", 104 "renderers", 105 "nodes", 106 "tags", 107 "truthy", 108 "global_attributes", 109 "Markdoc", 110 "__version__", 111]
@dataclass
class
Node:
11@dataclass 12class Node: 13 """AST node for parsed Markdoc content.""" 14 15 type: str 16 children: List["Node"] = field(default_factory=list) 17 attributes: Dict[str, Any] = field(default_factory=dict) 18 tag: Optional[str] = None 19 content: Optional[str] = None 20 slots: Dict[str, "Node"] = field(default_factory=dict) 21 inline: bool = False 22 23 def resolve(self, config: Any) -> "Node": 24 """Resolve variables/functions in this node and its children.""" 25 self.attributes = _resolve_value(self.attributes, config) 26 resolved_children = [] 27 for child in self.children: 28 if isinstance(child, Node): 29 resolved_children.append(child.resolve(config)) 30 else: 31 resolved_children.append(_resolve_value(child, config)) 32 self.children = resolved_children 33 return self 34 35 def transform(self, config: Any) -> Any: 36 """Transform the node into a renderable tree.""" 37 from ..transform.transformer import transform 38 39 return transform(self, config)
AST node for parsed Markdoc content.
Node( type: str, children: List[Node] = <factory>, attributes: Dict[str, Any] = <factory>, tag: Optional[str] = None, content: Optional[str] = None, slots: Dict[str, Node] = <factory>, inline: bool = False)
children: List[Node]
slots: Dict[str, Node]
23 def resolve(self, config: Any) -> "Node": 24 """Resolve variables/functions in this node and its children.""" 25 self.attributes = _resolve_value(self.attributes, config) 26 resolved_children = [] 27 for child in self.children: 28 if isinstance(child, Node): 29 resolved_children.append(child.resolve(config)) 30 else: 31 resolved_children.append(_resolve_value(child, config)) 32 self.children = resolved_children 33 return self
Resolve variables/functions in this node and its children.
@dataclass
class
Tag:
8@dataclass 9class Tag: 10 """Renderable tag node produced by the transformer.""" 11 12 name: Optional[str] 13 attributes: Mapping[str, Any] = field(default_factory=dict) 14 children: List[Any] = field(default_factory=list) 15 self_closing: bool = False 16 17 @staticmethod 18 def is_tag(value: Any) -> bool: 19 return isinstance(value, Tag) 20 21 def with_children(self, children: Iterable[Any]) -> "Tag": 22 return Tag(self.name, dict(self.attributes), list(children))
Renderable tag node produced by the transformer.
Tag( name: Optional[str], attributes: Mapping[str, Any] = <factory>, children: List[Any] = <factory>, self_closing: bool = False)
class
Tokenizer:
9class Tokenizer: 10 def __init__(self, config: dict | None = None) -> None: 11 options = config or {} 12 self.parser = MarkdownIt("commonmark", options_update=options) if options else MarkdownIt() 13 self.parser.enable("table") 14 self.parser.disable(["lheading", "code"]) 15 16 def tokenize(self, content: str): 17 normalized = _normalize_block_tags(content) 18 return self.parser.parse(normalized, {})
@dataclass
class
Variable:
8@dataclass 9class Variable: 10 """Reference to a variable to be resolved at transform time.""" 11 12 path: List[Any] = field(default_factory=list) 13 14 def __init__(self, path: str | Iterable[Any] | None = None): 15 if path is None: 16 self.path = [] 17 elif isinstance(path, str): 18 self.path = [path] 19 else: 20 self.path = list(path) 21 22 @property 23 def name(self) -> str: 24 return _path_to_string(self.path) 25 26 def resolve(self, config: Dict[str, Any]) -> Any: 27 """Resolve the variable value from the config.""" 28 variables = config.get("variables", {}) 29 if callable(variables): 30 return variables(self.path) 31 current = variables 32 for segment in self.path: 33 if isinstance(current, dict): 34 if segment not in current: 35 return MISSING 36 current = current[segment] 37 continue 38 if isinstance(current, (list, tuple)): 39 if not isinstance(segment, int) or segment < 0 or segment >= len(current): 40 return MISSING 41 current = current[segment] 42 continue 43 return MISSING 44 return current
Reference to a variable to be resolved at transform time.
def
resolve(self, config: Dict[str, Any]) -> Any:
26 def resolve(self, config: Dict[str, Any]) -> Any: 27 """Resolve the variable value from the config.""" 28 variables = config.get("variables", {}) 29 if callable(variables): 30 return variables(self.path) 31 current = variables 32 for segment in self.path: 33 if isinstance(current, dict): 34 if segment not in current: 35 return MISSING 36 current = current[segment] 37 continue 38 if isinstance(current, (list, tuple)): 39 if not isinstance(segment, int) or segment < 0 or segment >= len(current): 40 return MISSING 41 current = current[segment] 42 continue 43 return MISSING 44 return current
Resolve the variable value from the config.
@dataclass
class
Function:
11@dataclass 12class Function: 13 """Reference to a function to be resolved at transform time.""" 14 15 name: str 16 args: List[Any] = field(default_factory=list) 17 kwargs: Dict[str, Any] = field(default_factory=dict) 18 19 def resolve(self, config: Dict[str, Any]) -> Any: 20 """Resolve the function with args/kwargs using the config.""" 21 fn = config.get("functions", {}).get(self.name) 22 if fn is None: 23 return None 24 resolved_args = [_resolve_value(arg, config) for arg in self.args] 25 resolved_kwargs = {key: _resolve_value(val, config) for key, val in self.kwargs.items()} 26 parameters = {index: value for index, value in enumerate(resolved_args)} 27 parameters.update(resolved_kwargs) 28 if callable(fn): 29 return fn(*resolved_args, **resolved_kwargs) 30 transform = fn.get("transform") if isinstance(fn, dict) else None 31 if callable(transform): 32 return _call_transform(transform, parameters, config) 33 return None
Reference to a function to be resolved at transform time.
def
resolve(self, config: Dict[str, Any]) -> Any:
19 def resolve(self, config: Dict[str, Any]) -> Any: 20 """Resolve the function with args/kwargs using the config.""" 21 fn = config.get("functions", {}).get(self.name) 22 if fn is None: 23 return None 24 resolved_args = [_resolve_value(arg, config) for arg in self.args] 25 resolved_kwargs = {key: _resolve_value(val, config) for key, val in self.kwargs.items()} 26 parameters = {index: value for index, value in enumerate(resolved_args)} 27 parameters.update(resolved_kwargs) 28 if callable(fn): 29 return fn(*resolved_args, **resolved_kwargs) 30 transform = fn.get("transform") if isinstance(fn, dict) else None 31 if callable(transform): 32 return _call_transform(transform, parameters, config) 33 return None
Resolve the function with args/kwargs using the config.
def
parse( content: str, *, file: str | None = None, slots: bool = False, location: bool = False) -> Node:
def
create_element( name: Union[str, Dict[str, Any]], attributes: Optional[Dict[str, Any]] = None, *children: Any) -> Tag:
renderers =
_Renderers()
nodes =
{'document': {'render': 'article', 'children': ['heading', 'paragraph', 'image', 'table', 'tag', 'fence', 'blockquote', 'comment', 'list', 'hr'], 'attributes': {'frontmatter': {'render': False}}}, 'heading': {'render': 'h{level}', 'children': ['inline'], 'attributes': {'level': {'type': <class 'int'>, 'render': False, 'required': True}}}, 'paragraph': {'render': 'p', 'children': ['inline']}, 'image': {'render': 'img', 'self_closing': True, 'attributes': {'src': {'type': <class 'str'>, 'required': True}, 'alt': {'type': <class 'str'>}, 'title': {'type': <class 'str'>}}}, 'fence': {'render': 'pre', 'attributes': {'content': {'type': <class 'str'>, 'render': False, 'required': True}, 'language': {'type': <class 'str'>, 'render': 'data-language'}, 'process': {'type': <class 'bool'>, 'render': False, 'default': True}}}, 'blockquote': {'render': 'blockquote', 'children': ['heading', 'paragraph', 'image', 'table', 'tag', 'fence', 'blockquote', 'list', 'hr']}, 'item': {'render': 'li', 'children': ['inline', 'heading', 'paragraph', 'image', 'table', 'tag', 'fence', 'blockquote', 'list', 'hr']}, 'list': {'render': None, 'children': ['item'], 'attributes': {'ordered': {'type': <class 'bool'>, 'render': False, 'required': True}, 'start': {'type': <class 'int'>}, 'marker': {'type': <class 'str'>, 'render': False}}}, 'hr': {'render': 'hr', 'self_closing': True}, 'table': {'render': 'table'}, 'td': {'render': 'td', 'children': ['inline', 'heading', 'paragraph', 'image', 'table', 'tag', 'fence', 'blockquote', 'list', 'hr'], 'attributes': {'align': {'type': <class 'str'>}, 'colspan': {'type': <class 'int'>, 'render': 'colSpan'}, 'rowspan': {'type': <class 'int'>, 'render': 'rowSpan'}}}, 'th': {'render': 'th', 'attributes': {'width': {'type': <class 'str'>}, 'align': {'type': <class 'str'>}, 'colspan': {'type': <class 'int'>, 'render': 'colSpan'}, 'rowspan': {'type': <class 'int'>, 'render': 'rowSpan'}}}, 'tr': {'render': 'tr', 'children': ['th', 'td']}, 'tbody': {'render': 'tbody', 'children': ['tr', 'tag']}, 'thead': {'render': 'thead', 'children': ['tr']}, 'strong': {'render': 'strong', 'children': ['em', 's', 'link', 'code_inline', 'text', 'tag'], 'attributes': {'marker': {'type': <class 'str'>, 'render': False}}}, 'em': {'render': 'em', 'children': ['strong', 's', 'link', 'code_inline', 'text', 'tag'], 'attributes': {'marker': {'type': <class 'str'>, 'render': False}}}, 's': {'render': 's', 'children': ['strong', 'em', 'link', 'code_inline', 'text', 'tag']}, 'inline': {'children': ['strong', 'em', 's', 'code_inline', 'text', 'tag', 'link', 'image', 'hardbreak', 'softbreak', 'comment']}, 'link': {'render': 'a', 'children': ['strong', 'em', 's', 'code_inline', 'text', 'tag'], 'attributes': {'href': {'type': <class 'str'>, 'required': True}, 'title': {'type': <class 'str'>}}}, 'code_inline': {'render': 'code', 'attributes': {'content': {'type': <class 'str'>, 'render': False, 'required': True}}}, 'text': {'attributes': {'content': {'type': <class 'str'>, 'required': True}}}, 'hardbreak': {'render': 'br', 'self_closing': True}, 'softbreak': {'render': None}, 'comment': {'attributes': {'content': {'type': <class 'str'>, 'required': True}}}, 'error': {}, 'node': {}, 'code': {'render': 'pre'}}
def
truthy(value: Any) -> bool:
global_attributes =
{'class': {'type': <class 'markdocpy.schema_types.class_type.ClassType'>, 'render': True}, 'id': {'type': <class 'markdocpy.schema_types.id_type.IdType'>, 'render': True}}
class
Markdoc:
65class Markdoc: 66 Tokenizer = Tokenizer 67 Tag = Tag 68 renderers = renderers 69 nodes = nodes 70 tags = tags 71 truthy = truthy 72 73 def __init__(self, config: Dict[str, Any]): 74 """Create a Markdoc wrapper with a fixed config.""" 75 self.config = config 76 77 def parse(self, content: str) -> Node: 78 """Parse Markdoc content into an AST.""" 79 return parse(content) 80 81 def resolve(self, content: Node | List[Node]): 82 """Resolve variables/functions using the stored config.""" 83 return resolve(content, self.config) 84 85 def transform(self, content: Node | List[Node]): 86 """Transform AST nodes into a renderable tree.""" 87 return transform(content, self.config) 88 89 def validate(self, content: Node | List[Node]): 90 """Validate AST nodes against the schema.""" 91 return validate(content, self.config)
Markdoc(config: Dict[str, Any])
73 def __init__(self, config: Dict[str, Any]): 74 """Create a Markdoc wrapper with a fixed config.""" 75 self.config = config
Create a Markdoc wrapper with a fixed config.
nodes =
{'document': {'render': 'article', 'children': ['heading', 'paragraph', 'image', 'table', 'tag', 'fence', 'blockquote', 'comment', 'list', 'hr'], 'attributes': {'frontmatter': {'render': False}}}, 'heading': {'render': 'h{level}', 'children': ['inline'], 'attributes': {'level': {'type': <class 'int'>, 'render': False, 'required': True}}}, 'paragraph': {'render': 'p', 'children': ['inline']}, 'image': {'render': 'img', 'self_closing': True, 'attributes': {'src': {'type': <class 'str'>, 'required': True}, 'alt': {'type': <class 'str'>}, 'title': {'type': <class 'str'>}}}, 'fence': {'render': 'pre', 'attributes': {'content': {'type': <class 'str'>, 'render': False, 'required': True}, 'language': {'type': <class 'str'>, 'render': 'data-language'}, 'process': {'type': <class 'bool'>, 'render': False, 'default': True}}}, 'blockquote': {'render': 'blockquote', 'children': ['heading', 'paragraph', 'image', 'table', 'tag', 'fence', 'blockquote', 'list', 'hr']}, 'item': {'render': 'li', 'children': ['inline', 'heading', 'paragraph', 'image', 'table', 'tag', 'fence', 'blockquote', 'list', 'hr']}, 'list': {'render': None, 'children': ['item'], 'attributes': {'ordered': {'type': <class 'bool'>, 'render': False, 'required': True}, 'start': {'type': <class 'int'>}, 'marker': {'type': <class 'str'>, 'render': False}}}, 'hr': {'render': 'hr', 'self_closing': True}, 'table': {'render': 'table'}, 'td': {'render': 'td', 'children': ['inline', 'heading', 'paragraph', 'image', 'table', 'tag', 'fence', 'blockquote', 'list', 'hr'], 'attributes': {'align': {'type': <class 'str'>}, 'colspan': {'type': <class 'int'>, 'render': 'colSpan'}, 'rowspan': {'type': <class 'int'>, 'render': 'rowSpan'}}}, 'th': {'render': 'th', 'attributes': {'width': {'type': <class 'str'>}, 'align': {'type': <class 'str'>}, 'colspan': {'type': <class 'int'>, 'render': 'colSpan'}, 'rowspan': {'type': <class 'int'>, 'render': 'rowSpan'}}}, 'tr': {'render': 'tr', 'children': ['th', 'td']}, 'tbody': {'render': 'tbody', 'children': ['tr', 'tag']}, 'thead': {'render': 'thead', 'children': ['tr']}, 'strong': {'render': 'strong', 'children': ['em', 's', 'link', 'code_inline', 'text', 'tag'], 'attributes': {'marker': {'type': <class 'str'>, 'render': False}}}, 'em': {'render': 'em', 'children': ['strong', 's', 'link', 'code_inline', 'text', 'tag'], 'attributes': {'marker': {'type': <class 'str'>, 'render': False}}}, 's': {'render': 's', 'children': ['strong', 'em', 'link', 'code_inline', 'text', 'tag']}, 'inline': {'children': ['strong', 'em', 's', 'code_inline', 'text', 'tag', 'link', 'image', 'hardbreak', 'softbreak', 'comment']}, 'link': {'render': 'a', 'children': ['strong', 'em', 's', 'code_inline', 'text', 'tag'], 'attributes': {'href': {'type': <class 'str'>, 'required': True}, 'title': {'type': <class 'str'>}}}, 'code_inline': {'render': 'code', 'attributes': {'content': {'type': <class 'str'>, 'render': False, 'required': True}}}, 'text': {'attributes': {'content': {'type': <class 'str'>, 'required': True}}}, 'hardbreak': {'render': 'br', 'self_closing': True}, 'softbreak': {'render': None}, 'comment': {'attributes': {'content': {'type': <class 'str'>, 'required': True}}}, 'error': {}, 'node': {}, 'code': {'render': 'pre'}}
77 def parse(self, content: str) -> Node: 78 """Parse Markdoc content into an AST.""" 79 return parse(content)
Parse Markdoc content into an AST.
81 def resolve(self, content: Node | List[Node]): 82 """Resolve variables/functions using the stored config.""" 83 return resolve(content, self.config)
Resolve variables/functions using the stored config.
class
Markdoc.Tokenizer:
9class Tokenizer: 10 def __init__(self, config: dict | None = None) -> None: 11 options = config or {} 12 self.parser = MarkdownIt("commonmark", options_update=options) if options else MarkdownIt() 13 self.parser.enable("table") 14 self.parser.disable(["lheading", "code"]) 15 16 def tokenize(self, content: str): 17 normalized = _normalize_block_tags(content) 18 return self.parser.parse(normalized, {})
@dataclass
class
Markdoc.Tag:
8@dataclass 9class Tag: 10 """Renderable tag node produced by the transformer.""" 11 12 name: Optional[str] 13 attributes: Mapping[str, Any] = field(default_factory=dict) 14 children: List[Any] = field(default_factory=list) 15 self_closing: bool = False 16 17 @staticmethod 18 def is_tag(value: Any) -> bool: 19 return isinstance(value, Tag) 20 21 def with_children(self, children: Iterable[Any]) -> "Tag": 22 return Tag(self.name, dict(self.attributes), list(children))
Renderable tag node produced by the transformer.
__version__ =
'0.3.0.dev1'