Skip to content

Commit b4dd328

Browse files
committed
perf: optimize inline edge-case parsing
1 parent 1559d7b commit b4dd328

3 files changed

Lines changed: 132 additions & 33 deletions

File tree

src/mistune/core.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ def __init__(self, env: MutableMapping[str, Any]):
132132
self.in_link = False
133133
self.no_close_bracket_before: int = 0 # high-water mark for DoS mitigation
134134
self.no_link_before: int = 0 # high-water mark for failed balanced link candidates
135+
self.no_image_before: int = 0 # high-water mark for failed image candidates
135136
self.link_brackets: Dict[int, Tuple[str, Dict[int, int]]] = {}
136137
self.link_ranges: Dict[int, Tuple[str, List[int], List[int]]] = {}
137138
self.formatting_no_end: Dict[Tuple[int, str], Tuple[str, int]] = {}
@@ -150,6 +151,8 @@ def copy(self) -> "InlineState":
150151
state.in_image = self.in_image
151152
state.image_depth = self.image_depth
152153
state.in_link = self.in_link
154+
state.no_link_before = self.no_link_before
155+
state.no_image_before = self.no_image_before
153156
state.link_brackets = self.link_brackets
154157
state.link_ranges = self.link_ranges
155158
state.formatting_no_end = self.formatting_no_end

src/mistune/helpers.py

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -129,14 +129,28 @@ def parse_link_label(src: str, start_pos: int) -> Union[Tuple[str, int], Tuple[N
129129

130130

131131
def parse_link_href(src: str, start_pos: int, block: bool = False) -> Union[Tuple[str, int], Tuple[None, None]]:
132+
href, href_pos, _end_pos = _parse_link_href(src, start_pos, block=block)
133+
if href is None:
134+
return None, None
135+
assert href_pos is not None
136+
return href, href_pos
137+
138+
139+
def _parse_link_href(
140+
src: str, start_pos: int, block: bool = False
141+
) -> Tuple[Union[str, None], Union[int, None], int]:
132142
pos = _skip_link_start_whitespace(src, start_pos)
133143
if pos >= len(src):
134-
return None, None
144+
return None, None, pos
135145

136146
if src[pos] == "<":
137-
return _parse_angle_link_href(src, pos)
147+
href, href_pos = _parse_angle_link_href(src, pos)
148+
if href is None:
149+
return None, None, len(src)
150+
assert href_pos is not None
151+
return href, href_pos, href_pos
138152
if block and src[pos] in ASCII_WHITESPACE:
139-
return None, None
153+
return None, None, pos
140154

141155
start = pos
142156
level = 0
@@ -145,7 +159,7 @@ def parse_link_href(src: str, start_pos: int, block: bool = False) -> Union[Tupl
145159
if c in ASCII_WHITESPACE:
146160
break
147161
if c == "\x00":
148-
return None, None
162+
return None, None, pos
149163
if c == "\\" and pos + 1 < len(src) and src[pos + 1] in string.punctuation:
150164
pos = min(pos + 2, len(src))
151165
continue
@@ -159,8 +173,8 @@ def parse_link_href(src: str, start_pos: int, block: bool = False) -> Union[Tupl
159173
pos += 1
160174

161175
if not block and level != 0:
162-
return None, None
163-
return src[start:pos], pos
176+
return None, None, pos
177+
return src[start:pos], pos, pos
164178

165179

166180
def parse_link_title(src: str, start_pos: int, max_pos: int) -> Union[Tuple[str, int], Tuple[None, None]]:
@@ -197,21 +211,31 @@ def parse_link_title(src: str, start_pos: int, max_pos: int) -> Union[Tuple[str,
197211

198212

199213
def parse_link(src: str, pos: int) -> Union[Tuple[Dict[str, Any], int], Tuple[None, None]]:
200-
href, href_pos = parse_link_href(src, pos)
201-
if href is None:
214+
attrs, next_pos, _end_pos = parse_link_with_end(src, pos)
215+
if attrs is None:
202216
return None, None
217+
assert next_pos is not None
218+
return attrs, next_pos
219+
220+
221+
def parse_link_with_end(
222+
src: str, pos: int
223+
) -> Tuple[Union[Dict[str, Any], None], Union[int, None], int]:
224+
href, href_pos, scan_end = _parse_link_href(src, pos)
225+
if href is None:
226+
return None, None, scan_end
203227
assert href_pos is not None
204228
title, title_pos = parse_link_title(src, href_pos, len(src))
205229
next_pos = title_pos or href_pos
206230
next_pos = _skip_ascii_whitespace(src, next_pos)
207231
if next_pos >= len(src) or src[next_pos] != ")":
208-
return None, None
232+
return None, None, next_pos
209233

210234
href = unescape_char(href)
211235
attrs = {"url": escape_url(href)}
212236
if title:
213237
attrs["title"] = title
214-
return attrs, next_pos + 1
238+
return attrs, next_pos + 1, next_pos + 1
215239

216240

217241
def _skip_ascii_whitespace(src: str, pos: int, max_pos: Union[int, None] = None) -> int:

src/mistune/inline_parser.py

Lines changed: 95 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
HTML_TAGNAME,
2222
PUNCTUATION,
2323
parse_link,
24+
parse_link_with_end,
2425
parse_link_label,
2526
unescape_char,
2627
)
@@ -141,6 +142,9 @@ def parse_link(self, m: Match[str], state: InlineState) -> Optional[int]:
141142
if not is_image and pos <= state.no_link_before:
142143
state.append_token({"type": "text", "raw": marker})
143144
return pos
145+
if is_image and pos <= state.no_image_before:
146+
state.append_token({"type": "text", "raw": marker})
147+
return pos
144148

145149
text = None
146150
text_start = pos
@@ -175,22 +179,28 @@ def parse_link(self, m: Match[str], state: InlineState) -> Optional[int]:
175179
_mark_no_link_before(state, body_end_pos)
176180
return None
177181

178-
rules = ["codespan", "prec_auto_link", "prec_inline_html"]
179-
prec_pos = self.precedence_scan(m, state, end_pos, rules)
180-
if prec_pos:
181-
return prec_pos
182+
if not is_image:
183+
rules = ["codespan", "prec_auto_link", "prec_inline_html"]
184+
prec_pos = self.precedence_scan(m, state, end_pos, rules)
185+
if prec_pos:
186+
return prec_pos
182187

183188
if end_pos < len(state.src):
184189
c = state.src[end_pos]
185190
if c == "(":
186191
# standard link [text](<url> "title")
187-
attrs, pos2 = parse_link(state.src, end_pos + 1)
192+
attrs, pos2, scan_end = parse_link_with_end(state.src, end_pos + 1)
188193
if pos2:
189194
if text is None:
190195
text = state.src[text_start:text_end]
191196
token = self.__parse_link_token(is_image, text, attrs, state)
192197
state.append_token(token)
193198
return pos2
199+
if scan_end > body_end_pos:
200+
if is_image:
201+
_mark_no_image_before(state, scan_end)
202+
else:
203+
_mark_no_link_before(state, scan_end)
194204

195205
elif c == "[":
196206
# standard ref link [text][label]
@@ -557,6 +567,54 @@ class _Delimiter:
557567
#: original run length, used by the CommonMark multiple-of-3 rule even
558568
#: after the run has been partially consumed
559569
orig_length: int = 0
570+
order: int = 0
571+
572+
573+
class _DelimiterIndex:
574+
"""Track part positions without rewriting every delimiter after a splice."""
575+
576+
def __init__(self, delimiters: List[_Delimiter], part_count: int) -> None:
577+
self._tree = [0] * (part_count + 1)
578+
self._next = list(range(len(delimiters) + 1))
579+
580+
def current(self, delimiter: _Delimiter) -> int:
581+
index = delimiter.index
582+
total = 0
583+
cursor = index + 1
584+
while cursor:
585+
total += self._tree[cursor]
586+
cursor -= cursor & -cursor
587+
return index - total
588+
589+
def collapse(self, closer: _Delimiter, removed: int) -> None:
590+
if not removed:
591+
return
592+
cursor = closer.index + 1
593+
while cursor < len(self._tree):
594+
self._tree[cursor] += removed
595+
cursor += cursor & -cursor
596+
597+
def deactivate(self, order: int) -> None:
598+
self._next[order] = self._find(order + 1)
599+
600+
def deactivate_range(
601+
self, delimiters: List[_Delimiter], start: int, end: int
602+
) -> None:
603+
order = self._find(start)
604+
while order < end:
605+
delimiters[order].length = 0
606+
self._next[order] = self._find(order + 1)
607+
order = self._find(order)
608+
609+
def _find(self, order: int) -> int:
610+
root = order
611+
while self._next[root] != root:
612+
root = self._next[root]
613+
while self._next[order] != order:
614+
parent = self._next[order]
615+
self._next[order] = root
616+
order = parent
617+
return root
560618

561619

562620
def _finalize_emphasis_tokens(
@@ -711,7 +769,7 @@ def _split_text_token(
711769
index = len(parts)
712770
parts.append({"type": "text", "raw": text[pos:end]})
713771
if can_open or can_close:
714-
delimiters.append(_Delimiter(index, marker, length, can_open, can_close, length))
772+
delimiters.append(_Delimiter(index, marker, length, can_open, can_close, length, len(delimiters)))
715773
pos = end
716774

717775

@@ -726,6 +784,7 @@ def _process_emphasis_delimiters(
726784
delimiters: List[_Delimiter],
727785
max_depth: int,
728786
) -> None:
787+
index_map = _DelimiterIndex(delimiters, len(parts))
729788
closer_pos = 0
730789
openers_bottom: Dict[Tuple[str, int, bool], int] = {}
731790
while closer_pos < len(delimiters):
@@ -755,26 +814,28 @@ def _process_emphasis_delimiters(
755814
closer_pos += 1
756815
continue
757816

817+
opener_index = index_map.current(opener)
818+
closer_index = index_map.current(closer)
758819
if opener.length >= 2 and closer.length >= 2:
759820
use_length = 2
760821
else:
761822
use_length = 1
762-
if use_length == 2 and not _has_strong_enabled(parts, opener, closer):
823+
if use_length == 2 and not _has_strong_enabled_at(parts, opener_index, closer_index):
763824
use_length = 1
764-
if use_length == 1 and not _has_emphasis_enabled(parts, opener, closer):
825+
if use_length == 1 and not _has_emphasis_enabled_at(parts, opener_index, closer_index):
765826
closer_pos += 1
766827
continue
767-
if not _has_emphasis_content(parts, opener.index + 1, closer.index):
828+
if not _has_emphasis_content(parts, opener_index + 1, closer_index):
768829
closer_pos += 1
769830
continue
770831

771-
opener_text = parts[opener.index]
772-
closer_text = parts[closer.index]
832+
opener_text = parts[opener_index]
833+
closer_text = parts[closer_index]
773834
if opener_text["type"] != "text" or closer_text["type"] != "text":
774835
closer_pos += 1
775836
continue
776837

777-
children = parts[opener.index + 1 : closer.index]
838+
children = parts[opener_index + 1 : closer_index]
778839
if max_depth > 0 and _emphasis_depth(children) >= max_depth:
779840
closer_pos += 1
780841
continue
@@ -785,24 +846,22 @@ def _process_emphasis_delimiters(
785846
else:
786847
node = {"type": "emphasis", "children": children}
787848

788-
old_closer_index = closer.index
789-
parts[opener.index + 1 : old_closer_index] = [node]
849+
old_closer_index = closer_index
850+
parts[opener_index + 1 : old_closer_index] = [node]
790851

791-
removed = old_closer_index - opener.index - 2
792-
closer.index = opener.index + 2
852+
removed = old_closer_index - opener_index - 2
793853
if removed:
794-
for delimiter in delimiters:
795-
if opener.index < delimiter.index < old_closer_index:
796-
delimiter.length = 0
797-
elif delimiter.index >= old_closer_index:
798-
delimiter.index -= removed
854+
index_map.deactivate_range(delimiters, opener_pos + 1, closer_pos)
855+
index_map.collapse(closer, removed)
799856

800857
opener.length -= use_length
801858
closer.length -= use_length
802859
if opener.length == 0:
803860
opener.can_open = False
861+
index_map.deactivate(opener.order)
804862
if closer.length == 0:
805863
closer.can_close = False
864+
index_map.deactivate(closer.order)
806865

807866
if opener.can_open or closer.can_close:
808867
closer_pos = max(opener_pos, openers_bottom.get(opener_key, 0))
@@ -826,11 +885,19 @@ def _emphasis_depth(tokens: List[Dict[str, Any]]) -> int:
826885

827886

828887
def _has_strong_enabled(parts: List[Dict[str, Any]], opener: _Delimiter, closer: _Delimiter) -> bool:
829-
return len(_text_raw(parts[opener.index])) >= 2 and len(_text_raw(parts[closer.index])) >= 2
888+
return _has_strong_enabled_at(parts, opener.index, closer.index)
830889

831890

832891
def _has_emphasis_enabled(parts: List[Dict[str, Any]], opener: _Delimiter, closer: _Delimiter) -> bool:
833-
return bool(_text_raw(parts[opener.index]) and _text_raw(parts[closer.index]))
892+
return _has_emphasis_enabled_at(parts, opener.index, closer.index)
893+
894+
895+
def _has_strong_enabled_at(parts: List[Dict[str, Any]], opener_index: int, closer_index: int) -> bool:
896+
return len(_text_raw(parts[opener_index])) >= 2 and len(_text_raw(parts[closer_index])) >= 2
897+
898+
899+
def _has_emphasis_enabled_at(parts: List[Dict[str, Any]], opener_index: int, closer_index: int) -> bool:
900+
return bool(_text_raw(parts[opener_index]) and _text_raw(parts[closer_index]))
834901

835902

836903
def _text_raw(token: Dict[str, Any]) -> str:
@@ -915,6 +982,11 @@ def _mark_no_link_before(state: InlineState, end_pos: int) -> None:
915982
state.no_link_before = end_pos
916983

917984

985+
def _mark_no_image_before(state: InlineState, end_pos: int) -> None:
986+
if end_pos > state.no_image_before:
987+
state.no_image_before = end_pos
988+
989+
918990
def _find_closing_bracket(state: InlineState, pos: int) -> Optional[int]:
919991
return _get_closing_bracket_map(state).get(pos)
920992

0 commit comments

Comments
 (0)