Markdown: A Hierarchically Indexed Markdown Document#
- class my.files.Markdown.Markdown(*, idx: str = '', tags: list[str] = [], notes: dict[str, ~typing.Any] = {}, level: ~typing.Annotated[int, ~annotated_types.Ge(ge=1), ~annotated_types.Le(le=6)] = 1, title: str = '', prose: ~my.types.Buffer.Buffer = <factory>, nodes: list[~my.files.Markdown.Markdown] = [], buffer_factory: ~collections.abc.Callable[[...], ~my.types.Buffer.Buffer] = <bound method Buffer.new of <class 'my.types.Buffer.Buffer'>>)#
Hierarchical markdown document model with parsing and manipulation.
Supports parsing from text, tree traversal, node manipulation, YAML data extraction, and rendering back to markdown with optional formatting.
I Initial Methods#
- classmethod Markdown.new(source: str | bytes | Path | Buffer | Iterable[str] | None = None, **kwargs: Any) Self#
Create a new Markdown node with proper type conversions and tree building.
Handles conversion of prose strings to Buffers, tags to lists, and recursive construction of child nodes with automatic index assignment.
- Parameters:
source – Prose text to seed the node’s buffer. Only multiline (or 256+ character) strings are stored; short single-line strings and
Pathobjects are currently discarded without effect.**kwargs – Node properties (level, idx, tags, title, prose, nodes, etc.).
- Returns:
New Markdown instance with properly initialized tree structure.
Examples
Build a small document tree, indices and levels assigned automatically:
>>> from my import Markdown >>> doc = Markdown.new( ... title='Guide', ... nodes=[ ... dict(title='Setup', prose='Install it.'), ... dict(title='Usage', prose='Run it.'), ... ], ... ) >>> [(node.idx, node.level, node.title) for node in doc.tree] [('', 1, 'Guide'), ('0', 2, 'Setup'), ('1', 2, 'Usage')]
II Primary Methods#
- Markdown.indent(num: int) Self#
Increase the header level of this node and all descendants.
- Parameters:
num – Number of levels to indent (can be negative to outdent).
- Returns:
Self for chaining.
Examples
Shift a subtree one level deeper:
>>> from my import Markdown >>> sub = dict(title='Sub', prose='x.') >>> sec = Markdown.new(title='Section', level=2, nodes=[sub]) >>> _ = sec.indent(1) >>> [(node.title, node.level) for node in sec.tree] [('Section', 3), ('Sub', 4)]
- Markdown.refresh_indices(start: int = 0, end: int | None = None) None#
Update the indices of child nodes in a range.
- Parameters:
start – First child index to update (default: 0).
end – Last child index (exclusive, default: all children).
Examples
Repair indices after reordering children by hand:
>>> from my import Markdown >>> doc = Markdown.new( ... title='Guide', ... nodes=[dict(title='A', prose='a.'), dict(title='B', prose='b.')], ... ) >>> doc.nodes.reverse() >>> doc.refresh_indices() >>> [(node.idx, node.title) for node in doc.nodes] [('0', 'B'), ('1', 'A')]
- Markdown.walk(skip_self: bool = False, asc: bool = False, max_d: int = -1) Iterator[Markdown]#
Perform depth-first traversal of the document tree.
Handles dynamic tree modifications during iteration by tracking size changes.
- Parameters:
skip_self – Whether to exclude this node from iteration.
asc – Whether to traverse in reverse order (right to left).
max_d – Maximum depth to traverse (-1 for unlimited).
- Yields:
Markdown nodes in depth-first order.
Examples
Traverse depth-first, optionally bounded or reversed:
>>> from my import Markdown >>> adv = dict(title='Advanced', prose='x.') >>> doc = Markdown.new( ... title='Guide', ... nodes=[ ... dict(title='Setup', prose='Install it.'), ... dict(title='Usage', prose='Run it.', nodes=[adv]), ... ], ... ) >>> [node.title for node in doc.walk()] ['Guide', 'Setup', 'Usage', 'Advanced'] >>> [node.title for node in doc.walk(max_d=1)] ['Guide', 'Setup', 'Usage'] >>> [node.title for node in doc.walk(asc=True)] ['Guide', 'Usage', 'Advanced', 'Setup']
- Markdown.add_node(new_nodes: Markdown | Sequence[Markdown], left: bool = False) Markdown#
Add child nodes to this markdown node.
- Parameters:
new_nodes – Single node or list of nodes to add.
left – Whether to prepend (True) or append (False).
- Returns:
Self for chaining.
Examples
Append a child; sibling indices refresh, but
levelis kept as constructed:>>> from my import Markdown >>> doc = Markdown.new(title='Guide', nodes=[dict(title='Setup', prose='x.')]) >>> _ = doc.add_node(Markdown.new(title='FAQ', prose='Q & A.', level=2)) >>> [(node.idx, node.title) for node in doc.nodes] [('0', 'Setup'), ('1', 'FAQ')]
- Markdown.get(**kwargs: Any) Markdown | None#
Get a descendant node by one of various criteria.
- Parameters:
**kwargs – One of: idx, child, title, or path.
- Returns:
Matching Markdown node or None.
- Raises:
ValueError – If invalid parameter combination.
Examples
Fetch descendants by index string, child position, title, or index path (the same criteria back
get_idx(),get_child(),get_title(), andget_path()):>>> from my import Markdown >>> adv = dict(title='Advanced', prose='x.') >>> doc = Markdown.new( ... title='Guide', ... nodes=[ ... dict(title='Setup', prose='Install it.'), ... dict(title='Usage', prose='Run it.', nodes=[adv]), ... ], ... ) >>> doc.get(idx='10').title 'Advanced' >>> doc.get(child=0).title 'Setup' >>> doc.get(title='advanced').title 'Advanced' >>> doc.get(path=[1, 0]).title 'Advanced'
- Markdown.get_idx(*, idx: str = '', asc: bool = False, max_d: int = -1) Markdown | None#
Get a descendant node by its index string.
- Parameters:
idx – Target index string.
asc – Whether to traverse in reverse order (right to left).
max_d – Maximum depth to traverse (-1 for unlimited).
- Returns:
Matching Markdown node or None.
- Markdown.get_child(*, child: int = -1, asc: bool = False, max_d: int = -1) Markdown | None#
Get a direct child node by its index.
- Parameters:
child – Child index (0-based).
asc – Whether to traverse in reverse order (right to left).
max_d – Maximum depth to traverse (-1 for unlimited).
- Returns:
Matching Markdown node or None.
- Markdown.get_title(*, title: str = '', asc: bool = False, max_d: int = -1) Markdown | None#
Get a descendant node by its title.
- Parameters:
title – Target title string.
asc – Whether to traverse in reverse order (right to left).
max_d – Maximum depth to traverse (-1 for unlimited).
- Returns:
Matching Markdown node or None.
- Markdown.get_path(*, path: list[int] | None = None, asc: bool = False, max_d: int = -1) Markdown | None#
Get a descendant node by a path of child indices (each relative to its parent).
- Parameters:
path – List of child indices leading to the target node.
asc – Whether to traverse in reverse order (right to left).
max_d – Maximum depth to traverse (-1 for unlimited).
- Returns:
Matching Markdown node or None.
- Markdown.trace_path(target: str | list[int]) list[Markdown]#
Get all nodes along the path from this node to a target.
- Parameters:
target – Target index string or list of child indices.
- Returns:
List of nodes from this node to target (inclusive). Empty list if target not found or invalid.
Examples
Collect the chain of nodes leading to a target:
>>> from my import Markdown >>> adv = dict(title='Advanced', prose='x.') >>> doc = Markdown.new( ... title='Guide', ... nodes=[dict(title='Usage', prose='Run it.', nodes=[adv])], ... ) >>> [node.title for node in doc.trace_path('00')] ['Guide', 'Usage', 'Advanced']
- Markdown.set_idx(base_idx: str = '', rel_idx: int | str = 0) None#
Set this node’s index and recursively update all descendants.
- Parameters:
base_idx – Parent’s index string.
rel_idx – This node’s position relative to parent (0-61).
Examples
Rebase a subtree’s indices onto a new parent position:
>>> from my import Markdown >>> adv = dict(title='Advanced', prose='x.') >>> node = Markdown.new(title='Usage', level=2, nodes=[adv]) >>> node.set_idx('3', 2) >>> (node.idx, node.nodes[0].idx) ('32', '320')
III Properties#
- property Markdown.tree: Iterator[Markdown]#
Iterates depth-first over all nodes in this markdown object. See walk().
- property Markdown.prose_tree: Iterator[Buffer]#
Iterates depth-first over all text content in this markdown object.
- property Markdown.prefix: str#
The backtick-escaped prefix for this node’s title, or emptystring if there is none.
- property Markdown.header: str#
Returns the full, markdown-ready header line for this node.
Examples
The header composes the level’s hashes, the
prefix, and the title:>>> from my import Markdown >>> node = Markdown.new(title='Tagged', tags=['draft'], idx='3', level=2) >>> node.prefix '`3 draft`' >>> node.header '## `3 draft` Tagged'
- property Markdown.fulltext: str#
Returns the full text of this markdown object.
Examples
Join headers and prose across the whole tree:
>>> from my import Markdown >>> sub = dict(title='Sub', prose='More.') >>> doc = Markdown.new(title='Note', prose='Body.', nodes=[sub]) >>> doc.fulltext '# Note\n\nBody.\n\n## `0` Sub\n\nMore.'
IV Standard#
- Markdown.pop(**kwargs: Any) Markdown | None#
Remove and return a descendant node.
- Parameters:
**kwargs – Node search criteria (passed to get()).
- Returns:
Removed node, or None if not found.
Examples
Remove a node by any
get()criteria; its siblings re-index:>>> from my import Markdown >>> doc = Markdown.new( ... title='Guide', ... nodes=[dict(title='Setup', prose='a.'), dict(title='Usage', prose='b.')], ... ) >>> doc.pop(title='Setup').title 'Setup' >>> [(node.idx, node.title) for node in doc.nodes] [('0', 'Usage')]
- Markdown.replace(orig: str | Pattern, new: str) None#
Replace text in all prose buffers throughout the tree.
- Parameters:
orig – String or regex pattern to replace.
new – Replacement string.
Examples
Substitute text across every node’s prose:
>>> from my import Markdown >>> doc = Markdown.new( ... title='Guide', ... prose='Use foo.', ... nodes=[dict(title='Sub', prose='More foo.')], ... ) >>> doc.replace('foo', 'bar') >>> [str(node.prose) for node in doc.tree] ['Use bar.', 'More bar.']
V Parsing & Rendering#
- classmethod Markdown.parse(text: str | Buffer, base_level: int = 0) list[Self]#
Parse markdown text into a hierarchical tree structure.
Recognizes headers, tags, indices, and prose to build nested Markdown nodes. Automatically handles “Notes” sections by parsing them as YAML. Lines inside fenced code blocks are ignored by the header scanner, so a
#comment in a code fence is kept as prose rather than misread as a header. Empty sections are preserved, including adjacent headings with no intervening prose, so their descendants remain attached to the right parent.- Parameters:
text – Markdown text or Buffer to parse.
base_level – Minimum header level to parse (default: 0 for all).
- Returns:
List of top-level Markdown nodes with nested children.
Examples
Parse nested sections – including tagged, indexed headers – into a tree:
>>> from my import Markdown >>> text = '# Guide\n\nIntro.\n\n## `0 draft` Usage\n\nRun it.\n' >>> doc = Markdown.parse(text)[0] >>> [(n.level, n.idx, n.tags, n.title) for n in doc.tree] [(1, '', [], 'Guide'), (2, '0', ['draft'], 'Usage')]
Empty parent sections remain in the hierarchy:
>>> text = '# Guide\n\n## Empty\n\n### Leaf\n\nBody.\n' >>> doc = Markdown.parse(text)[0] >>> [(n.level, n.title, str(n.prose)) for n in doc.tree] [(1, 'Guide', ''), (2, 'Empty', ''), (3, 'Leaf', 'Body.')]
- Markdown.parse_predicates() Predicate#
Parse a YAML-filled markdown document into a Predicate object.
- Markdown.reparse_prose() None#
Identify and parse newly-created header nodes embedded in this node’s raw prose.
Examples
Promote headers typed into prose to real child nodes:
>>> from my import Markdown >>> node = Markdown.new(title='Host', level=1) >>> node.prose = node.buffer_factory('Intro.\n\n## Fresh\n\nNew content.') >>> node.reparse_prose() >>> [(n.level, n.title) for n in node.tree] [(1, 'Host'), (2, 'Fresh')] >>> str(node.prose).strip() 'Intro.'
- Markdown.from_yaml() dict[str, Any]#
Parses prose as YAML and recursively collects child nodes as nested dicts.
- Returns:
Dictionary of parsed YAML data with child nodes as nested keys.
Examples
Collect YAML prose and child sections into one dict:
>>> from my import Markdown >>> cfg = Markdown.new( ... title='Config', ... prose='debug: true', ... nodes=[dict(title='paths', prose='root: /tmp')], ... ) >>> cfg.from_yaml() {'debug': True, 'paths': {'root': '/tmp'}}
- Markdown.render(data: dict[str, Any], fix: bool = True) str#
Render markdown template with the given data dict.
- Parameters:
data – Data to render template with.
fix – Whether to format output with mdformat.
- Returns:
Rendered markdown string.
- Markdown.to_string(strip_notes: bool = False, fix: bool = True) str#
Render this node and its children as markdown text.
- Parameters:
strip_notes – Whether to exclude notes from output.
fix – Whether to apply mdformat formatting.
- Returns:
Formatted markdown string.
Examples
Root-level notes render as YAML frontmatter unless stripped:
>>> from my import Markdown >>> noted = Markdown.new(title='Doc', prose='Body.', notes={'k': 'v'}) >>> noted.to_string() '---\nk: v\n---\n\n# Doc\n\nBody.\n' >>> noted.to_string(strip_notes=True) '# Doc\n\nBody.\n'