1# Python Markdown
2
3# A Python implementation of John Gruber's Markdown.
4
5# Documentation: https://python-markdown.github.io/
6# GitHub: https://github.com/Python-Markdown/markdown/
7# PyPI: https://pypi.org/project/Markdown/
8
9# Started by Manfred Stienstra (http://www.dwerg.net/).
10# Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
11# Currently maintained by Waylan Limberg (https://github.com/waylan),
12# Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
13
14# Copyright 2007-2023 The Python Markdown Project (v. 1.7 and later)
15# Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
16# Copyright 2004 Manfred Stienstra (the original version)
17
18# License: BSD (see LICENSE.md for details).
19
20"""
21Tree processors manipulate the tree created by block processors. They can even create an entirely
22new `ElementTree` object. This is an excellent place for creating summaries, adding collected
23references, or last minute adjustments.
24
25"""
26
27from __future__ import annotations
28
29import re
30import xml.etree.ElementTree as etree
31from typing import TYPE_CHECKING, Any
32from . import util
33from . import inlinepatterns
34
35if TYPE_CHECKING: # pragma: no cover
36 from markdown import Markdown
37
38
39def build_treeprocessors(md: Markdown, **kwargs: Any) -> util.Registry[Treeprocessor]:
40 """ Build the default `treeprocessors` for Markdown. """
41 treeprocessors = util.Registry()
42 treeprocessors.register(InlineProcessor(md), 'inline', 20)
43 treeprocessors.register(PrettifyTreeprocessor(md), 'prettify', 10)
44 treeprocessors.register(UnescapeTreeprocessor(md), 'unescape', 0)
45 return treeprocessors
46
47
48def isString(s: object) -> bool:
49 """ Return `True` if object is a string but not an [`AtomicString`][markdown.util.AtomicString]. """
50 if not isinstance(s, util.AtomicString):
51 return isinstance(s, str)
52 return False
53
54
55class Treeprocessor(util.Processor):
56 """
57 `Treeprocessor`s are run on the `ElementTree` object before serialization.
58
59 Each `Treeprocessor` implements a `run` method that takes a pointer to an
60 `Element` and modifies it as necessary.
61
62 `Treeprocessors` must extend `markdown.Treeprocessor`.
63
64 """
65 def run(self, root: etree.Element) -> etree.Element | None:
66 """
67 Subclasses of `Treeprocessor` should implement a `run` method, which
68 takes a root `Element`. This method can return another `Element`
69 object, and the existing root `Element` will be replaced, or it can
70 modify the current tree and return `None`.
71 """
72 pass # pragma: no cover
73
74
75class InlineProcessor(Treeprocessor):
76 """
77 A `Treeprocessor` that traverses a tree, applying inline patterns.
78 """
79
80 def __init__(self, md: Markdown):
81 self.__placeholder_prefix = util.INLINE_PLACEHOLDER_PREFIX
82 self.__placeholder_suffix = util.ETX
83 self.__placeholder_length = 4 + len(self.__placeholder_prefix) \
84 + len(self.__placeholder_suffix)
85 self.__placeholder_re = util.INLINE_PLACEHOLDER_RE
86 self.md = md
87 self.inlinePatterns = md.inlinePatterns
88 self.ancestors: list[str] = []
89
90 def __makePlaceholder(self, type: str) -> tuple[str, str]:
91 """ Generate a placeholder """
92 id = "%04d" % len(self.stashed_nodes)
93 hash = util.INLINE_PLACEHOLDER % id
94 return hash, id
95
96 def __findPlaceholder(self, data: str, index: int) -> tuple[str | None, int]:
97 """
98 Extract id from data string, start from index.
99
100 Arguments:
101 data: String.
102 index: Index, from which we start search.
103
104 Returns:
105 Placeholder id and string index, after the found placeholder.
106
107 """
108 m = self.__placeholder_re.search(data, index)
109 if m:
110 return m.group(1), m.end()
111 else:
112 return None, index + 1
113
114 def __stashNode(self, node: etree.Element | str, type: str) -> str:
115 """ Add node to stash. """
116 placeholder, id = self.__makePlaceholder(type)
117 self.stashed_nodes[id] = node
118 return placeholder
119
120 def __handleInline(self, data: str, patternIndex: int = 0) -> str:
121 """
122 Process string with inline patterns and replace it with placeholders.
123
124 Arguments:
125 data: A line of Markdown text.
126 patternIndex: The index of the `inlinePattern` to start with.
127
128 Returns:
129 String with placeholders.
130
131 """
132 if not isinstance(data, util.AtomicString):
133 startIndex = 0
134 count = len(self.inlinePatterns)
135 while patternIndex < count:
136 data, matched, startIndex = self.__applyPattern(
137 self.inlinePatterns[patternIndex], data, patternIndex, startIndex
138 )
139 if not matched:
140 patternIndex += 1
141 return data
142
143 def __processElementText(self, node: etree.Element, subnode: etree.Element, isText: bool = True) -> None:
144 """
145 Process placeholders in `Element.text` or `Element.tail`
146 of Elements popped from `self.stashed_nodes`.
147
148 Arguments:
149 node: Parent node.
150 subnode: Processing node.
151 isText: Boolean variable, True - it's text, False - it's a tail.
152
153 """
154 if isText:
155 text = subnode.text
156 subnode.text = None
157 else:
158 text = subnode.tail
159 subnode.tail = None
160
161 childResult = self.__processPlaceholders(text, subnode, isText)
162
163 if not isText and node is not subnode:
164 pos = list(node).index(subnode) + 1
165 else:
166 pos = 0
167
168 childResult.reverse()
169 for newChild in childResult:
170 node.insert(pos, newChild[0])
171
172 def __processPlaceholders(
173 self,
174 data: str | None,
175 parent: etree.Element,
176 isText: bool = True
177 ) -> list[tuple[etree.Element, list[str]]]:
178 """
179 Process string with placeholders and generate `ElementTree` tree.
180
181 Arguments:
182 data: String with placeholders instead of `ElementTree` elements.
183 parent: Element, which contains processing inline data.
184 isText: Boolean variable, True - it's text, False - it's a tail.
185
186 Returns:
187 List with `ElementTree` elements with applied inline patterns.
188
189 """
190 def linkText(text: str | None) -> None:
191 if text:
192 if result:
193 if result[-1][0].tail:
194 result[-1][0].tail += text
195 else:
196 result[-1][0].tail = text
197 elif not isText:
198 if parent.tail:
199 parent.tail += text
200 else:
201 parent.tail = text
202 else:
203 if parent.text:
204 parent.text += text
205 else:
206 parent.text = text
207 result = []
208 strartIndex = 0
209 while data:
210 index = data.find(self.__placeholder_prefix, strartIndex)
211 if index != -1:
212 id, phEndIndex = self.__findPlaceholder(data, index)
213
214 if id in self.stashed_nodes:
215 node = self.stashed_nodes.get(id)
216
217 if index > 0:
218 text = data[strartIndex:index]
219 linkText(text)
220
221 if not isinstance(node, str): # it's Element
222 for child in [node] + list(node):
223 if child.tail:
224 if child.tail.strip():
225 self.__processElementText(
226 node, child, False
227 )
228 if child.text:
229 if child.text.strip():
230 self.__processElementText(child, child)
231 else: # it's just a string
232 linkText(node)
233 strartIndex = phEndIndex
234 continue
235
236 strartIndex = phEndIndex
237 result.append((node, self.ancestors[:]))
238
239 else: # wrong placeholder
240 end = index + len(self.__placeholder_prefix)
241 linkText(data[strartIndex:end])
242 strartIndex = end
243 else:
244 text = data[strartIndex:]
245 if isinstance(data, util.AtomicString):
246 # We don't want to loose the `AtomicString`
247 text = util.AtomicString(text)
248 linkText(text)
249 data = ""
250
251 return result
252
253 def __applyPattern(
254 self,
255 pattern: inlinepatterns.Pattern,
256 data: str,
257 patternIndex: int,
258 startIndex: int = 0
259 ) -> tuple[str, bool, int]:
260 """
261 Check if the line fits the pattern, create the necessary
262 elements, add it to `stashed_nodes`.
263
264 Arguments:
265 data: The text to be processed.
266 pattern: The pattern to be checked.
267 patternIndex: Index of current pattern.
268 startIndex: String index, from which we start searching.
269
270 Returns:
271 String with placeholders instead of `ElementTree` elements.
272
273 """
274 new_style = isinstance(pattern, inlinepatterns.InlineProcessor)
275
276 for exclude in pattern.ANCESTOR_EXCLUDES:
277 if exclude.lower() in self.ancestors:
278 return data, False, 0
279
280 if new_style:
281 match = None
282 # Since `handleMatch` may reject our first match,
283 # we iterate over the buffer looking for matches
284 # until we can't find any more.
285 for match in pattern.getCompiledRegExp().finditer(data, startIndex):
286 node, start, end = pattern.handleMatch(match, data)
287 if start is None or end is None:
288 startIndex += match.end(0)
289 match = None
290 continue
291 break
292 else: # pragma: no cover
293 match = pattern.getCompiledRegExp().match(data[startIndex:])
294 leftData = data[:startIndex]
295
296 if not match:
297 return data, False, 0
298
299 if not new_style: # pragma: no cover
300 node = pattern.handleMatch(match)
301 start = match.start(0)
302 end = match.end(0)
303
304 if node is None:
305 return data, True, end
306
307 if not isinstance(node, str):
308 if not isinstance(node.text, util.AtomicString):
309 # We need to process current node too
310 for child in [node] + list(node):
311 if not isString(node):
312 if child.text:
313 self.ancestors.append(child.tag.lower())
314 child.text = self.__handleInline(
315 child.text, patternIndex + 1
316 )
317 self.ancestors.pop()
318 if child.tail:
319 child.tail = self.__handleInline(
320 child.tail, patternIndex
321 )
322
323 placeholder = self.__stashNode(node, pattern.type())
324
325 if new_style:
326 # Return the index just past the inserted placeholder so the
327 # next call scans only the unprocessed tail. Scanning from 0
328 # after every match makes repeated inline patterns quadratic.
329 return "{}{}{}".format(data[:start],
330 placeholder, data[end:]), True, start + len(placeholder)
331 else: # pragma: no cover
332 return "{}{}{}{}".format(leftData,
333 match.group(1),
334 placeholder, match.groups()[-1]), True, (
335 len(leftData) + len(match.group(1)) + len(placeholder)
336 )
337
338 def __build_ancestors(self, parent: etree.Element | None, parents: list[str]) -> None:
339 """Build the ancestor list."""
340 ancestors = []
341 while parent is not None:
342 if parent is not None:
343 ancestors.append(parent.tag.lower())
344 parent = self.parent_map.get(parent)
345 ancestors.reverse()
346 parents.extend(ancestors)
347
348 def run(self, tree: etree.Element, ancestors: list[str] | None = None) -> etree.Element:
349 """Apply inline patterns to a parsed Markdown tree.
350
351 Iterate over `Element`, find elements with inline tag, apply inline
352 patterns and append newly created Elements to tree. To avoid further
353 processing of string with inline patterns, instead of normal string,
354 use subclass [`AtomicString`][markdown.util.AtomicString]:
355
356 node.text = markdown.util.AtomicString("This will not be processed.")
357
358 Arguments:
359 tree: `Element` object, representing Markdown tree.
360 ancestors: List of parent tag names that precede the tree node (if needed).
361
362 Returns:
363 An element tree object with applied inline patterns.
364
365 """
366 self.stashed_nodes: dict[str, etree.Element | str] = {}
367
368 # Ensure a valid parent list, but copy passed in lists
369 # to ensure we don't have the user accidentally change it on us.
370 tree_parents = [] if ancestors is None else ancestors[:]
371
372 self.parent_map = {c: p for p in tree.iter() for c in p}
373 stack = [(tree, tree_parents)]
374
375 while stack:
376 currElement, parents = stack.pop(0)
377
378 self.ancestors = parents
379 self.__build_ancestors(currElement, self.ancestors)
380
381 insertQueue = []
382 for child in currElement:
383 if child.text and not isinstance(
384 child.text, util.AtomicString
385 ):
386 self.ancestors.append(child.tag.lower())
387 text = child.text
388 child.text = None
389 lst = self.__processPlaceholders(
390 self.__handleInline(text), child
391 )
392 for item in lst:
393 self.parent_map[item[0]] = child
394 stack += lst
395 insertQueue.append((child, lst))
396 self.ancestors.pop()
397 if child.tail:
398 tail = self.__handleInline(child.tail)
399 dumby = etree.Element('d')
400 child.tail = None
401 tailResult = self.__processPlaceholders(tail, dumby, False)
402 if dumby.tail:
403 child.tail = dumby.tail
404 pos = list(currElement).index(child) + 1
405 tailResult.reverse()
406 for newChild in tailResult:
407 self.parent_map[newChild[0]] = currElement
408 currElement.insert(pos, newChild[0])
409 if len(child):
410 self.parent_map[child] = currElement
411 stack.append((child, self.ancestors[:]))
412
413 for element, lst in insertQueue:
414 for i, obj in enumerate(lst):
415 newChild = obj[0]
416 element.insert(i, newChild)
417 return tree
418
419
420class PrettifyTreeprocessor(Treeprocessor):
421 """ Add line breaks to the html document. """
422
423 def _prettifyETree(self, elem: etree.Element) -> None:
424 """ Recursively add line breaks to `ElementTree` children. """
425
426 i = "\n"
427 if self.md.is_block_level(elem.tag) and elem.tag not in ['code', 'pre']:
428 if (not elem.text or not elem.text.strip()) \
429 and len(elem) and self.md.is_block_level(elem[0].tag):
430 elem.text = i
431 for e in elem:
432 if self.md.is_block_level(e.tag):
433 self._prettifyETree(e)
434 if not elem.tail or not elem.tail.strip():
435 elem.tail = i
436
437 def run(self, root: etree.Element) -> None:
438 """ Add line breaks to `Element` object and its children. """
439
440 self._prettifyETree(root)
441 # Do `<br />`'s separately as they are often in the middle of
442 # inline content and missed by `_prettifyETree`.
443 brs = root.iter('br')
444 for br in brs:
445 if not br.tail or not br.tail.strip():
446 br.tail = '\n'
447 else:
448 br.tail = '\n%s' % br.tail
449 # Clean up extra empty lines at end of code blocks.
450 pres = root.iter('pre')
451 for pre in pres:
452 if len(pre) and pre[0].tag == 'code':
453 code = pre[0]
454 # Only prettify code containing text only
455 if not len(code) and code.text is not None:
456 code.text = util.AtomicString(code.text.rstrip() + '\n')
457
458
459class UnescapeTreeprocessor(Treeprocessor):
460 """ Restore escaped chars """
461
462 RE = re.compile(r'{}(\d+){}'.format(util.STX, util.ETX))
463
464 def _unescape(self, m: re.Match[str]) -> str:
465 return chr(int(m.group(1)))
466
467 def unescape(self, text: str) -> str:
468 return self.RE.sub(self._unescape, text)
469
470 def run(self, root: etree.Element) -> None:
471 """ Loop over all elements and unescape all text. """
472 for elem in root.iter():
473 # Unescape text content
474 if elem.text and not elem.tag == 'code':
475 elem.text = self.unescape(elem.text)
476 # Unescape tail content
477 if elem.tail:
478 elem.tail = self.unescape(elem.tail)
479 # Unescape attribute values
480 for key, value in elem.items():
481 elem.set(key, self.unescape(value))