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"""
21In version 3.0, a new, more flexible inline processor was added, [`markdown.inlinepatterns.InlineProcessor`][]. The
22original inline patterns, which inherit from [`markdown.inlinepatterns.Pattern`][] or one of its children are still
23supported, though users are encouraged to migrate.
24
25The new `InlineProcessor` provides two major enhancements to `Patterns`:
26
271. Inline Processors no longer need to match the entire block, so regular expressions no longer need to start with
28 `r'^(.*?)'` and end with `r'(.*?)%'`. This runs faster. The returned [`Match`][re.Match] object will only contain
29 what is explicitly matched in the pattern, and extension pattern groups now start with `m.group(1)`.
30
312. The `handleMatch` method now takes an additional input called `data`, which is the entire block under analysis,
32 not just what is matched with the specified pattern. The method now returns the element *and* the indexes relative
33 to `data` that the return element is replacing (usually `m.start(0)` and `m.end(0)`). If the boundaries are
34 returned as `None`, it is assumed that the match did not take place, and nothing will be altered in `data`.
35
36 This allows handling of more complex constructs than regular expressions can handle, e.g., matching nested
37 brackets, and explicit control of the span "consumed" by the processor.
38
39"""
40
41from __future__ import annotations
42
43from . import util
44from typing import TYPE_CHECKING, Any, Collection, NamedTuple, cast
45from collections import deque
46import re
47import xml.etree.ElementTree as etree
48from html import entities
49
50if TYPE_CHECKING: # pragma: no cover
51 from markdown import Markdown
52
53
54def build_inlinepatterns(md: Markdown, **kwargs: Any) -> util.Registry[InlineProcessor]:
55 """
56 Build the default set of inline patterns for Markdown.
57
58 The order in which processors and/or patterns are applied is very important - e.g. if we first replace
59 `http://.../` links with `<a>` tags and _then_ try to replace inline HTML, we would end up with a mess. So, we
60 apply the expressions in the following order:
61
62 * backticks and escaped characters have to be handled before everything else so that we can preempt any markdown
63 patterns by escaping them;
64
65 * then we handle the various types of links (auto-links must be handled before inline HTML);
66
67 * then we handle inline HTML. At this point we will simply replace all inline HTML strings with a placeholder
68 and add the actual HTML to a stash;
69
70 * finally we apply strong, emphasis, etc.
71
72 """
73 inlinePatterns = util.Registry()
74 inlinePatterns.register(BacktickInlineProcessor(BACKTICK_RE), 'backtick', 190)
75 inlinePatterns.register(EscapeInlineProcessor(ESCAPE_RE, md), 'escape', 180)
76 inlinePatterns.register(ReferenceInlineProcessor(REFERENCE_RE, md), 'reference', 170)
77 inlinePatterns.register(LinkInlineProcessor(LINK_RE, md), 'link', 160)
78 inlinePatterns.register(ImageInlineProcessor(IMAGE_LINK_RE, md), 'image_link', 150)
79 inlinePatterns.register(
80 ImageReferenceInlineProcessor(IMAGE_REFERENCE_RE, md), 'image_reference', 140
81 )
82 inlinePatterns.register(
83 ShortReferenceInlineProcessor(REFERENCE_RE, md), 'short_reference', 130
84 )
85 inlinePatterns.register(
86 ShortImageReferenceInlineProcessor(IMAGE_REFERENCE_RE, md), 'short_image_ref', 125
87 )
88 inlinePatterns.register(AutolinkInlineProcessor(AUTOLINK_RE, md), 'autolink', 120)
89 inlinePatterns.register(AutomailInlineProcessor(AUTOMAIL_RE, md), 'automail', 110)
90 inlinePatterns.register(SubstituteTagInlineProcessor(LINE_BREAK_RE, 'br'), 'linebreak', 100)
91 inlinePatterns.register(HtmlInlineProcessor(HTML_RE, md), 'html', 90)
92 inlinePatterns.register(HtmlInlineProcessor(ENTITY_RE, md), 'entity', 80)
93 inlinePatterns.register(DelimiterProcessor('*', 'strong,em', md), 'em_strong', 60)
94 md.delimiters.add('_', 'strong,em', smart=True)
95 return inlinePatterns
96
97
98# The actual regular expressions for patterns
99# -----------------------------------------------------------------------------
100
101NOIMG = r'(?<!\!)'
102""" Match not an image. Partial regular expression which matches if not preceded by `!`. """
103
104BACKTICK_RE = r'(?:(?<!\\)((?:\\{2})+)(?=`+)|(?<!\\)`)'
105""" Match backtick quoted string (`` `e=f()` `` or ``` ``e=f("`")`` ```). """
106
107ESCAPE_RE = r'\\(.)'
108""" Match a backslash escaped character (`\\<` or `\\*`). """
109
110LINK_RE = NOIMG + r'\['
111""" Match start of in-line link (`[text](url)` or `[text](<url>)` or `[text](url "title")`). """
112
113IMAGE_LINK_RE = r'\!\['
114""" Match start of in-line image link (`` or ``). """
115
116REFERENCE_RE = LINK_RE
117""" Match start of reference link (`[Label][3]`). """
118
119IMAGE_REFERENCE_RE = IMAGE_LINK_RE
120""" Match start of image reference (`![alt text][2]`). """
121
122AUTOLINK_RE = r'<((?:[Ff]|[Hh][Tt])[Tt][Pp][Ss]?://[^<>]*)>'
123""" Match an automatic link (`<http://www.example.com>`). """
124
125AUTOMAIL_RE = r'<([^<> !]+@[^@<> ]+)>'
126""" Match an automatic email link (`<me@example.com>`). """
127
128HTML_RE = (
129 # Tag
130 r'''(<(\/?+[a-zA-Z][^\s"'<>@]*+(?:\s+[^\s"'=<>]++(?:\s*+=\s*+(?:"[^"]*+"|'[^']*+'|[^\s"'=<>]++))?+)*+\s*+/?|'''
131 # Comment
132 r'!--(?:(?!<!--|-->).)*--|'
133 # Processing instruction
134 r'[?](?:(?!<[?]|[?]>).)*[?]|'
135 # `CDATA`
136 r'!\[CDATA\[(?:(?!<!\[CDATA\[|\]\]>).)*\]\]'
137 ')>)'
138)
139""" Match an HTML tag (`<...>`). """
140
141ENTITY_RE = r'(&(?:\#[0-9]+|\#x[0-9a-fA-F]+|[a-zA-Z0-9]+);)'
142""" Match an HTML entity (`&` (decimal) or `&` (hex) or `&` (named)). """
143
144LINE_BREAK_RE = r' \n'
145""" Match two spaces at end of line. """
146
147
148def dequote(string: str) -> str:
149 """Remove quotes from around a string."""
150 if ((string.startswith('"') and string.endswith('"')) or
151 (string.startswith("'") and string.endswith("'"))):
152 return string[1:-1]
153 else:
154 return string
155
156
157class EmStrongItem(NamedTuple):
158 """Emphasis/strong pattern item."""
159 pattern: re.Pattern[str]
160 builder: str
161 tags: str
162
163
164# The pattern classes
165# -----------------------------------------------------------------------------
166
167
168class Pattern: # pragma: no cover
169 """
170 Base class that inline patterns subclass.
171
172 Inline patterns are handled by means of `Pattern` subclasses, one per regular expression.
173 Each pattern object uses a single regular expression and must support the following methods:
174 [`getCompiledRegExp`][markdown.inlinepatterns.Pattern.getCompiledRegExp] and
175 [`handleMatch`][markdown.inlinepatterns.Pattern.handleMatch].
176
177 All the regular expressions used by `Pattern` subclasses must capture the whole block. For this
178 reason, they all start with `^(.*)` and end with `(.*)!`. When passing a regular expression on
179 class initialization, the `^(.*)` and `(.*)!` are added automatically and the regular expression
180 is pre-compiled.
181
182 It is strongly suggested that the newer style [`markdown.inlinepatterns.InlineProcessor`][] that
183 use a more efficient and flexible search approach be used instead. However, the older style
184 `Pattern` remains for backward compatibility with many existing third-party extensions.
185
186 """
187
188 ANCESTOR_EXCLUDES: Collection[str] = tuple()
189 """
190 A collection of elements which are undesirable ancestors. The processor will be skipped if it
191 would cause the content to be a descendant of one of the listed tag names.
192 """
193
194 compiled_re: re.Pattern[str]
195 md: Markdown | None
196
197 def __init__(self, pattern: str, md: Markdown | None = None):
198 """
199 Create an instant of an inline pattern.
200
201 Arguments:
202 pattern: A regular expression that matches a pattern.
203 md: An optional pointer to the instance of `markdown.Markdown` and is available as
204 `self.md` on the class instance.
205
206
207 """
208 self.pattern = pattern
209 self.compiled_re = re.compile(r"^(.*?)%s(.*)$" % pattern,
210 re.DOTALL | re.UNICODE)
211
212 self.md = md
213
214 def getCompiledRegExp(self) -> re.Pattern:
215 """ Return a compiled regular expression. """
216 return self.compiled_re
217
218 def handleMatch(self, m: re.Match[str]) -> etree.Element | str:
219 """Return a ElementTree element from the given match.
220
221 Subclasses should override this method.
222
223 Arguments:
224 m: A match object containing a match of the pattern.
225
226 Returns: An ElementTree Element object.
227
228 """
229 pass # pragma: no cover
230
231 def type(self) -> str:
232 """ Return class name, to define pattern type """
233 return self.__class__.__name__
234
235 def unescape(self, text: str) -> str:
236 """ Return unescaped text given text with an inline placeholder. """
237 try:
238 stash = self.md.treeprocessors['inline'].stashed_nodes
239 except KeyError: # pragma: no cover
240 return text
241
242 def get_stash(m):
243 id = m.group(1)
244 if id in stash:
245 value = stash.get(id)
246 if isinstance(value, str):
247 return value
248 else:
249 # An `etree` Element - return text content only
250 return ''.join(value.itertext())
251 return util.INLINE_PLACEHOLDER_RE.sub(get_stash, text)
252
253
254class InlineProcessor(Pattern):
255 """
256 Base class that inline processors subclass.
257
258 This is the newer style inline processor that uses a more
259 efficient and flexible search approach.
260
261 """
262
263 def __init__(self, pattern: str, md: Markdown | None = None):
264 """
265 Create an instant of an inline processor.
266
267 Arguments:
268 pattern: A regular expression that matches a pattern.
269 md: An optional pointer to the instance of `markdown.Markdown` and is available as
270 `self.md` on the class instance.
271
272 """
273 self.pattern = pattern
274 self.compiled_re = re.compile(pattern, re.DOTALL | re.UNICODE)
275
276 # API for Markdown to pass `safe_mode` into instance
277 self.safe_mode = False
278 self.md = md
279
280 def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | str | None, int | None, int | None]:
281 """Return a ElementTree element from the given match and the
282 start and end index of the matched text.
283
284 If `start` and/or `end` are returned as `None`, it will be
285 assumed that the processor did not find a valid region of text.
286
287 Subclasses should override this method.
288
289 Arguments:
290 m: A re match object containing a match of the pattern.
291 data: The buffer currently under analysis.
292
293 Returns:
294 el: The ElementTree element, text or None.
295 start: The start of the region that has been matched or None.
296 end: The end of the region that has been matched or None.
297
298 """
299 pass # pragma: no cover
300
301
302class SimpleTextPattern(Pattern): # pragma: no cover
303 """ Return a simple text of `group(2)` of a Pattern. """
304 def handleMatch(self, m: re.Match[str]) -> str:
305 """ Return string content of `group(2)` of a matching pattern. """
306 return m.group(2)
307
308
309class SimpleTextInlineProcessor(InlineProcessor):
310 """ Return a simple text of `group(1)` of a Pattern. """
311 def handleMatch(self, m: re.Match[str], data: str) -> tuple[str, int, int]:
312 """ Return string content of `group(1)` of a matching pattern. """
313 return m.group(1), m.start(0), m.end(0)
314
315
316class EscapeInlineProcessor(InlineProcessor):
317 """ Return an escaped character. """
318
319 def handleMatch(self, m: re.Match[str], data: str) -> tuple[str | None, int, int]:
320 """
321 If the character matched by `group(1)` of a pattern is in [`ESCAPED_CHARS`][markdown.Markdown.ESCAPED_CHARS]
322 then return the integer representing the character's Unicode code point (as returned by [`ord`][]) wrapped
323 in [`util.STX`][markdown.util.STX] and [`util.ETX`][markdown.util.ETX].
324
325 If the matched character is not in [`ESCAPED_CHARS`][markdown.Markdown.ESCAPED_CHARS], then return `None`.
326 """
327
328 char = m.group(1)
329 if char in self.md.ESCAPED_CHARS:
330 return '{}{}{}'.format(util.STX, ord(char), util.ETX), m.start(0), m.end(0)
331 else:
332 return None, m.start(0), m.end(0)
333
334
335class SimpleTagPattern(Pattern): # pragma: no cover
336 """
337 Return element of type `tag` with a text attribute of `group(3)`
338 of a Pattern.
339
340 """
341 def __init__(self, pattern: str, tag: str):
342 """
343 Create an instant of an simple tag pattern.
344
345 Arguments:
346 pattern: A regular expression that matches a pattern.
347 tag: Tag of element.
348
349 """
350 Pattern.__init__(self, pattern)
351 self.tag = tag
352 """ The tag of the rendered element. """
353
354 def handleMatch(self, m: re.Match[str]) -> etree.Element:
355 """
356 Return [`Element`][xml.etree.ElementTree.Element] of type `tag` with the string in `group(3)` of a
357 matching pattern as the Element's text.
358 """
359 el = etree.Element(self.tag)
360 el.text = m.group(3)
361 return el
362
363
364class SimpleTagInlineProcessor(InlineProcessor):
365 """
366 Return element of type `tag` with a text attribute of `group(2)`
367 of a Pattern.
368
369 """
370 def __init__(self, pattern: str, tag: str):
371 """
372 Create an instant of an simple tag processor.
373
374 Arguments:
375 pattern: A regular expression that matches a pattern.
376 tag: Tag of element.
377
378 """
379 InlineProcessor.__init__(self, pattern)
380 self.tag = tag
381 """ The tag of the rendered element. """
382
383 def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element, int, int]: # pragma: no cover
384 """
385 Return [`Element`][xml.etree.ElementTree.Element] of type `tag` with the string in `group(2)` of a
386 matching pattern as the Element's text.
387 """
388 el = etree.Element(self.tag)
389 el.text = m.group(2)
390 return el, m.start(0), m.end(0)
391
392
393class SubstituteTagPattern(SimpleTagPattern): # pragma: no cover
394 """ Return an element of type `tag` with no children. """
395 def handleMatch(self, m: re.Match[str]) -> etree.Element:
396 """ Return empty [`Element`][xml.etree.ElementTree.Element] of type `tag`. """
397 return etree.Element(self.tag)
398
399
400class SubstituteTagInlineProcessor(SimpleTagInlineProcessor):
401 """ Return an element of type `tag` with no children. """
402 def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element, int, int]:
403 """ Return empty [`Element`][xml.etree.ElementTree.Element] of type `tag`. """
404 return etree.Element(self.tag), m.start(0), m.end(0)
405
406
407class BacktickInlineProcessor(InlineProcessor):
408 """ Return a `<code>` element containing the escaped matching text. """
409
410 def __init__(self, pattern: str):
411 InlineProcessor.__init__(self, pattern)
412 self.ESCAPED_BSLASH = '{}{}{}'.format(util.STX, ord('\\'), util.ETX)
413 self.tag = 'code'
414 """ The tag of the rendered element. """
415
416 def find_code_spans(self, start: int, text: str) -> tuple[int, int] | None:
417 """Find code spans."""
418
419 last = len(text)
420
421 # Get the maximum starting ticks
422 max_ticks = 0
423 while start < last and text[start] == '`':
424 max_ticks += 1
425 start += 1
426
427 if not max_ticks: # pragma: no cover
428 # This is not ever expected to happen.
429 return None
430
431 longest_span = 0
432 end = 0
433
434 # Find an ending span of backticks that matches our opening
435 i = start
436 while i < last:
437 span_length = 0
438 while i < last and text[i] == '`':
439 span_length += 1
440 i += 1
441 if not span_length:
442 i += 1
443 continue
444
445 # Did we find the end?
446 if max_ticks == span_length:
447 return start, i - span_length
448
449 # Track the longest span of backticks we find as a fallback.
450 if span_length > longest_span:
451 longest_span = span_length
452 end = i
453
454 # Since we didn't find an exact matching start and end,
455 # adjust start to match the largest end we could calculate.
456 if longest_span:
457 return start - (max_ticks - longest_span), end - longest_span
458
459 # We could not find a suitable pairing
460 return None
461
462 def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | str | None, int | None, int | None]:
463 """
464 If the match contains `group(3)` of a pattern, then return a `code`
465 [`Element`][xml.etree.ElementTree.Element] which contains HTML escaped text (with
466 [`code_escape`][markdown.util.code_escape]) as an [`AtomicString`][markdown.util.AtomicString].
467
468 If the match contains `group(1)` then return the text of `group(1)` as backslash escaped.
469
470 """
471 if m.group(1):
472 return m.group(1).replace('\\\\', self.ESCAPED_BSLASH), m.start(0), m.end(0)
473
474 begin = m.start(0)
475 result = self.find_code_spans(begin, data)
476 if result is not None:
477 start, end = result
478 el = etree.Element(self.tag)
479 el.text = util.AtomicString(util.code_escape(data[start:end].strip()))
480 return el, begin, result[1] + (start - begin)
481 return None, None, None
482
483
484class DoubleTagPattern(SimpleTagPattern): # pragma: no cover
485 """Return a ElementTree element nested in tag2 nested in tag1.
486
487 Useful for strong emphasis etc.
488
489 """
490 def handleMatch(self, m: re.Match[str]) -> etree.Element:
491 """
492 Return [`Element`][xml.etree.ElementTree.Element] in following format:
493 `<tag1><tag2>group(3)</tag2>group(4)</tag2>` where `group(4)` is optional.
494
495 """
496 tag1, tag2 = self.tag.split(",")
497 el1 = etree.Element(tag1)
498 el2 = etree.SubElement(el1, tag2)
499 el2.text = m.group(3)
500 if len(m.groups()) == 5:
501 el2.tail = m.group(4)
502 return el1
503
504
505class DoubleTagInlineProcessor(SimpleTagInlineProcessor):
506 """Return a ElementTree element nested in tag2 nested in tag1.
507
508 Useful for strong emphasis etc.
509
510 """
511 def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element, int, int]: # pragma: no cover
512 """
513 Return [`Element`][xml.etree.ElementTree.Element] in following format:
514 `<tag1><tag2>group(2)</tag2>group(3)</tag2>` where `group(3)` is optional.
515
516 """
517 tag1, tag2 = self.tag.split(",")
518 el1 = etree.Element(tag1)
519 el2 = etree.SubElement(el1, tag2)
520 el2.text = m.group(2)
521 if len(m.groups()) == 3:
522 el2.tail = m.group(3)
523 return el1, m.start(0), m.end(0)
524
525
526class HtmlInlineProcessor(InlineProcessor):
527 """ Store raw inline html and return a placeholder. """
528 def handleMatch(self, m: re.Match[str], data: str) -> tuple[str, int, int]:
529 """ Store the text of `group(1)` of a pattern and return a placeholder string. """
530 rawhtml = self.backslash_unescape(self.unescape(m.group(1)))
531 place_holder = self.md.htmlStash.store(rawhtml)
532 return place_holder, m.start(0), m.end(0)
533
534 def unescape(self, text: str) -> str:
535 """ Return unescaped text given text with an inline placeholder. """
536 try:
537 stash = self.md.treeprocessors['inline'].stashed_nodes
538 except KeyError: # pragma: no cover
539 return text
540
541 def get_stash(m: re.Match[str]) -> str:
542 id = m.group(1)
543 value = stash.get(id)
544 if value is not None:
545 if isinstance(value, etree.Element):
546 # Ensure we don't have a placeholder inside a placeholder
547 return self.unescape(self.md.serializer(value))
548 else:
549 return r'\%s' % value
550
551 return util.INLINE_PLACEHOLDER_RE.sub(get_stash, text)
552
553 def backslash_unescape(self, text: str) -> str:
554 """ Return text with backslash escapes undone (backslashes are restored). """
555 try:
556 RE = self.md.treeprocessors['unescape'].RE
557 except KeyError: # pragma: no cover
558 return text
559
560 def _unescape(m: re.Match[str]) -> str:
561 return chr(int(m.group(1)))
562
563 return RE.sub(_unescape, text)
564
565
566class Delimiter:
567 """Delimiter."""
568
569 def __init__(self, token: str, tags: str, smart: bool, double: bool):
570 """Initialize."""
571
572 self.stack: deque[tuple[int, int, bool, int]] = deque()
573 temp = tags.split(',')
574 self.tag_count = len(temp)
575 self.tags: tuple[str, str] = (temp[0], temp[1]) if self.tag_count == 2 else (temp[0], temp[0])
576 self.double = len(temp) != 2 and double
577 self.single = len(temp) != 2 and not double
578 self.smart = smart
579 self._build_patterns(token)
580
581 def _build_patterns(self, token: str) -> str:
582 """Build regular expression patterns."""
583
584 # Build up patterns
585 self.token = token
586 etoken = re.escape(token)
587 # Avoid at start and end
588 xstart = fr'(?:(?<=_)|(?<![\w{etoken}]))' if token != '_' else fr'(?<![\w{etoken}])'
589 xend = fr'(?:(?=_)|(?![\w{etoken}]))' if token != '_' else fr'(?![\w{etoken}])'
590 self.max_size = 2
591 if self.tag_count != 2 and not self.double:
592 self.max_size = 1
593
594 # Patterns for "smart" cases.
595 if self.smart:
596 self.boundary = re.compile(
597 fr'''(?x)
598 (?P<ambiguous>(?<!^)(?<![\s{etoken}]){xstart}{etoken}{{1,}}{xend}(?![\s{etoken}])(?!$))|
599 (?P<end>(?<!^)(?<![\s{etoken}]){etoken}{{1,}}{xend})|
600 (?P<start>{xstart}{etoken}{{1,}}(?![\s{etoken}])(?!$))
601 ''',
602 flags=re.UNICODE
603 )
604 # Patterns for "dumb" cases.
605 else:
606 self.boundary = re.compile(
607 fr'''(?x)
608 (?P<ambiguous>(?<!^)(?<![\s{etoken}]){etoken}{{1,}}(?![\s{etoken}])(?!$))|
609 (?P<end>(?<!^)(?<![\s{etoken}]){etoken}{{1,}})|
610 (?P<start>{etoken}{{1,}}(?![\s{etoken}])(?!$))
611 ''',
612 flags=re.UNICODE
613 )
614
615 self.bad = re.compile(fr'{etoken}+')
616
617 return fr'{etoken}'
618
619 def reset(self) -> None:
620 """Reset."""
621
622 # Cache info
623 self.stack.clear()
624
625
626class DelimiterProcessor(InlineProcessor):
627 """Processor for handling complex nested patterns such as strong and em matches."""
628
629 SPACE = re.compile(r'\s')
630
631 def __init__(
632 self,
633 token: str,
634 tags: str,
635 md: Markdown,
636 smart: bool = False,
637 double: bool = False
638 ) -> None:
639 """Initialize."""
640
641 md.delimiters = self
642 self.regions: list[tuple[int, int, int, int, tuple[str, str], int]] = []
643 self.stack: list[tuple[int, int, bool, int]] = []
644 self.tokens: list[str] = []
645 self.delimiters: dict[str, Delimiter] = {}
646 self.cache_index = 0
647 self.cache_pos = 0
648 self.md = md
649 # API for Markdown to pass `safe_mode` into instance
650 self.safe_mode = False
651 self.add(token, tags, smart, double)
652
653 def add(
654 self,
655 token: str,
656 tags: str,
657 smart: bool = False,
658 double: bool = False
659 ) -> None:
660 """Add a delimiter."""
661
662 if token not in self.tokens:
663 self.tokens.append(token)
664 self.delimiters[token] = Delimiter(token, tags, smart, double)
665 self.pattern = '|'.join([re.escape(t) for t in self.tokens])
666 self.compiled_re = re.compile(self.pattern, re.DOTALL | re.UNICODE)
667
668 def remove(self, token: str) -> None:
669 """Remove a token."""
670
671 try:
672 i = self.tokens.index(token)
673 del self.tokens[i]
674 del self.delimiters[token]
675 except Exception:
676 pass
677 self.pattern = '|'.join([re.escape(t) for t in self.tokens])
678 self.compiled_re = re.compile(self.pattern, re.DOTALL | re.UNICODE) if self.pattern else re.compile(r'(?!)')
679
680 def reset(self) -> None:
681 """Reset."""
682
683 # Cache info
684 for v in self.delimiters.values():
685 v.reset()
686 self.regions.clear()
687 self.stack.clear()
688 self.cache_index = 0
689 self.cache_pos = 0
690
691 def _build_element(
692 self,
693 data: str,
694 start: int = 0,
695 offset: int = 0
696 ) -> tuple[etree.Element, int]:
697 """Element builder."""
698
699 regions = self.regions
700 el: etree.Element | None = None
701 last: Any = None
702 previous: Any = None
703
704 outer: list[etree.Element] = []
705 outer_r: list[tuple[int, int, int, int, tuple[str, str], int]] = []
706
707 # Iterate regions creating the elements they represent
708 end = len(regions)
709 idx = 0
710 for idx, i in enumerate(range(start, end), 1):
711 r = regions[i]
712 # Not contained within region
713 if idx and r[0] >= regions[start][3]:
714 idx -= 1
715 break
716
717 # Get the appropriate element(s)
718 if r[-1] == 2:
719 el1 = etree.Element(r[4][0])
720 else:
721 el1 = etree.Element(r[4][1])
722
723 # Populate the elements with their text
724 if idx > 1:
725 if last.text is None:
726 if previous[2] < r[0]:
727 last.text = data[previous[1]+offset:previous[2]+offset]
728 else:
729 last.text = data[previous[1]+offset:r[0]+offset]
730 if last is not outer[-1] and last.tail is None:
731 if r[0] < outer_r[-1][3]:
732 last.tail = data[previous[3]+offset:r[0]+offset]
733 else:
734 last.tail = data[previous[3]+offset:outer_r[-1][2]+offset]
735 outer[-1].tail = data[outer_r[-1][3]+offset:r[0]+offset]
736
737 # First element
738 if el is None:
739 el = el1
740 last = el
741 outer.append(el)
742 outer_r.append(r)
743
744 # Subsequent elements
745 else:
746 # Is the current outer element no longer wrapping this one?
747 while len(outer_r) > 1 and r[3] > outer_r[-1][3]:
748 outer.pop()
749 outer_r.pop()
750
751 # Non-nested
752 else:
753 outer[-1].append(el1)
754
755 # Is this element wrapping the next?
756 if i + 1 < end:
757 if r[3] > regions[i + 1][3]:
758 outer.append(el1)
759 outer_r.append(r)
760
761 # Track the last element we parsed.
762 last = el1
763
764 # Track the previous region.
765 previous = r
766
767 # Populate remaining elements with their text
768 while outer:
769 if last.text is None:
770 last.text = data[previous[1]+offset:previous[2]+offset]
771 if last.tail is None and last is not outer[-1]:
772 last.tail = data[previous[3]+offset:outer_r[-1][2]+offset]
773 last = outer.pop()
774 previous = outer_r.pop()
775
776 return cast('etree.Element', el), idx
777
778 def increment_next_position(self, start: int, count: int) -> None:
779 """
780 Increment cache position to the next location that we can initiate an insertion.
781
782 Cache position should be the first match after our current replacement.
783 This gives us an anchor to calculate the new offset after insertion.
784 """
785
786 # Determine next offset
787 self.cache_index += count
788 if self.cache_index < len(self.regions):
789 self.cache_pos = self.regions[self.cache_index][0]
790 while self.stack:
791 entry = self.stack.pop(0)
792 if start < entry[0] <= self.cache_pos:
793 self.cache_pos = entry[0]
794 break
795
796 # Nothing left to process
797 else:
798 self.reset()
799
800 def get_cached_result(self, pos: int, data: str) -> tuple[etree.Element, int, int]:
801 """Get a cached result."""
802
803 # Process the next region(s) in the cache
804 regions = self.regions
805 offset = pos - self.cache_pos if pos != self.cache_pos else pos - regions[self.cache_index][0]
806 start, end = regions[self.cache_index][0], regions[self.cache_index][3]
807 el, count = self._build_element(data, self.cache_index, offset)
808 self.increment_next_position(start, count)
809 return el, start + offset, end + offset
810
811 def get_match(self, data: str, start: int) -> re.Match[str] | None:
812 """Get match."""
813
814 for d in self.delimiters.values():
815 m = d.boundary.match(data, start)
816 if m is not None:
817 return m
818 return None
819
820 def search(self, data: str, start: int) -> re.Match[str] | None:
821 """Search."""
822
823 for i in range(start, len(data)):
824 if data[i] in self.delimiters:
825 d = self.delimiters[data[i]]
826 m = d.boundary.match(data, i)
827 if m is not None:
828 if not d.stack and m.lastgroup[0] == 'e': # type: ignore[index]
829 continue
830 return m
831 return None
832
833 def add_region(self, delim: Delimiter, a: int, b: int, c: int, d: int, size: int) -> None:
834 """Add region."""
835
836 self.regions.append((a, b, c, d, delim.tags, size))
837 for delim in self.delimiters.values():
838 while delim.stack:
839 p = delim.stack[-1][0]
840 if max(p, a) <= min(p, d - 1):
841 delim.stack.pop()
842 continue
843 break
844
845 def handleMatch( # type: ignore[override]
846 self,
847 m: re.Match[str],
848 data: str
849 ) -> tuple[etree.Element | None, int | None, int | None]:
850 """Parse delimiter pattern."""
851
852 # Do we have entries we haven't returned yet?
853 if self.regions:
854 return self.get_cached_result(m.start(0), data)
855
856 # If token is not an opening, quit
857 m2 = self.get_match(data, m.start(0))
858 if m2 is None or m2.lastgroup[0] == 'e': # type: ignore[index]
859 if m2 is None:
860 delim = self.delimiters[m.group(0)]
861 m2 = delim.bad.match(data, m.start(0))
862 # Advance past the full length of the delimiter found
863 return None, m2.start(0), m2.end(0)
864
865 # Get the stack and regions
866 token = data[m.start(0)]
867 delim = self.delimiters[token]
868 stack = delim.stack
869
870 start = m2.start(0)
871 end = m2.end(0)
872 length = end - start
873
874 # Double needs at least a size of 2
875 if delim.double and length < 2:
876 return None, m.start(0), m.end(0)
877
878 is_ambiguous = m2.lastgroup[0] != 's' # type: ignore[index]
879 stack.append((start, start + length, is_ambiguous, length))
880
881 # Pair tokens until the stack is empty or we can no longer find tokens.
882 while any(d.stack for d in self.delimiters.values()):
883 m2 = self.search(data, end)
884 if m2 is None:
885 break
886
887 token = data[m2.start(0)]
888 delim = self.delimiters[token]
889 stack = delim.stack
890
891 start = m2.start(0)
892 end = m2.end(0)
893
894 # Get current and last delimiter size
895 current = len(m2.group(0))
896
897 # Some delimiters may be ambiguous and look like both a start or an end
898 is_start = m2.lastgroup[0] != 'e' # type: ignore[index]
899 is_end = not is_start or m2.lastgroup[0] != 's' # type: ignore[index]
900 is_ambiguous = is_start and is_end
901
902 last = stack[-1][-1] if stack else 0
903
904 # Find closing tokens
905 # Looking for:
906 # - `*em*`
907 # - `**strong**`
908 # - `***strong,em***`
909 # - `*em**`
910 # - `*em***`
911 # - `**strong***`
912 #
913 # Avoid ambiguous tokens that could be a start or an end.
914 # Consume starts until the end token is fully consumed.
915 # If we don't consume the entire end, see if next rule consumes it.
916 if stack and is_end and ((not is_ambiguous and current > last) or current == last or current >= 3):
917 is_start = False
918
919 # Consume previous points until the delimiter is consumed
920 original = current
921 while stack and current and last <= current:
922 delimiter = stack.pop()
923
924 # Build up region for pair and adjust accounting.
925 size = min(delimiter[-1], 1 if delim.tag_count == 2 and delimiter[-1] == 3 else delim.max_size)
926 self.add_region(delim, delimiter[1] - size, delimiter[1], start, start + size, size)
927 start += size
928 current -= size
929 new = 0
930 if size < delimiter[-1] and (not delim.double or (delimiter[-1] - size) != 1):
931 new = delimiter[-1] - size
932 stack.append((delimiter[0], delimiter[1] - size, delimiter[2], new))
933
934 if not stack:
935 if any(d.stack for d in self.delimiters.values() if d is not delim):
936 delim.reset()
937 continue
938 is_end = False
939 break
940
941 last = stack[-1][-1]
942
943 # Should remainder be treated as a new start?
944 if original >= 3 and current and is_ambiguous:
945 delim.stack.append((m2.start(0) + (original - current), end, False, current))
946 is_end = False
947
948 # Do we still have more to consume?
949 else:
950 is_end = current and stack and last > current
951
952 # Looking for:
953 # - `***em*`
954 # - `***strong**`
955 # - `**em*`
956 if stack and is_end and (last >= 3 or not is_ambiguous) and last > current:
957 delimiter = stack.pop()
958
959 # Don't pair with an ambiguous opening
960 while stack and delimiter[-1] != 3 and delimiter[2]:
961 delimiter = stack.pop()
962 last = delimiter[-1]
963
964 if delimiter[2] and delimiter[-1] != 3:
965 if any(d.stack for d in self.delimiters.values() if d is not delim):
966 delim.reset()
967 continue
968 break
969
970 # Create new regions
971 is_start = False
972 ds, de = delimiter[:2]
973 while current and (not delim.double or current != 1):
974 size = min(current, 1 if delim.tag_count == 2 and current == 3 else delim.max_size)
975 new = last - size
976 self.add_region(delim, ds + new, de, start, start + size, size)
977 start += size
978 current -= size
979 last -= size
980 de -= size
981 if last and (not delim.double or last > 1):
982 stack.append((ds, de, False, last))
983
984 # Find opening tokens
985 # Looking for:
986 # - `*em ...*`
987 # - `**strong ...*`
988 # - `***em ...*`
989 if is_start and (not delim.double or current != 1):
990 stack.append((start, end, is_ambiguous, current))
991
992 # Combine the stacks and order them
993 for delim in self.delimiters.values():
994 self.stack.extend(delim.stack)
995 self.stack.sort(key=lambda x: x[0])
996
997 # Build the HTML elements
998 if self.regions:
999 # Regions may be out of order.
1000 self.regions.sort(key=lambda x: x[0])
1001 start, end = self.regions[0][0], self.regions[0][3]
1002 el, count = self._build_element(data)
1003 self.increment_next_position(start, count)
1004 return el, start, end
1005
1006 # We failed to pair any valid start/end delimiters, avoid the parsed range next pass.
1007 start = m.start(0)
1008 end = self.stack[-1][1] if self.stack else m.end(0)
1009 self.reset()
1010 return None, start, end
1011
1012
1013class LinkInlineProcessor(InlineProcessor):
1014 """ Return a link element from the given match. """
1015 RE_LINK = re.compile(r'''\(\s*(?:(<[^<>]*>)\s*(?:('[^']*'|"[^"]*")\s*)?\))?''', re.DOTALL | re.UNICODE)
1016 RE_TITLE_CLEAN = re.compile(r'\s')
1017
1018 def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | None, int | None, int | None]:
1019 """ Return an `a` [`Element`][xml.etree.ElementTree.Element] or `(None, None, None)`. """
1020 text, index, handled = self.getText(data, m.end(0))
1021
1022 if not handled:
1023 return None, None, None
1024
1025 href, title, index, handled = self.getLink(data, index)
1026 if not handled:
1027 return None, None, None
1028
1029 el = etree.Element("a")
1030 el.text = text
1031
1032 el.set("href", href)
1033
1034 if title is not None:
1035 el.set("title", title)
1036
1037 return el, m.start(0), index
1038
1039 def getLink(self, data: str, index: int) -> tuple[str, str | None, int, bool]:
1040 """Parse data between `()` of `[Text]()` allowing recursive `()`. """
1041
1042 href = ''
1043 title: str | None = None
1044 handled = False
1045
1046 m = self.RE_LINK.match(data, pos=index)
1047 if m and m.group(1):
1048 # Matches [Text](<link> "title")
1049 href = m.group(1)[1:-1].strip()
1050 if m.group(2):
1051 title = m.group(2)[1:-1]
1052 index = m.end(0)
1053 handled = True
1054 elif m:
1055 # Track bracket nesting and index in string
1056 bracket_count = 1
1057 backtrack_count = 1
1058 start_index = m.end()
1059 index = start_index
1060 last_bracket = -1
1061
1062 # Primary (first found) quote tracking.
1063 quote: str | None = None
1064 start_quote = -1
1065 exit_quote = -1
1066 ignore_matches = False
1067
1068 # Secondary (second found) quote tracking.
1069 alt_quote = None
1070 start_alt_quote = -1
1071 exit_alt_quote = -1
1072
1073 # Track last character
1074 last = ''
1075
1076 for pos in range(index, len(data)):
1077 c = data[pos]
1078 if c == '(':
1079 # Count nested (
1080 # Don't increment the bracket count if we are sure we're in a title.
1081 if not ignore_matches:
1082 bracket_count += 1
1083 elif backtrack_count > 0:
1084 backtrack_count -= 1
1085 elif c == ')':
1086 # Match nested ) to (
1087 # Don't decrement if we are sure we are in a title that is unclosed.
1088 if ((exit_quote != -1 and quote == last) or (exit_alt_quote != -1 and alt_quote == last)):
1089 bracket_count = 0
1090 elif not ignore_matches:
1091 bracket_count -= 1
1092 elif backtrack_count > 0:
1093 backtrack_count -= 1
1094 # We've found our backup end location if the title doesn't resolve.
1095 if backtrack_count == 0:
1096 last_bracket = index + 1
1097
1098 elif c in ("'", '"'):
1099 # Quote has started
1100 if not quote:
1101 # We'll assume we are now in a title.
1102 # Brackets are quoted, so no need to match them (except for the final one).
1103 ignore_matches = True
1104 backtrack_count = bracket_count
1105 bracket_count = 1
1106 start_quote = index + 1
1107 quote = c
1108 # Secondary quote (in case the first doesn't resolve): [text](link'"title")
1109 elif c != quote and not alt_quote:
1110 start_alt_quote = index + 1
1111 alt_quote = c
1112 # Update primary quote match
1113 elif c == quote:
1114 exit_quote = index + 1
1115 # Update secondary quote match
1116 elif alt_quote and c == alt_quote:
1117 exit_alt_quote = index + 1
1118
1119 index += 1
1120
1121 # Link is closed, so let's break out of the loop
1122 if bracket_count == 0:
1123 # Get the title if we closed a title string right before link closed
1124 if exit_quote >= 0 and quote == last:
1125 href = data[start_index:start_quote - 1]
1126 title = ''.join(data[start_quote:exit_quote - 1])
1127 elif exit_alt_quote >= 0 and alt_quote == last:
1128 href = data[start_index:start_alt_quote - 1]
1129 title = ''.join(data[start_alt_quote:exit_alt_quote - 1])
1130 else:
1131 href = data[start_index:index - 1]
1132 break
1133
1134 if c != ' ':
1135 last = c
1136
1137 # We have a scenario: `[test](link"notitle)`
1138 # When we enter a string, we stop tracking bracket resolution in the main counter,
1139 # but we do keep a backup counter up until we discover where we might resolve all brackets
1140 # if the title string fails to resolve.
1141 if bracket_count != 0 and backtrack_count == 0:
1142 href = data[start_index:last_bracket - 1]
1143 index = last_bracket
1144 bracket_count = 0
1145
1146 handled = bracket_count == 0
1147
1148 if title is not None:
1149 title = self.RE_TITLE_CLEAN.sub(' ', dequote(self.unescape(title.strip())))
1150
1151 href = self.unescape(href).strip()
1152
1153 return href, title, index, handled
1154
1155 def getText(self, data: str, index: int) -> tuple[str, int, bool]:
1156 """Parse the content between `[]` of the start of an image or link
1157 resolving nested square brackets.
1158
1159 """
1160 bracket_count = 1
1161 text = []
1162 for pos in range(index, len(data)):
1163 c = data[pos]
1164 if c == ']':
1165 bracket_count -= 1
1166 elif c == '[':
1167 bracket_count += 1
1168 index += 1
1169 if bracket_count == 0:
1170 break
1171 text.append(c)
1172 return ''.join(text), index, bracket_count == 0
1173
1174
1175class ImageInlineProcessor(LinkInlineProcessor):
1176 """ Return a `img` element from the given match. """
1177
1178 def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | None, int | None, int | None]:
1179 """ Return an `img` [`Element`][xml.etree.ElementTree.Element] or `(None, None, None)`. """
1180 text, index, handled = self.getText(data, m.end(0))
1181 if not handled:
1182 return None, None, None
1183
1184 src, title, index, handled = self.getLink(data, index)
1185 if not handled:
1186 return None, None, None
1187
1188 el = etree.Element("img")
1189
1190 el.set("src", src)
1191
1192 if title is not None:
1193 el.set("title", title)
1194
1195 el.set('alt', self.unescape(text))
1196 return el, m.start(0), index
1197
1198
1199class ReferenceInlineProcessor(LinkInlineProcessor):
1200 """ Match to a stored reference and return link element. """
1201 NEWLINE_CLEANUP_RE = re.compile(r'\s+', re.MULTILINE)
1202
1203 RE_LINK = re.compile(r'\s?\[([^\]]*)\]', re.DOTALL | re.UNICODE)
1204
1205 def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | None, int | None, int | None]:
1206 """
1207 Return [`Element`][xml.etree.ElementTree.Element] returned by `makeTag` method or `(None, None, None)`.
1208
1209 """
1210 text, index, handled = self.getText(data, m.end(0))
1211 if not handled:
1212 return None, None, None
1213
1214 id, end, handled = self.evalId(data, index, text)
1215 if not handled:
1216 return None, None, None
1217
1218 # Clean up line breaks in id
1219 id = self.NEWLINE_CLEANUP_RE.sub(' ', id)
1220 if id not in self.md.references: # ignore undefined refs
1221 return None, m.start(0), end
1222
1223 href, title = self.md.references[id]
1224
1225 return self.makeTag(href, title, text), m.start(0), end
1226
1227 def evalId(self, data: str, index: int, text: str) -> tuple[str | None, int, bool]:
1228 """
1229 Evaluate the id portion of `[ref][id]`.
1230
1231 If `[ref][]` use `[ref]`.
1232 """
1233 m = self.RE_LINK.match(data, pos=index)
1234 if not m:
1235 return None, index, False
1236 else:
1237 id = m.group(1).lower()
1238 end = m.end(0)
1239 if not id:
1240 id = text.lower()
1241 return id, end, True
1242
1243 def makeTag(self, href: str, title: str, text: str) -> etree.Element:
1244 """ Return an `a` [`Element`][xml.etree.ElementTree.Element]. """
1245 el = etree.Element('a')
1246
1247 el.set('href', href)
1248 if title:
1249 el.set('title', title)
1250
1251 el.text = text
1252 return el
1253
1254
1255class ShortReferenceInlineProcessor(ReferenceInlineProcessor):
1256 """Short form of reference: `[google]`. """
1257 def evalId(self, data: str, index: int, text: str) -> tuple[str, int, bool]:
1258 """Evaluate the id of `[ref]`. """
1259
1260 return text.lower(), index, True
1261
1262
1263class ImageReferenceInlineProcessor(ReferenceInlineProcessor):
1264 """ Match to a stored reference and return `img` element. """
1265 def makeTag(self, href: str, title: str, text: str) -> etree.Element:
1266 """ Return an `img` [`Element`][xml.etree.ElementTree.Element]. """
1267 el = etree.Element("img")
1268 el.set("src", href)
1269 if title:
1270 el.set("title", title)
1271 el.set("alt", self.unescape(text))
1272 return el
1273
1274
1275class ShortImageReferenceInlineProcessor(ImageReferenceInlineProcessor):
1276 """ Short form of image reference: `![ref]`. """
1277 def evalId(self, data: str, index: int, text: str) -> tuple[str, int, bool]:
1278 """Evaluate the id of `[ref]`. """
1279
1280 return text.lower(), index, True
1281
1282
1283class AutolinkInlineProcessor(InlineProcessor):
1284 """ Return a link Element given an auto-link (`<http://example/com>`). """
1285 def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element, int, int]:
1286 """ Return an `a` [`Element`][xml.etree.ElementTree.Element] of `group(1)`. """
1287 el = etree.Element("a")
1288 el.set('href', self.unescape(m.group(1)))
1289 el.text = util.AtomicString(m.group(1))
1290 return el, m.start(0), m.end(0)
1291
1292
1293class AutomailInlineProcessor(InlineProcessor):
1294 """
1295 Return a `mailto` link Element given an auto-mail link (`<foo@example.com>`).
1296 """
1297 def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element, int, int]:
1298 """ Return an [`Element`][xml.etree.ElementTree.Element] containing a `mailto` link of `group(1)`. """
1299 el = etree.Element('a')
1300 email = self.unescape(m.group(1))
1301 if email.startswith("mailto:"):
1302 email = email[len("mailto:"):]
1303
1304 def codepoint2name(code: int) -> str:
1305 """Return entity definition by code, or the code if not defined."""
1306 entity = entities.codepoint2name.get(code)
1307 if entity:
1308 return "{}{};".format(util.AMP_SUBSTITUTE, entity)
1309 else:
1310 return "%s#%d;" % (util.AMP_SUBSTITUTE, code)
1311
1312 letters = [codepoint2name(ord(letter)) for letter in email]
1313 el.text = util.AtomicString(''.join(letters))
1314
1315 mailto = "mailto:" + email
1316 mailto = "".join([util.AMP_SUBSTITUTE + '#%d;' %
1317 ord(letter) for letter in mailto])
1318 el.set('href', mailto)
1319 return el, m.start(0), m.end(0)