Skip to content

Commit bf95c32

Browse files
committed
perf: improve performance
1 parent 5de41fb commit bf95c32

5 files changed

Lines changed: 85 additions & 22 deletions

File tree

src/mistune/block_parser.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
)
2121
from .list_parser import parse_list, LIST_PATTERN
2222

23+
DEFAULT_MAX_NESTED_LEVEL = 20
24+
2325
_INDENT_CODE_TRIM = re.compile(r"^ {1,4}", flags=re.M)
2426
_ATX_HEADING_TRIM = re.compile(r"(\s+|^)#+\s*$")
2527
_BLOCK_QUOTE_TRIM = re.compile(r"^ ?", flags=re.M)
@@ -92,7 +94,7 @@ def __init__(
9294
self,
9395
block_quote_rules: Optional[List[str]] = None,
9496
list_rules: Optional[List[str]] = None,
95-
max_nested_level: int = 100,
97+
max_nested_level: int = DEFAULT_MAX_NESTED_LEVEL,
9698
):
9799
super(BlockParser, self).__init__()
98100

src/mistune/core.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
Type,
1818
TypeVar,
1919
Union,
20-
cast,
2120
)
2221

2322
if sys.version_info >= (3, 11):
@@ -201,6 +200,7 @@ def register(
201200
:param before: insert this rule before a built-in rule
202201
"""
203202
self._methods[name] = lambda m, state: func(self, m, state)
203+
self.__sc.clear()
204204
if pattern:
205205
self.specification[name] = pattern
206206
if name not in self.rules:
@@ -246,7 +246,7 @@ def render_wiki(renderer, key, title):
246246

247247
def _get_method(self, name: str) -> Callable[..., str]:
248248
try:
249-
return cast(Callable[..., str], object.__getattribute__(self, name))
249+
return object.__getattribute__(self, name)
250250
except AttributeError:
251251
method = self.__methods.get(name)
252252
if not method:

src/mistune/inline_parser.py

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ def __init__(self, hard_wrap: bool = False) -> None:
8686

8787
self.hard_wrap = hard_wrap
8888
self._fast_trigger_chars: Optional[Set[str]] = None
89+
self._fast_trigger_re: Optional[re.Pattern[str]] = None
90+
self._fast_trigger_re_chars: Optional[Tuple[str, ...]] = None
8991
# lazy add linebreak
9092
if hard_wrap:
9193
self.specification["linebreak"] = self.HARD_LINEBREAK
@@ -103,6 +105,8 @@ def register(
103105
) -> None:
104106
super().register(name, pattern, func, before=before)
105107
self._fast_trigger_chars = None
108+
self._fast_trigger_re = None
109+
self._fast_trigger_re_chars = None
106110

107111
def parse_escape(self, m: Match[str], state: InlineState) -> int:
108112
text = m.group(0)
@@ -372,24 +376,23 @@ def _find_fast_text_end(self, src: str, pos: int) -> Optional[int]:
372376
if chars is None:
373377
return None
374378

375-
end_pos: Optional[int] = None
376-
for c in chars:
377-
if c == "\n":
378-
continue
379-
p = src.find(c, pos)
380-
if p != -1 and (end_pos is None or p < end_pos):
381-
end_pos = p
382-
383-
if "\n" in chars:
384-
p = src.find("\n", pos)
385-
if p != -1:
386-
p = self._find_linebreak_start(src, pos, p)
387-
if end_pos is None or p < end_pos:
388-
end_pos = p
389-
390-
if end_pos is None:
379+
trigger_re = self._get_fast_trigger_re(chars)
380+
m = trigger_re.search(src, pos)
381+
if m is None:
391382
return len(src)
392-
return end_pos
383+
384+
if m.group(0) == "\n":
385+
return self._find_linebreak_start(src, pos, m.start())
386+
return m.start()
387+
388+
def _get_fast_trigger_re(self, chars: Set[str]) -> re.Pattern[str]:
389+
key = tuple(sorted(chars))
390+
if self._fast_trigger_re is None or self._fast_trigger_re_chars != key:
391+
pattern = "[" + re.escape("".join(key)) + "]"
392+
self._fast_trigger_re = re.compile(pattern)
393+
self._fast_trigger_re_chars = key
394+
assert self._fast_trigger_re is not None
395+
return self._fast_trigger_re
393396

394397
def _find_linebreak_start(self, src: str, min_pos: int, newline_pos: int) -> int:
395398
pos = newline_pos

tests/test_misc.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,41 @@ def test_use_plugin(self):
154154
md = mistune.Markdown(mistune.HTMLRenderer())
155155
md.use(url)
156156

157+
def test_inline_plugin_refreshes_fast_trigger_cache(self):
158+
def parse_at(inline, m, state):
159+
state.append_token({"type": "at", "raw": m.group(0)})
160+
return m.end()
161+
162+
def render_at(renderer, text):
163+
return "<at>" + text + "</at>"
164+
165+
def plugin(md):
166+
md.inline.register("at", r"@+", parse_at)
167+
md.renderer.register("at", render_at)
168+
169+
md = mistune.create_markdown(escape=False)
170+
md("plain text")
171+
md.use(plugin)
172+
173+
self.assertEqual(md("@@").strip(), "<p><at>@@</at></p>")
174+
175+
def test_inline_plugin_with_untracked_trigger_uses_regex_scanner(self):
176+
def parse_x(inline, m, state):
177+
state.append_token({"type": "xword", "raw": m.group(0)})
178+
return m.end()
179+
180+
def render_x(renderer, text):
181+
return "<x>" + text + "</x>"
182+
183+
def plugin(md):
184+
md.inline.register("xword", r"(?=x)x+", parse_x)
185+
md.renderer.register("xword", render_x)
186+
187+
md = mistune.create_markdown(escape=False)
188+
md.use(plugin)
189+
190+
self.assertEqual(md("xx").strip(), "<p><x>xx</x></p>")
191+
157192
def test_markdown_func(self):
158193
result = mistune.markdown("**b**")
159194
expected = "<p><strong>b</strong></p>\n"
@@ -248,11 +283,18 @@ def test_html_tag_text_following_list(self):
248283
expected = "<p>foo</p>\n<ul>\n<li>bar</li>\n</ul>\n<p>table</p>"
249284
self.assertEqual(result.strip(), expected)
250285

286+
def test_max_nested_level_setting(self):
287+
self.assertEqual(mistune.BlockParser().max_nested_level, 20)
288+
self.assertEqual(mistune.create_markdown().block.max_nested_level, 20)
289+
md = mistune.Markdown(block=mistune.BlockParser(max_nested_level=7))
290+
self.assertEqual(md.block.max_nested_level, 7)
291+
251292
def test_deeply_nested_block_quote_and_list(self):
252293
# Block quotes and lists each capped their own nesting at the limit but
253294
# not each other's, so alternating them recursed without bound. This
254295
# must terminate instead of raising RecursionError.
255-
text = "".join(">" * i + " " + "- " * i + "item\n" for i in range(1, 300))
296+
max_level = mistune.create_markdown().block.max_nested_level
297+
text = "".join(">" * i + " " + "- " * i + "item\n" for i in range(1, max_level + 80))
256298
md = mistune.create_markdown()
257299
md(text)
258300
ast = mistune.create_markdown(renderer="ast")

tests/test_renderers.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from unittest import TestCase
22

3-
from mistune import create_markdown
3+
from mistune import BlockState, HTMLRenderer, create_markdown
44
from mistune.renderers.rst import RSTRenderer
55
from mistune.renderers.markdown import MarkdownRenderer
66
from tests import BaseTestCase
@@ -65,3 +65,19 @@ def test_prose_not_over_escaped(self):
6565
"ratio a/b is fine\n",
6666
):
6767
self.assert_round_trip(text)
68+
69+
70+
class TestRendererMethodRegistration(TestCase):
71+
def test_registered_renderer_method(self):
72+
renderer = HTMLRenderer(escape=False)
73+
74+
def render_wiki(renderer, key, title):
75+
return f'<a href="/wiki/{key}">{title}</a>'
76+
77+
renderer.register("wiki", render_wiki)
78+
token = {"type": "wiki", "raw": "python", "attrs": {"title": "Python"}}
79+
80+
self.assertEqual(
81+
renderer.render_token(token, BlockState()),
82+
'<a href="/wiki/python">Python</a>',
83+
)

0 commit comments

Comments
 (0)