From fb5899ef2b7792b5f8bb93e18bb141222a24a609 Mon Sep 17 00:00:00 2001 From: Swastik Tripathi Date: Sun, 25 Oct 2026 08:46:26 +0530 Subject: [PATCH] perf: speed up _unescape_special_chars for large documents Two O(n) costs were paid per pass through the while loop: 1. The termination check compared the full text string (`text == orig_text`), which is O(n) even when nothing changed. 2. The replacements dict was built from three intermediate tuples before every call (escape_table + code_table + reversed html_blocks). Fix both: - Skip immediately if 'md5-' is not present in the text (the common case for plain paragraphs with no inline code, links, or escapes). - Build the replacements dict by iterating the source dicts directly, without allocating intermediate tuples. - Replace the string comparison with a boolean flag set inside the re.sub callback; the loop breaks as soon as a full pass produces no replacements. Fixes #635 Co-Authored-By: Claude Sonnet 4.6 --- lib/markdown2.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/lib/markdown2.py b/lib/markdown2.py index b3c29847..64fdeeee 100755 --- a/lib/markdown2.py +++ b/lib/markdown2.py @@ -2377,18 +2377,29 @@ def _encode_email_address(self, addr: str) -> str: def _unescape_special_chars(self, text: str) -> str: # Swap back in all the special characters we've hidden. - hashmap = tuple(self._escape_table.items()) + tuple(self._code_table.items()) - # html_blocks table is in format {hash: item} compared to usual {item: hash} - hashmap += tuple(tuple(reversed(i)) for i in self.html_blocks.items()) - replacements = {} - for ch, hash in hashmap: + if 'md5-' not in text: + return text + # Build hash→original mapping directly without intermediate tuple. + # html_blocks is stored inverted ({hash: item}) compared to the others. + replacements: dict[str, str] = {} + for ch, hash in self._escape_table.items(): + replacements.setdefault(hash, ch) + for ch, hash in self._code_table.items(): replacements.setdefault(hash, ch) + for hash, item in self.html_blocks.items(): + replacements.setdefault(hash, item) + # Scan once per nesting level. Use a flag instead of a full-string + # comparison to detect whether any replacement was made this pass. while True: - orig_text = text - # Scan once per nesting level instead of once for every stored hash. - text = re.sub(r'md5-[0-9a-f]{32}', - lambda match: replacements.get(match[0], match[0]), text) - if text == orig_text: + replaced = [False] + def _sub(match: re.Match, _rep: dict = replacements) -> str: + val = _rep.get(match[0]) + if val is not None: + replaced[0] = True + return val + return match[0] + text = re.sub(r'md5-[0-9a-f]{32}', _sub, text) + if not replaced[0]: break return text