codekingpro/portable-devtools
114k
1"""A tree representation of a linear markdown-it token stream.2 3This module is not part of upstream JavaScript markdown-it.4"""5 6from __future__ import annotations7 8from collections.abc import Generator, Sequence9import textwrap10from typing import Any, NamedTuple, TypeVar, overload11 12from .token import Token13 14 15class _NesterTokens(NamedTuple):16 opening: Token17 closing: Token18 19 20_NodeType = TypeVar("_NodeType", bound="SyntaxTreeNode")21 22 23class SyntaxTreeNode:24 """A Markdown syntax tree node.25 26 A class that can be used to construct a tree representation of a linear27 `markdown-it-py` token stream.28 29 Each node in the tree represents either:30 - root of the Markdown document31 - a single unnested `Token`32 - a `Token` "_open" and "_close" token pair, and the tokens nested in33 between34 """35 36 def __init__(37 self, tokens: Sequence[Token] = (), *, create_root: bool = True38 ) -> None:39 """Initialize a `SyntaxTreeNode` from a token stream.40 41 If `create_root` is True, create a root node for the document.42 """43 # Only nodes representing an unnested token have self.token44 self.token: Token | None = None45 46 # Only containers have nester tokens47 self.nester_tokens: _NesterTokens | None = None48 49 # Root node does not have self.parent50 self._parent: Any = None51 52 # Empty list unless a non-empty container, or unnested token that has53 # children (i.e. inline or img)54 self._children: list[Any] = []55 56 if create_root:57 self._set_children_from_tokens(tokens)58 return59 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_token72 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])77 78 def __repr__(self) -> str:79 return f"{type(self).__name__}({self.type})"80 81 @overload82 def __getitem__(self: _NodeType, item: int) -> _NodeType: ...83 84 @overload85 def __getitem__(self: _NodeType, item: slice) -> list[_NodeType]: ...86 87 def __getitem__(self: _NodeType, item: int | slice) -> _NodeType | list[_NodeType]:88 return self.children[item]89 90 def to_tokens(self: _NodeType) -> list[Token]:91 """Recover the linear token stream."""92 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_tokens101 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)105 106 tokens: list[Token] = []107 recursive_collect_tokens(self, tokens)108 return tokens109 110 @property111 def children(self: _NodeType) -> list[_NodeType]:112 return self._children113 114 @children.setter115 def children(self: _NodeType, value: list[_NodeType]) -> None:116 self._children = value117 118 @property119 def parent(self: _NodeType) -> _NodeType | None:120 return self._parent # type: ignore121 122 @parent.setter123 def parent(self: _NodeType, value: _NodeType | None) -> None:124 self._parent = value125 126 @property127 def is_root(self) -> bool:128 """Is the node a special root node?"""129 return not (self.token or self.nester_tokens)130 131 @property132 def is_nested(self) -> bool:133 """Is this node nested?.134 135 Returns `True` if the node represents a `Token` pair and tokens in the136 sequence between them, where `Token.nesting` of the first `Token` in137 the pair is 1 and nesting of the other `Token` is -1.138 """139 return bool(self.nester_tokens)140 141 @property142 def siblings(self: _NodeType) -> Sequence[_NodeType]:143 """Get siblings of the node.144 145 Gets the whole group of siblings, including self.146 """147 if not self.parent:148 return [self]149 return self.parent.children150 151 @property152 def type(self) -> str:153 """Get a string type of the represented syntax.154 155 - "root" for root nodes156 - `Token.type` if the node represents an unnested token157 - `Token.type` of the opening token, with "_open" suffix stripped, if158 the node represents a nester token pair159 """160 if self.is_root:161 return "root"162 if self.token:163 return self.token.type164 assert self.nester_tokens165 return self.nester_tokens.opening.type.removesuffix("_open")166 167 @property168 def next_sibling(self: _NodeType) -> _NodeType | None:169 """Get the next node in the sequence of siblings.170 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 None177 178 @property179 def previous_sibling(self: _NodeType) -> _NodeType | None:180 """Get the previous node in the sequence of siblings.181 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 None188 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 = self196 self.children.append(child)197 198 def _set_children_from_tokens(self, tokens: Sequence[Token]) -> None:199 """Convert the token stream to a tree structure and set the resulting200 nodes as children of `self`."""201 reversed_tokens = list(reversed(tokens))202 while reversed_tokens:203 token = reversed_tokens.pop()204 205 if not token.nesting:206 self._add_child([token])207 continue208 if token.nesting != 1:209 raise ValueError("Invalid token nesting")210 211 nested_tokens = [token]212 nesting = 1213 while reversed_tokens and nesting:214 token = reversed_tokens.pop()215 nested_tokens.append(token)216 nesting += token.nesting217 if nesting:218 raise ValueError(f"unclosed tokens starting {nested_tokens[0]}")219 220 self._add_child(nested_tokens)221 222 def pretty(223 self, *, indent: int = 2, show_text: bool = False, _current: int = 0224 ) -> str:225 """Create an XML style string of the tree."""226 prefix = " " * _current227 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_text233 and not self.is_root234 and self.type in ("text", "text_special")235 and self.content236 ):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 + indent241 )242 return text243 244 def walk(245 self: _NodeType, *, include_self: bool = True246 ) -> Generator[_NodeType, None, None]:247 """Recursively yield all descendant nodes in the tree starting at self.248 249 The order mimics the order of the underlying linear token250 stream (i.e. depth first).251 """252 if include_self:253 yield self254 for child in self.children:255 yield from child.walk(include_self=True)256 257 # NOTE:258 # The values of the properties defined below directly map to properties259 # 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 an261 # `AttributeError`.262 #263 # There is no mapping for `Token.nesting` because the `is_nested` property264 # provides that data, and can be called on any node type, including root.265 266 def _attribute_token(self) -> Token:267 """Return the `Token` that is used as the data source for the268 properties defined below."""269 if self.token:270 return self.token271 if self.nester_tokens:272 return self.nester_tokens.opening273 raise AttributeError("Root node does not have the accessed attribute")274 275 @property276 def tag(self) -> str:277 """html tag name, e.g. \"p\""""278 return self._attribute_token().tag279 280 @property281 def attrs(self) -> dict[str, str | int | float]:282 """Html attributes."""283 return self._attribute_token().attrs284 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)288 289 @property290 def map(self) -> tuple[int, int] | None:291 """Source map info. Format: `tuple[ line_begin, line_end ]`"""292 map_ = self._attribute_token().map293 if map_:294 # Type ignore because `Token`s attribute types are not perfect295 return tuple(map_) # type: ignore296 return None297 298 @property299 def level(self) -> int:300 """nesting level, the same as `state.level`"""301 return self._attribute_token().level302 303 @property304 def content(self) -> str:305 """In a case of self-closing tag (code, html, fence, etc.), it306 has contents of this tag."""307 return self._attribute_token().content308 309 @property310 def markup(self) -> str:311 """'*' or '_' for emphasis, fence string for fence, etc."""312 return self._attribute_token().markup313 314 @property315 def info(self) -> str:316 """fence infostring"""317 return self._attribute_token().info318 319 @property320 def meta(self) -> dict[Any, Any]:321 """A place for plugins to store an arbitrary data."""322 return self._attribute_token().meta323 324 @property325 def block(self) -> bool:326 """True for block-level tokens, false for inline tokens."""327 return self._attribute_token().block328 329 @property330 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().hidden334 