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

175 statements  

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 annotations 

7 

8from collections.abc import Generator, Sequence 

9import textwrap 

10from typing import Any, NamedTuple, TypeVar, overload 

11 

12from .token import Token 

13 

14 

15class _NesterTokens(NamedTuple): 

16 opening: Token 

17 closing: Token 

18 

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 linear 

27 `markdown-it-py` token stream. 

28 

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 """ 

35 

36 def __init__( 

37 self, tokens: Sequence[Token] = (), *, create_root: bool = True 

38 ) -> 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.token 

44 self.token: Token | None = None 

45 

46 # Only containers have nester tokens 

47 self.nester_tokens: _NesterTokens | None = None 

48 

49 # Root node does not have self.parent 

50 self._parent: Any = None 

51 

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] = [] 

55 

56 if create_root: 

57 self._set_children_from_tokens(tokens) 

58 return 

59 

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]) 

77 

78 def __repr__(self) -> str: 

79 return f"{type(self).__name__}({self.type})" 

80 

81 @overload 

82 def __getitem__(self: _NodeType, item: int) -> _NodeType: ... 

83 

84 @overload 

85 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_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) 

105 

106 tokens: list[Token] = [] 

107 recursive_collect_tokens(self, tokens) 

108 return tokens 

109 

110 @property 

111 def children(self: _NodeType) -> list[_NodeType]: 

112 return self._children 

113 

114 @children.setter 

115 def children(self: _NodeType, value: list[_NodeType]) -> None: 

116 self._children = value 

117 

118 @property 

119 def parent(self: _NodeType) -> _NodeType | None: 

120 return self._parent # type: ignore 

121 

122 @parent.setter 

123 def parent(self: _NodeType, value: _NodeType | None) -> None: 

124 self._parent = value 

125 

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) 

130 

131 @property 

132 def is_nested(self) -> bool: 

133 """Is this node nested?. 

134 

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) 

140 

141 @property 

142 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.children 

150 

151 @property 

152 def type(self) -> str: 

153 """Get a string type of the represented syntax. 

154 

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") 

166 

167 @property 

168 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 None 

177 

178 @property 

179 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 None 

188 

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) 

197 

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() 

204 

205 if not token.nesting: 

206 self._add_child([token]) 

207 continue 

208 if token.nesting != 1: 

209 raise ValueError("Invalid token nesting") 

210 

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]}") 

219 

220 self._add_child(nested_tokens) 

221 

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 

243 

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. 

248 

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) 

256 

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. 

265 

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") 

274 

275 @property 

276 def tag(self) -> str: 

277 """html tag name, e.g. \"p\"""" 

278 return self._attribute_token().tag 

279 

280 @property 

281 def attrs(self) -> dict[str, str | int | float]: 

282 """Html attributes.""" 

283 return self._attribute_token().attrs 

284 

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 @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 

297 

298 @property 

299 def level(self) -> int: 

300 """nesting level, the same as `state.level`""" 

301 return self._attribute_token().level 

302 

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 

308 

309 @property 

310 def markup(self) -> str: 

311 """'*' or '_' for emphasis, fence string for fence, etc.""" 

312 return self._attribute_token().markup 

313 

314 @property 

315 def info(self) -> str: 

316 """fence infostring""" 

317 return self._attribute_token().info 

318 

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 

323 

324 @property 

325 def block(self) -> bool: 

326 """True for block-level tokens, false for inline tokens.""" 

327 return self._attribute_token().block 

328 

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