mistune 3.3.2__tar.gz → 3.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mistune-3.3.2 → mistune-3.3.3}/PKG-INFO +1 -1
- {mistune-3.3.2 → mistune-3.3.3}/docs/changes.rst +17 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/__init__.py +1 -1
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/core.py +7 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/inline_parser.py +162 -73
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/footnotes.py +6 -1
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/formatting.py +6 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/ruby.py +16 -12
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/spoiler.py +21 -5
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/renderers/_list.py +3 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/renderers/html.py +33 -21
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/renderers/markdown.py +35 -6
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/renderers/rst.py +1 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune.egg-info/PKG-INFO +1 -1
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/fix-commonmark.txt +15 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/renderer_rst.txt +16 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/test_commonmark.py +4 -4
- {mistune-3.3.2 → mistune-3.3.3}/tests/test_misc.py +12 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/test_renderers.py +66 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/test_security_formatting.py +22 -0
- mistune-3.3.3/tests/test_security_inline.py +156 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/test_security_math.py +19 -0
- mistune-3.3.3/tests/test_security_urls.py +76 -0
- mistune-3.3.2/tests/test_security_inline.py +0 -37
- mistune-3.3.2/tests/test_security_urls.py +0 -20
- {mistune-3.3.2 → mistune-3.3.3}/LICENSE +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/MANIFEST.in +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/README.rst +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/benchmark/bench.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/atx.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/auto_links.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/blockcode.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/blockhtml.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/blockquote.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/emphasis.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/fenced.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/insane_ol.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/insane_ul.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/normal_ol.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/normal_ul.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/paragraph.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/ref_links.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/setext.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/std_links.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/docs/Makefile +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/docs/_static/dark-icon.svg +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/docs/_static/light-icon.svg +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/docs/_static/logo-black.svg +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/docs/_static/logo-white.svg +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/docs/_templates/partials/extra-foot.html +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/docs/_templates/partials/globaltoc-above.html +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/docs/advanced.rst +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/docs/api.rst +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/docs/cli.rst +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/docs/community.rst +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/docs/conf.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/docs/directives.rst +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/docs/guide.rst +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/docs/index.rst +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/docs/plugins.rst +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/docs/renderers.rst +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/docs/upgrade.rst +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/pyproject.toml +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/setup.cfg +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/setup.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/__main__.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/block_parser.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/directives/__init__.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/directives/_base.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/directives/_fenced.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/directives/_rst.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/directives/admonition.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/directives/image.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/directives/include.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/directives/toc.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/helpers.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/list_parser.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/markdown.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/__init__.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/abbr.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/def_list.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/math.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/speedup.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/table.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/task_lists.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/url.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/py.typed +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/renderers/__init__.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/toc.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune/util.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune.egg-info/SOURCES.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune.egg-info/dependency_links.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune.egg-info/entry_points.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune.egg-info/requires.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/src/mistune.egg-info/top_level.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/__init__.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/__init__.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/abbr.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/cli.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/commonmark.json +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/def_list.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/fenced_admonition.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/fenced_figure.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/fenced_image.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/fenced_toc.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/footnotes.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/hook_toc.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/include/hello.md +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/include/hello.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/include/text.html +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/include/text.md +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/insert.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/mark.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/math.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/renderer_markdown.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/rst_admonition.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/rst_toc.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/ruby.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/spoiler.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/strikethrough.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/subscript.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/superscript.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/table.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/task_lists.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/url.txt +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/test_cli.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/test_directives.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/test_hooks.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/test_plugins.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/test_security_image.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/test_security_include.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/test_security_ref_links.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/test_security_toc.py +0 -0
- {mistune-3.3.2 → mistune-3.3.3}/tests/test_syntax.py +0 -0
|
@@ -3,6 +3,23 @@ Changelog
|
|
|
3
3
|
|
|
4
4
|
Here is the full history of mistune v3.
|
|
5
5
|
|
|
6
|
+
Unreleased
|
|
7
|
+
----------
|
|
8
|
+
|
|
9
|
+
* Escape literal ``*``/``_`` emphasis markers in the Markdown renderer so
|
|
10
|
+
round-tripping escaped text does not re-introduce emphasis.
|
|
11
|
+
|
|
12
|
+
Version 3.3.3
|
|
13
|
+
-------------
|
|
14
|
+
|
|
15
|
+
**Released on Jul 9, 2026**
|
|
16
|
+
|
|
17
|
+
* Limit deeply nested emphasis and image parsing to avoid ``RecursionError``.
|
|
18
|
+
* Fix repeated link suffix and unclosed formatting marker performance issues.
|
|
19
|
+
* Fix unclosed inline spoiler performance issues.
|
|
20
|
+
* Avoid recursive parsing for adjacent ruby tokens.
|
|
21
|
+
* Speed up footnote reference indexing.
|
|
22
|
+
|
|
6
23
|
Version 3.3.2
|
|
7
24
|
-------------
|
|
8
25
|
|
|
@@ -128,9 +128,13 @@ class InlineState:
|
|
|
128
128
|
self.src = ""
|
|
129
129
|
self.tokens: List[Dict[str, Any]] = []
|
|
130
130
|
self.in_image = False
|
|
131
|
+
self.image_depth = 0
|
|
131
132
|
self.in_link = False
|
|
132
133
|
self.no_close_bracket_before: int = 0 # high-water mark for DoS mitigation
|
|
134
|
+
self.no_link_before: int = 0 # high-water mark for failed balanced link candidates
|
|
133
135
|
self.link_brackets: Dict[int, Tuple[str, Dict[int, int]]] = {}
|
|
136
|
+
self.link_ranges: Dict[int, Tuple[str, List[int], List[int]]] = {}
|
|
137
|
+
self.formatting_no_end: Dict[Tuple[int, str], Tuple[str, int]] = {}
|
|
134
138
|
|
|
135
139
|
def prepend_token(self, token: Dict[str, Any]) -> None:
|
|
136
140
|
"""Insert token before the last token."""
|
|
@@ -144,8 +148,11 @@ class InlineState:
|
|
|
144
148
|
"""Create a copy of current state."""
|
|
145
149
|
state = self.__class__(self.env)
|
|
146
150
|
state.in_image = self.in_image
|
|
151
|
+
state.image_depth = self.image_depth
|
|
147
152
|
state.in_link = self.in_link
|
|
148
153
|
state.link_brackets = self.link_brackets
|
|
154
|
+
state.link_ranges = self.link_ranges
|
|
155
|
+
state.formatting_no_end = self.formatting_no_end
|
|
149
156
|
return state
|
|
150
157
|
|
|
151
158
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
+
from bisect import bisect_left
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from typing import (
|
|
6
7
|
Any,
|
|
@@ -27,6 +28,8 @@ from .util import escape_url, unikey
|
|
|
27
28
|
|
|
28
29
|
_REGEX_META_CHARS = set(r"()[]{}?*+|.^$")
|
|
29
30
|
_CHARREF_PREFIX = re.compile(r"(#[0-9]{1,7};|#[xX][0-9a-fA-F]+;|[^\t\n\f <&#;]{1,32};)")
|
|
31
|
+
DEFAULT_MAX_EMPHASIS_DEPTH = 20
|
|
32
|
+
DEFAULT_MAX_IMAGE_DEPTH = 20
|
|
30
33
|
|
|
31
34
|
AUTO_EMAIL = (
|
|
32
35
|
r"""<[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9]"""
|
|
@@ -84,10 +87,17 @@ class InlineParser(Parser[InlineState]):
|
|
|
84
87
|
"linebreak",
|
|
85
88
|
)
|
|
86
89
|
|
|
87
|
-
def __init__(
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
hard_wrap: bool = False,
|
|
93
|
+
max_emphasis_depth: int = DEFAULT_MAX_EMPHASIS_DEPTH,
|
|
94
|
+
max_image_depth: int = DEFAULT_MAX_IMAGE_DEPTH,
|
|
95
|
+
) -> None:
|
|
88
96
|
super(InlineParser, self).__init__()
|
|
89
97
|
|
|
90
98
|
self.hard_wrap = hard_wrap
|
|
99
|
+
self.max_emphasis_depth = max_emphasis_depth
|
|
100
|
+
self.max_image_depth = max_image_depth
|
|
91
101
|
self._fast_trigger_chars: Optional[Set[str]] = None
|
|
92
102
|
self._fast_trigger_re: Optional[re.Pattern[str]] = None
|
|
93
103
|
self._fast_trigger_re_chars: Optional[Tuple[str, ...]] = None
|
|
@@ -122,33 +132,47 @@ class InlineParser(Parser[InlineState]):
|
|
|
122
132
|
|
|
123
133
|
marker = m.group(0)
|
|
124
134
|
is_image = marker[0] == "!"
|
|
135
|
+
if is_image and self.max_image_depth > 0 and state.image_depth >= self.max_image_depth:
|
|
136
|
+
state.append_token({"type": "text", "raw": marker})
|
|
137
|
+
return pos
|
|
125
138
|
if not is_image and state.in_link:
|
|
126
139
|
state.append_token({"type": "text", "raw": marker})
|
|
127
140
|
return pos
|
|
141
|
+
if not is_image and pos <= state.no_link_before:
|
|
142
|
+
state.append_token({"type": "text", "raw": marker})
|
|
143
|
+
return pos
|
|
128
144
|
|
|
129
145
|
text = None
|
|
146
|
+
text_start = pos
|
|
147
|
+
text_end = pos
|
|
130
148
|
label, end_pos = parse_link_label(state.src, pos)
|
|
131
149
|
if label is None:
|
|
132
150
|
if pos <= state.no_close_bracket_before:
|
|
133
151
|
state.append_token({"type": "text", "raw": marker})
|
|
134
152
|
return pos
|
|
135
|
-
|
|
136
|
-
if
|
|
137
|
-
if
|
|
138
|
-
state.no_close_bracket_before =
|
|
153
|
+
close_pos = _find_closing_bracket(state, pos)
|
|
154
|
+
if close_pos is None:
|
|
155
|
+
if len(state.src) > state.no_close_bracket_before:
|
|
156
|
+
state.no_close_bracket_before = len(state.src)
|
|
139
157
|
return None
|
|
158
|
+
text_start = pos
|
|
159
|
+
text_end = close_pos
|
|
160
|
+
end_pos = close_pos + 1
|
|
140
161
|
|
|
141
162
|
assert end_pos is not None
|
|
142
163
|
|
|
143
|
-
if
|
|
164
|
+
if label is not None:
|
|
144
165
|
text = label
|
|
166
|
+
text_start = pos
|
|
167
|
+
text_end = end_pos - 1
|
|
145
168
|
|
|
146
|
-
|
|
169
|
+
body_end_pos = end_pos
|
|
147
170
|
|
|
148
|
-
|
|
171
|
+
has_nested_link = not is_image and _label_contains_link(state, text_start, text_end)
|
|
172
|
+
if has_nested_link:
|
|
149
173
|
return None
|
|
150
|
-
|
|
151
|
-
|
|
174
|
+
if end_pos >= len(state.src) and label is None:
|
|
175
|
+
_mark_no_link_before(state, body_end_pos)
|
|
152
176
|
return None
|
|
153
177
|
|
|
154
178
|
rules = ["codespan", "prec_auto_link", "prec_inline_html"]
|
|
@@ -162,6 +186,8 @@ class InlineParser(Parser[InlineState]):
|
|
|
162
186
|
# standard link [text](<url> "title")
|
|
163
187
|
attrs, pos2 = parse_link(state.src, end_pos + 1)
|
|
164
188
|
if pos2:
|
|
189
|
+
if text is None:
|
|
190
|
+
text = state.src[text_start:text_end]
|
|
165
191
|
token = self.__parse_link_token(is_image, text, attrs, state)
|
|
166
192
|
state.append_token(token)
|
|
167
193
|
return pos2
|
|
@@ -175,46 +201,32 @@ class InlineParser(Parser[InlineState]):
|
|
|
175
201
|
label = label2
|
|
176
202
|
|
|
177
203
|
if label is None:
|
|
178
|
-
|
|
179
|
-
|
|
204
|
+
ref_links = state.env.get("ref_links")
|
|
205
|
+
if not ref_links:
|
|
206
|
+
_mark_no_link_before(state, body_end_pos)
|
|
207
|
+
return None
|
|
208
|
+
if text is None:
|
|
209
|
+
text = state.src[text_start:text_end]
|
|
210
|
+
label = text
|
|
180
211
|
ref_links = state.env.get("ref_links")
|
|
181
212
|
if not ref_links:
|
|
213
|
+
_mark_no_link_before(state, body_end_pos)
|
|
182
214
|
return None
|
|
183
215
|
|
|
184
216
|
key = unikey(label)
|
|
185
217
|
env = ref_links.get(key)
|
|
186
218
|
if env:
|
|
219
|
+
if text is None:
|
|
220
|
+
text = state.src[text_start:text_end]
|
|
187
221
|
attrs = {"url": env["url"], "title": env.get("title")}
|
|
188
222
|
token = self.__parse_link_token(is_image, text, attrs, state)
|
|
189
223
|
token["ref"] = key
|
|
190
224
|
token["label"] = label
|
|
191
225
|
state.append_token(token)
|
|
192
226
|
return end_pos
|
|
227
|
+
_mark_no_link_before(state, body_end_pos)
|
|
193
228
|
return None
|
|
194
229
|
|
|
195
|
-
def _contains_nested_link(self, text: str, state: InlineState) -> bool:
|
|
196
|
-
if "[" not in text:
|
|
197
|
-
return False
|
|
198
|
-
if "](" not in text and "][" not in text and not state.env.get("ref_links"):
|
|
199
|
-
return False
|
|
200
|
-
|
|
201
|
-
sc = self.compile_sc(["link"])
|
|
202
|
-
nested_state = state.copy()
|
|
203
|
-
nested_state.src = text
|
|
204
|
-
pos = 0
|
|
205
|
-
while pos < len(text):
|
|
206
|
-
m = sc.search(text, pos)
|
|
207
|
-
if not m:
|
|
208
|
-
return False
|
|
209
|
-
|
|
210
|
-
marker = m.group(0)
|
|
211
|
-
if marker == "[" and (m.start() == 0 or text[m.start() - 1] != "!"):
|
|
212
|
-
if _is_link_like(text, m.end(), nested_state):
|
|
213
|
-
return True
|
|
214
|
-
pos = m.start() + 1
|
|
215
|
-
|
|
216
|
-
return False
|
|
217
|
-
|
|
218
230
|
def __parse_link_token(
|
|
219
231
|
self,
|
|
220
232
|
is_image: bool,
|
|
@@ -226,6 +238,7 @@ class InlineParser(Parser[InlineState]):
|
|
|
226
238
|
new_state.src = text
|
|
227
239
|
if is_image:
|
|
228
240
|
new_state.in_image = True
|
|
241
|
+
new_state.image_depth += 1
|
|
229
242
|
token = {
|
|
230
243
|
"type": "image",
|
|
231
244
|
"children": self.render(new_state),
|
|
@@ -372,7 +385,11 @@ class InlineParser(Parser[InlineState]):
|
|
|
372
385
|
self.process_text(state.src, state)
|
|
373
386
|
elif pos < len(state.src):
|
|
374
387
|
self.process_text(state.src[pos:], state)
|
|
375
|
-
state.tokens = _finalize_emphasis_tokens(
|
|
388
|
+
state.tokens = _finalize_emphasis_tokens(
|
|
389
|
+
state.tokens,
|
|
390
|
+
"emphasis" in self.rules,
|
|
391
|
+
self.max_emphasis_depth,
|
|
392
|
+
)
|
|
376
393
|
return state.tokens
|
|
377
394
|
|
|
378
395
|
def _find_fast_text_end(self, src: str, pos: int) -> Optional[int]:
|
|
@@ -529,9 +546,16 @@ class _Delimiter:
|
|
|
529
546
|
length: int
|
|
530
547
|
can_open: bool
|
|
531
548
|
can_close: bool
|
|
549
|
+
#: original run length, used by the CommonMark multiple-of-3 rule even
|
|
550
|
+
#: after the run has been partially consumed
|
|
551
|
+
orig_length: int = 0
|
|
532
552
|
|
|
533
553
|
|
|
534
|
-
def _finalize_emphasis_tokens(
|
|
554
|
+
def _finalize_emphasis_tokens(
|
|
555
|
+
tokens: List[Dict[str, Any]],
|
|
556
|
+
enabled: bool,
|
|
557
|
+
max_depth: int = DEFAULT_MAX_EMPHASIS_DEPTH,
|
|
558
|
+
) -> List[Dict[str, Any]]:
|
|
535
559
|
if not enabled:
|
|
536
560
|
return _clean_emphasis_tokens(tokens)
|
|
537
561
|
if not _contains_emphasis_marker(tokens):
|
|
@@ -548,7 +572,7 @@ def _finalize_emphasis_tokens(tokens: List[Dict[str, Any]], enabled: bool) -> Li
|
|
|
548
572
|
parts.append(_clean_emphasis_token(token))
|
|
549
573
|
source_pos += _emphasis_source_length(token)
|
|
550
574
|
|
|
551
|
-
_process_emphasis_delimiters(parts, delimiters)
|
|
575
|
+
_process_emphasis_delimiters(parts, delimiters, max_depth)
|
|
552
576
|
return _merge_text_tokens(parts)
|
|
553
577
|
|
|
554
578
|
|
|
@@ -618,7 +642,7 @@ def _split_text_token(
|
|
|
618
642
|
index = len(parts)
|
|
619
643
|
parts.append({"type": "text", "raw": text[pos:end]})
|
|
620
644
|
if can_open or can_close:
|
|
621
|
-
delimiters.append(_Delimiter(index, marker, length, can_open, can_close))
|
|
645
|
+
delimiters.append(_Delimiter(index, marker, length, can_open, can_close, length))
|
|
622
646
|
pos = end
|
|
623
647
|
|
|
624
648
|
|
|
@@ -628,7 +652,11 @@ def _next_delimiter_run(text: str, pos: int) -> int:
|
|
|
628
652
|
return pos
|
|
629
653
|
|
|
630
654
|
|
|
631
|
-
def _process_emphasis_delimiters(
|
|
655
|
+
def _process_emphasis_delimiters(
|
|
656
|
+
parts: List[Dict[str, Any]],
|
|
657
|
+
delimiters: List[_Delimiter],
|
|
658
|
+
max_depth: int,
|
|
659
|
+
) -> None:
|
|
632
660
|
closer_pos = 0
|
|
633
661
|
openers_bottom: Dict[Tuple[str, int, bool], int] = {}
|
|
634
662
|
while closer_pos < len(delimiters):
|
|
@@ -677,9 +705,12 @@ def _process_emphasis_delimiters(parts: List[Dict[str, Any]], delimiters: List[_
|
|
|
677
705
|
closer_pos += 1
|
|
678
706
|
continue
|
|
679
707
|
|
|
708
|
+
children = parts[opener.index + 1 : closer.index]
|
|
709
|
+
if max_depth > 0 and _emphasis_depth(children) >= max_depth:
|
|
710
|
+
closer_pos += 1
|
|
711
|
+
continue
|
|
680
712
|
opener_text["raw"] = opener_text["raw"][:-use_length]
|
|
681
713
|
closer_text["raw"] = closer_text["raw"][use_length:]
|
|
682
|
-
children = parts[opener.index + 1 : closer.index]
|
|
683
714
|
if use_length == 2:
|
|
684
715
|
node = {"type": "strong", "children": children}
|
|
685
716
|
else:
|
|
@@ -710,6 +741,21 @@ def _process_emphasis_delimiters(parts: List[Dict[str, Any]], delimiters: List[_
|
|
|
710
741
|
closer_pos += 1
|
|
711
742
|
|
|
712
743
|
|
|
744
|
+
def _emphasis_depth(tokens: List[Dict[str, Any]]) -> int:
|
|
745
|
+
max_depth = 0
|
|
746
|
+
stack = [(token, 0) for token in tokens]
|
|
747
|
+
while stack:
|
|
748
|
+
token, depth = stack.pop()
|
|
749
|
+
token_type = token["type"]
|
|
750
|
+
if token_type in ("emphasis", "strong"):
|
|
751
|
+
depth += 1
|
|
752
|
+
if depth > max_depth:
|
|
753
|
+
max_depth = depth
|
|
754
|
+
for child in token.get("children", ()):
|
|
755
|
+
stack.append((child, depth))
|
|
756
|
+
return max_depth
|
|
757
|
+
|
|
758
|
+
|
|
713
759
|
def _has_strong_enabled(parts: List[Dict[str, Any]], opener: _Delimiter, closer: _Delimiter) -> bool:
|
|
714
760
|
return len(_text_raw(parts[opener.index])) >= 2 and len(_text_raw(parts[closer.index])) >= 2
|
|
715
761
|
|
|
@@ -733,7 +779,12 @@ def _has_emphasis_content(parts: List[Dict[str, Any]], start: int, end: int) ->
|
|
|
733
779
|
|
|
734
780
|
def _can_match_emphasis_delimiters(opener: _Delimiter, closer: _Delimiter) -> bool:
|
|
735
781
|
if opener.can_close or closer.can_open:
|
|
736
|
-
|
|
782
|
+
# CommonMark rules 9 and 10: the multiple-of-3 test uses the lengths of
|
|
783
|
+
# the original delimiter runs, not the lengths remaining after the runs
|
|
784
|
+
# have been partially consumed.
|
|
785
|
+
open_len = opener.orig_length
|
|
786
|
+
close_len = closer.orig_length
|
|
787
|
+
return (open_len + close_len) % 3 != 0 or open_len % 3 == 0 and close_len % 3 == 0
|
|
737
788
|
return True
|
|
738
789
|
|
|
739
790
|
|
|
@@ -790,51 +841,89 @@ def _merge_text_tokens(tokens: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
|
790
841
|
return result
|
|
791
842
|
|
|
792
843
|
|
|
793
|
-
def
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
return None, len(state.src)
|
|
797
|
-
return state.src[pos:close_pos], close_pos + 1
|
|
844
|
+
def _mark_no_link_before(state: InlineState, end_pos: int) -> None:
|
|
845
|
+
if end_pos > state.no_link_before:
|
|
846
|
+
state.no_link_before = end_pos
|
|
798
847
|
|
|
799
848
|
|
|
800
849
|
def _find_closing_bracket(state: InlineState, pos: int) -> Optional[int]:
|
|
850
|
+
return _get_closing_bracket_map(state).get(pos)
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def _label_contains_link(state: InlineState, start: int, end: int) -> bool:
|
|
854
|
+
if start >= end:
|
|
855
|
+
return False
|
|
856
|
+
|
|
857
|
+
starts, suffix_min_ends = _get_link_range_index(state)
|
|
858
|
+
index = bisect_left(starts, start)
|
|
859
|
+
return index < len(starts) and starts[index] < end and suffix_min_ends[index] <= end
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def _get_link_range_index(state: InlineState) -> Tuple[List[int], List[int]]:
|
|
863
|
+
cache = state.link_ranges.get(id(state.src))
|
|
864
|
+
if cache is not None and cache[0] is state.src:
|
|
865
|
+
return cache[1], cache[2]
|
|
866
|
+
|
|
867
|
+
pairs = _get_closing_bracket_map(state)
|
|
868
|
+
ranges: List[Tuple[int, int]] = []
|
|
869
|
+
for label_start, close_pos in pairs.items():
|
|
870
|
+
opener = label_start - 1
|
|
871
|
+
if opener > 0 and state.src[opener - 1] == "!":
|
|
872
|
+
continue
|
|
873
|
+
link_end = _find_link_range_end(state.src, label_start, close_pos, state)
|
|
874
|
+
if link_end is not None:
|
|
875
|
+
ranges.append((opener, link_end))
|
|
876
|
+
|
|
877
|
+
ranges.sort()
|
|
878
|
+
starts = [start for start, _end in ranges]
|
|
879
|
+
suffix_min_ends = [0] * len(ranges)
|
|
880
|
+
min_end = len(state.src) + 1
|
|
881
|
+
for index in range(len(ranges) - 1, -1, -1):
|
|
882
|
+
end = ranges[index][1]
|
|
883
|
+
if end < min_end:
|
|
884
|
+
min_end = end
|
|
885
|
+
suffix_min_ends[index] = min_end
|
|
886
|
+
|
|
887
|
+
state.link_ranges[id(state.src)] = (state.src, starts, suffix_min_ends)
|
|
888
|
+
return starts, suffix_min_ends
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def _get_closing_bracket_map(state: InlineState) -> Dict[int, int]:
|
|
801
892
|
cache = state.link_brackets.get(id(state.src))
|
|
802
893
|
if cache is not None and cache[0] is state.src:
|
|
803
|
-
return cache[1]
|
|
894
|
+
return cache[1]
|
|
804
895
|
|
|
805
896
|
pairs = _build_closing_bracket_map(state.src)
|
|
806
897
|
state.link_brackets[id(state.src)] = (state.src, pairs)
|
|
807
|
-
return pairs
|
|
808
|
-
|
|
898
|
+
return pairs
|
|
809
899
|
|
|
810
|
-
def _is_link_like(src: str, pos: int, state: InlineState) -> bool:
|
|
811
|
-
label, end_pos = parse_link_label(src, pos)
|
|
812
|
-
if label is None:
|
|
813
|
-
label, end_pos = _parse_link_text(state, pos)
|
|
814
|
-
if label is None:
|
|
815
|
-
return False
|
|
816
900
|
|
|
817
|
-
|
|
818
|
-
|
|
901
|
+
def _find_link_range_end(src: str, label_start: int, close_pos: int, state: InlineState) -> Optional[int]:
|
|
902
|
+
end_pos = close_pos + 1
|
|
903
|
+
if end_pos < len(src):
|
|
904
|
+
marker = src[end_pos]
|
|
905
|
+
if marker == "(":
|
|
906
|
+
_attrs, new_pos = parse_link(src, end_pos + 1)
|
|
907
|
+
return new_pos
|
|
819
908
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
909
|
+
if marker == "[":
|
|
910
|
+
label, new_pos = parse_link_label(src, end_pos + 1)
|
|
911
|
+
if not new_pos:
|
|
912
|
+
return None
|
|
913
|
+
if label:
|
|
914
|
+
ref_label = label
|
|
915
|
+
else:
|
|
916
|
+
ref_label = src[label_start:close_pos]
|
|
828
917
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
return
|
|
833
|
-
if label2:
|
|
834
|
-
label = label2
|
|
918
|
+
ref_links = state.env.get("ref_links")
|
|
919
|
+
if ref_links and unikey(ref_label) in ref_links:
|
|
920
|
+
return new_pos
|
|
921
|
+
return None
|
|
835
922
|
|
|
836
923
|
ref_links = state.env.get("ref_links")
|
|
837
|
-
|
|
924
|
+
if ref_links and unikey(src[label_start:close_pos]) in ref_links:
|
|
925
|
+
return end_pos
|
|
926
|
+
return None
|
|
838
927
|
|
|
839
928
|
|
|
840
929
|
def _build_closing_bracket_map(src: str) -> Dict[int, int]:
|
|
@@ -34,10 +34,15 @@ def parse_inline_footnote(inline: "InlineParser", m: Match[str], state: "InlineS
|
|
|
34
34
|
notes = state.env.get("footnotes")
|
|
35
35
|
if not notes:
|
|
36
36
|
notes = []
|
|
37
|
+
indexes = state.env.get("footnote_indexes")
|
|
38
|
+
if not indexes:
|
|
39
|
+
indexes = {note_key: index for index, note_key in enumerate(notes)}
|
|
40
|
+
state.env["footnote_indexes"] = indexes
|
|
37
41
|
if key not in notes:
|
|
38
42
|
notes.append(key)
|
|
43
|
+
indexes[key] = len(notes) - 1
|
|
39
44
|
state.env["footnotes"] = notes
|
|
40
|
-
state.append_token({"type": "footnote_ref", "raw": key, "attrs": {"index":
|
|
45
|
+
state.append_token({"type": "footnote_ref", "raw": key, "attrs": {"index": indexes[key] + 1}})
|
|
41
46
|
else:
|
|
42
47
|
state.append_token({"type": "text", "raw": m.group(0)})
|
|
43
48
|
return m.end()
|
|
@@ -61,8 +61,14 @@ def _parse_to_end(
|
|
|
61
61
|
marker: str,
|
|
62
62
|
) -> Optional[int]:
|
|
63
63
|
pos = m.end()
|
|
64
|
+
cache_key = (id(state.src), marker)
|
|
65
|
+
cache = state.formatting_no_end.get(cache_key)
|
|
66
|
+
if cache is not None and cache[0] is state.src and pos <= cache[1]:
|
|
67
|
+
return None
|
|
68
|
+
|
|
64
69
|
end_pos = _find_end_marker(state.src, pos, marker)
|
|
65
70
|
if end_pos is None:
|
|
71
|
+
state.formatting_no_end[cache_key] = (state.src, len(state.src))
|
|
66
72
|
return None
|
|
67
73
|
text = state.src[pos : end_pos - 2]
|
|
68
74
|
new_state = state.copy()
|
|
@@ -15,20 +15,15 @@ _ruby_re = re.compile(RUBY_PATTERN)
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def parse_ruby(inline: "InlineParser", m: Match[str], state: "InlineState") -> int:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
end_pos = m.end()
|
|
26
|
-
|
|
27
|
-
next_match = _ruby_re.match(state.src, end_pos)
|
|
28
|
-
if next_match:
|
|
18
|
+
while True:
|
|
19
|
+
tokens = _parse_ruby_tokens(m)
|
|
20
|
+
end_pos = m.end()
|
|
21
|
+
next_match = _ruby_re.match(state.src, end_pos)
|
|
22
|
+
if not next_match:
|
|
23
|
+
break
|
|
29
24
|
for tok in tokens:
|
|
30
25
|
state.append_token(tok)
|
|
31
|
-
|
|
26
|
+
m = next_match
|
|
32
27
|
|
|
33
28
|
# repeat link logic
|
|
34
29
|
if end_pos < len(state.src):
|
|
@@ -41,6 +36,15 @@ def parse_ruby(inline: "InlineParser", m: Match[str], state: "InlineState") -> i
|
|
|
41
36
|
return end_pos
|
|
42
37
|
|
|
43
38
|
|
|
39
|
+
def _parse_ruby_tokens(m: Match[str]) -> List[Dict[str, Any]]:
|
|
40
|
+
text = m.group(0)[1:-2]
|
|
41
|
+
tokens = []
|
|
42
|
+
for item in text.split(")"):
|
|
43
|
+
rb, rt = item.split("(")
|
|
44
|
+
tokens.append({"type": "ruby", "raw": rb, "attrs": {"rt": rt}})
|
|
45
|
+
return tokens
|
|
46
|
+
|
|
47
|
+
|
|
44
48
|
def _parse_ruby_link(
|
|
45
49
|
inline: "InlineParser", state: "InlineState", pos: int, tokens: List[Dict[str, Any]]
|
|
46
50
|
) -> Optional[int]:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import re
|
|
2
|
-
from typing import TYPE_CHECKING, Match
|
|
2
|
+
from typing import TYPE_CHECKING, Match, Optional
|
|
3
3
|
|
|
4
4
|
if TYPE_CHECKING:
|
|
5
5
|
from ..block_parser import BlockParser
|
|
@@ -12,7 +12,8 @@ __all__ = ["spoiler"]
|
|
|
12
12
|
_BLOCK_SPOILER_START = re.compile(r"^ {0,3}! ?", re.M)
|
|
13
13
|
_BLOCK_SPOILER_MATCH = re.compile(r"^( {0,3}![^\n]*\n)+$")
|
|
14
14
|
|
|
15
|
-
INLINE_SPOILER_PATTERN = r"
|
|
15
|
+
INLINE_SPOILER_PATTERN = r">!"
|
|
16
|
+
_INLINE_SPOILER_END = re.compile(r"\s*(?P<spoiler_text>.+?)\s*!<")
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
def parse_block_spoiler(block: "BlockParser", m: Match[str], state: "BlockState") -> int:
|
|
@@ -46,13 +47,28 @@ def parse_block_spoiler(block: "BlockParser", m: Match[str], state: "BlockState"
|
|
|
46
47
|
return state.cursor
|
|
47
48
|
|
|
48
49
|
|
|
49
|
-
def parse_inline_spoiler(inline: "InlineParser", m: Match[str], state: "InlineState") -> int:
|
|
50
|
-
|
|
50
|
+
def parse_inline_spoiler(inline: "InlineParser", m: Match[str], state: "InlineState") -> Optional[int]:
|
|
51
|
+
pos = m.end()
|
|
52
|
+
cache_key = (id(state.src), "inline_spoiler")
|
|
53
|
+
cache = state.formatting_no_end.get(cache_key)
|
|
54
|
+
if cache is not None and cache[0] is state.src and pos <= cache[1]:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
line_end = state.src.find("\n", pos)
|
|
58
|
+
if line_end == -1:
|
|
59
|
+
line_end = len(state.src)
|
|
60
|
+
|
|
61
|
+
m2 = _INLINE_SPOILER_END.match(state.src, pos, line_end)
|
|
62
|
+
if not m2:
|
|
63
|
+
state.formatting_no_end[cache_key] = (state.src, line_end)
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
text = m2.group("spoiler_text")
|
|
51
67
|
new_state = state.copy()
|
|
52
68
|
new_state.src = text
|
|
53
69
|
children = inline.render(new_state)
|
|
54
70
|
state.append_token({"type": "inline_spoiler", "children": children})
|
|
55
|
-
return
|
|
71
|
+
return m2.end()
|
|
56
72
|
|
|
57
73
|
|
|
58
74
|
def render_block_spoiler(renderer: "BaseRenderer", text: str) -> str:
|
|
@@ -34,11 +34,14 @@ def render_list_item(
|
|
|
34
34
|
|
|
35
35
|
leading = cast(str, parent["leading"]) + marker
|
|
36
36
|
text = ""
|
|
37
|
+
prev = None
|
|
37
38
|
for tok in item["children"]:
|
|
38
39
|
if tok["type"] == "list":
|
|
39
40
|
tok["parent"] = parent
|
|
40
41
|
elif tok["type"] == "blank_line":
|
|
41
42
|
continue
|
|
43
|
+
tok["prev"] = prev
|
|
44
|
+
prev = tok
|
|
42
45
|
text += renderer.render_token(tok, state)
|
|
43
46
|
|
|
44
47
|
lines = text.splitlines()
|