Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/markdown_it/tree.py: 37%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""A tree representation of a linear markdown-it token stream.
3This module is not part of upstream JavaScript markdown-it.
4"""
6from __future__ import annotations
8from collections.abc import Generator, Sequence
9import textwrap
10from typing import Any, NamedTuple, TypeVar, overload
12from .token import Token
15class _NesterTokens(NamedTuple):
16 opening: Token
17 closing: Token
20_NodeType = TypeVar("_NodeType", bound="SyntaxTreeNode")
23class SyntaxTreeNode:
24 """A Markdown syntax tree node.
26 A class that can be used to construct a tree representation of a linear
27 `markdown-it-py` token stream.
29 Each node in the tree represents either:
30 - root of the Markdown document
31 - a single unnested `Token`
32 - a `Token` "_open" and "_close" token pair, and the tokens nested in
33 between
34 """
36 def __init__(
37 self, tokens: Sequence[Token] = (), *, create_root: bool = True
38 ) -> None:
39 """Initialize a `SyntaxTreeNode` from a token stream.
41 If `create_root` is True, create a root node for the document.
42 """
43 # Only nodes representing an unnested token have self.token
44 self.token: Token | None = None
46 # Only containers have nester tokens
47 self.nester_tokens: _NesterTokens | None = None
49 # Root node does not have self.parent
50 self._parent: Any = None
52 # Empty list unless a non-empty container, or unnested token that has
53 # children (i.e. inline or img)
54 self._children: list[Any] = []
56 if create_root:
57 self._set_children_from_tokens(tokens)
58 return
60 if not tokens:
61 raise ValueError(
62 "Can only create root from empty token sequence."
63 " Set `create_root=True`."
64 )
65 elif len(tokens) == 1:
66 inline_token = tokens[0]
67 if inline_token.nesting:
68 raise ValueError(
69 "Unequal nesting level at the start and end of token stream."
70 )
71 self.token = inline_token
72 if inline_token.children:
73 self._set_children_from_tokens(inline_token.children)
74 else:
75 self.nester_tokens = _NesterTokens(tokens[0], tokens[-1])
76 self._set_children_from_tokens(tokens[1:-1])
78 def __repr__(self) -> str:
79 return f"{type(self).__name__}({self.type})"
81 @overload
82 def __getitem__(self: _NodeType, item: int) -> _NodeType: ...
84 @overload
85 def __getitem__(self: _NodeType, item: slice) -> list[_NodeType]: ...
87 def __getitem__(self: _NodeType, item: int | slice) -> _NodeType | list[_NodeType]:
88 return self.children[item]
90 def to_tokens(self: _NodeType) -> list[Token]:
91 """Recover the linear token stream."""
93 def recursive_collect_tokens(node: _NodeType, token_list: list[Token]) -> None:
94 if node.type == "root":
95 for child in node.children:
96 recursive_collect_tokens(child, token_list)
97 elif node.token:
98 token_list.append(node.token)
99 else:
100 assert node.nester_tokens
101 token_list.append(node.nester_tokens.opening)
102 for child in node.children:
103 recursive_collect_tokens(child, token_list)
104 token_list.append(node.nester_tokens.closing)
106 tokens: list[Token] = []
107 recursive_collect_tokens(self, tokens)
108 return tokens
110 @property
111 def children(self: _NodeType) -> list[_NodeType]:
112 return self._children
114 @children.setter
115 def children(self: _NodeType, value: list[_NodeType]) -> None:
116 self._children = value
118 @property
119 def parent(self: _NodeType) -> _NodeType | None:
120 return self._parent # type: ignore
122 @parent.setter
123 def parent(self: _NodeType, value: _NodeType | None) -> None:
124 self._parent = value
126 @property
127 def is_root(self) -> bool:
128 """Is the node a special root node?"""
129 return not (self.token or self.nester_tokens)
131 @property
132 def is_nested(self) -> bool:
133 """Is this node nested?.
135 Returns `True` if the node represents a `Token` pair and tokens in the
136 sequence between them, where `Token.nesting` of the first `Token` in
137 the pair is 1 and nesting of the other `Token` is -1.
138 """
139 return bool(self.nester_tokens)
141 @property
142 def siblings(self: _NodeType) -> Sequence[_NodeType]:
143 """Get siblings of the node.
145 Gets the whole group of siblings, including self.
146 """
147 if not self.parent:
148 return [self]
149 return self.parent.children
151 @property
152 def type(self) -> str:
153 """Get a string type of the represented syntax.
155 - "root" for root nodes
156 - `Token.type` if the node represents an unnested token
157 - `Token.type` of the opening token, with "_open" suffix stripped, if
158 the node represents a nester token pair
159 """
160 if self.is_root:
161 return "root"
162 if self.token:
163 return self.token.type
164 assert self.nester_tokens
165 return self.nester_tokens.opening.type.removesuffix("_open")
167 @property
168 def next_sibling(self: _NodeType) -> _NodeType | None:
169 """Get the next node in the sequence of siblings.
171 Returns `None` if this is the last sibling.
172 """
173 self_index = self.siblings.index(self)
174 if self_index + 1 < len(self.siblings):
175 return self.siblings[self_index + 1]
176 return None
178 @property
179 def previous_sibling(self: _NodeType) -> _NodeType | None:
180 """Get the previous node in the sequence of siblings.
182 Returns `None` if this is the first sibling.
183 """
184 self_index = self.siblings.index(self)
185 if self_index - 1 >= 0:
186 return self.siblings[self_index - 1]
187 return None
189 def _add_child(
190 self,
191 tokens: Sequence[Token],
192 ) -> None:
193 """Make a child node for `self`."""
194 child = type(self)(tokens, create_root=False)
195 child.parent = self
196 self.children.append(child)
198 def _set_children_from_tokens(self, tokens: Sequence[Token]) -> None:
199 """Convert the token stream to a tree structure and set the resulting
200 nodes as children of `self`."""
201 reversed_tokens = list(reversed(tokens))
202 while reversed_tokens:
203 token = reversed_tokens.pop()
205 if not token.nesting:
206 self._add_child([token])
207 continue
208 if token.nesting != 1:
209 raise ValueError("Invalid token nesting")
211 nested_tokens = [token]
212 nesting = 1
213 while reversed_tokens and nesting:
214 token = reversed_tokens.pop()
215 nested_tokens.append(token)
216 nesting += token.nesting
217 if nesting:
218 raise ValueError(f"unclosed tokens starting {nested_tokens[0]}")
220 self._add_child(nested_tokens)
222 def pretty(
223 self, *, indent: int = 2, show_text: bool = False, _current: int = 0
224 ) -> str:
225 """Create an XML style string of the tree."""
226 prefix = " " * _current
227 text = prefix + f"<{self.type}"
228 if not self.is_root and self.attrs:
229 text += " " + " ".join(f"{k}={v!r}" for k, v in self.attrs.items())
230 text += ">"
231 if (
232 show_text
233 and not self.is_root
234 and self.type in ("text", "text_special")
235 and self.content
236 ):
237 text += "\n" + textwrap.indent(self.content, prefix + " " * indent)
238 for child in self.children:
239 text += "\n" + child.pretty(
240 indent=indent, show_text=show_text, _current=_current + indent
241 )
242 return text
244 def walk(
245 self: _NodeType, *, include_self: bool = True
246 ) -> Generator[_NodeType, None, None]:
247 """Recursively yield all descendant nodes in the tree starting at self.
249 The order mimics the order of the underlying linear token
250 stream (i.e. depth first).
251 """
252 if include_self:
253 yield self
254 for child in self.children:
255 yield from child.walk(include_self=True)
257 # NOTE:
258 # The values of the properties defined below directly map to properties
259 # of the underlying `Token`s. A root node does not translate to a `Token`
260 # object, so calling these property getters on a root node will raise an
261 # `AttributeError`.
262 #
263 # There is no mapping for `Token.nesting` because the `is_nested` property
264 # provides that data, and can be called on any node type, including root.
266 def _attribute_token(self) -> Token:
267 """Return the `Token` that is used as the data source for the
268 properties defined below."""
269 if self.token:
270 return self.token
271 if self.nester_tokens:
272 return self.nester_tokens.opening
273 raise AttributeError("Root node does not have the accessed attribute")
275 @property
276 def tag(self) -> str:
277 """html tag name, e.g. \"p\""""
278 return self._attribute_token().tag
280 @property
281 def attrs(self) -> dict[str, str | int | float]:
282 """Html attributes."""
283 return self._attribute_token().attrs
285 def attrGet(self, name: str) -> None | str | int | float:
286 """Get the value of attribute `name`, or null if it does not exist."""
287 return self._attribute_token().attrGet(name)
289 @property
290 def map(self) -> tuple[int, int] | None:
291 """Source map info. Format: `tuple[ line_begin, line_end ]`"""
292 map_ = self._attribute_token().map
293 if map_:
294 # Type ignore because `Token`s attribute types are not perfect
295 return tuple(map_) # type: ignore
296 return None
298 @property
299 def level(self) -> int:
300 """nesting level, the same as `state.level`"""
301 return self._attribute_token().level
303 @property
304 def content(self) -> str:
305 """In a case of self-closing tag (code, html, fence, etc.), it
306 has contents of this tag."""
307 return self._attribute_token().content
309 @property
310 def markup(self) -> str:
311 """'*' or '_' for emphasis, fence string for fence, etc."""
312 return self._attribute_token().markup
314 @property
315 def info(self) -> str:
316 """fence infostring"""
317 return self._attribute_token().info
319 @property
320 def meta(self) -> dict[Any, Any]:
321 """A place for plugins to store an arbitrary data."""
322 return self._attribute_token().meta
324 @property
325 def block(self) -> bool:
326 """True for block-level tokens, false for inline tokens."""
327 return self._attribute_token().block
329 @property
330 def hidden(self) -> bool:
331 """If it's true, ignore this element when rendering.
332 Used for tight lists to hide paragraphs."""
333 return self._attribute_token().hidden