Skip to content

Commit 7975553

Browse files
Merge pull request #748 from SwastikTripathi/perf/unescape-special-chars
perf: speed up _unescape_special_chars for large documents
2 parents 38b1df9 + a13e621 commit 7975553

2 files changed

Lines changed: 26 additions & 11 deletions

File tree

‎CHANGES.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## python-markdown2 2.5.6 (not yet released)
44

55
- [pull #762] Speed up documents with many links by skipping the backslash-escape scan for text without a backslash
6+
- [pull #748] Speed up `_unescape_special_chars` by skipping text with no hashes and dropping the full-text comparison per pass (#635)
67
- [pull #758] Fix `footnotes` extra adding an empty footnotes block when no footnote is referenced.
78
- [pull #746] Preserve query strings and fragments in `markdown-file-links` conversions, including reference-link targets.
89
- [pull #739] Fix `header-ids` extra putting sanitized HTML into header ids in safe mode, which allowed attribute injection (#649)

‎lib/markdown2.py‎

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2425,19 +2425,33 @@ def _encode_email_address(self, addr: str) -> str:
24252425

24262426
def _unescape_special_chars(self, text: str) -> str:
24272427
# Swap back in all the special characters we've hidden.
2428-
hashmap = tuple(self._escape_table.items()) + tuple(self._code_table.items())
2428+
if 'md5-' not in text:
2429+
return text
2430+
# Build the hash -> original mapping directly, without intermediate tuples.
24292431
# html_blocks table is in format {hash: item} compared to usual {item: hash}
2430-
hashmap += tuple(tuple(reversed(i)) for i in self.html_blocks.items())
2431-
replacements = {}
2432-
for ch, hash in hashmap:
2432+
replacements: dict[str, str] = {}
2433+
for ch, hash in self._escape_table.items():
24332434
replacements.setdefault(hash, ch)
2434-
while True:
2435-
orig_text = text
2436-
# Scan once per nesting level instead of once for every stored hash.
2437-
text = re.sub(r'md5-[0-9a-f]{32}',
2438-
lambda match: replacements.get(match[0], match[0]), text)
2439-
if text == orig_text:
2440-
break
2435+
for ch, hash in self._code_table.items():
2436+
replacements.setdefault(hash, ch)
2437+
for hash, item in self.html_blocks.items():
2438+
replacements.setdefault(hash, item)
2439+
2440+
def _sub(match: re.Match) -> str:
2441+
nonlocal replaced
2442+
val = replacements.get(match[0])
2443+
if val is None:
2444+
return match[0]
2445+
replaced = True
2446+
return val
2447+
2448+
# Scan once per nesting level instead of once for every stored hash.
2449+
# Stop when a pass makes no replacement, rather than comparing the
2450+
# whole text against the previous pass.
2451+
replaced = True
2452+
while replaced:
2453+
replaced = False
2454+
text = re.sub(r'md5-[0-9a-f]{32}', _sub, text)
24412455
return text
24422456

24432457
def _outdent(self, text: str) -> str:

0 commit comments

Comments
 (0)