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