Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/markdown/inlinepatterns.py: 98%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

623 statements  

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 (`![alttxt](url)` or `![alttxt](<url>)`). """ 

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 (`&#38;` (decimal) or `&#x26;` (hex) or `&amp;` (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)