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.
Files changed (134) hide show
  1. {mistune-3.3.2 → mistune-3.3.3}/PKG-INFO +1 -1
  2. {mistune-3.3.2 → mistune-3.3.3}/docs/changes.rst +17 -0
  3. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/__init__.py +1 -1
  4. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/core.py +7 -0
  5. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/inline_parser.py +162 -73
  6. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/footnotes.py +6 -1
  7. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/formatting.py +6 -0
  8. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/ruby.py +16 -12
  9. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/spoiler.py +21 -5
  10. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/renderers/_list.py +3 -0
  11. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/renderers/html.py +33 -21
  12. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/renderers/markdown.py +35 -6
  13. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/renderers/rst.py +1 -0
  14. {mistune-3.3.2 → mistune-3.3.3}/src/mistune.egg-info/PKG-INFO +1 -1
  15. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/fix-commonmark.txt +15 -0
  16. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/renderer_rst.txt +16 -0
  17. {mistune-3.3.2 → mistune-3.3.3}/tests/test_commonmark.py +4 -4
  18. {mistune-3.3.2 → mistune-3.3.3}/tests/test_misc.py +12 -0
  19. {mistune-3.3.2 → mistune-3.3.3}/tests/test_renderers.py +66 -0
  20. {mistune-3.3.2 → mistune-3.3.3}/tests/test_security_formatting.py +22 -0
  21. mistune-3.3.3/tests/test_security_inline.py +156 -0
  22. {mistune-3.3.2 → mistune-3.3.3}/tests/test_security_math.py +19 -0
  23. mistune-3.3.3/tests/test_security_urls.py +76 -0
  24. mistune-3.3.2/tests/test_security_inline.py +0 -37
  25. mistune-3.3.2/tests/test_security_urls.py +0 -20
  26. {mistune-3.3.2 → mistune-3.3.3}/LICENSE +0 -0
  27. {mistune-3.3.2 → mistune-3.3.3}/MANIFEST.in +0 -0
  28. {mistune-3.3.2 → mistune-3.3.3}/README.rst +0 -0
  29. {mistune-3.3.2 → mistune-3.3.3}/benchmark/bench.py +0 -0
  30. {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/atx.txt +0 -0
  31. {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/auto_links.txt +0 -0
  32. {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/blockcode.txt +0 -0
  33. {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/blockhtml.txt +0 -0
  34. {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/blockquote.txt +0 -0
  35. {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/emphasis.txt +0 -0
  36. {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/fenced.txt +0 -0
  37. {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/insane_ol.txt +0 -0
  38. {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/insane_ul.txt +0 -0
  39. {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/normal_ol.txt +0 -0
  40. {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/normal_ul.txt +0 -0
  41. {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/paragraph.txt +0 -0
  42. {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/ref_links.txt +0 -0
  43. {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/setext.txt +0 -0
  44. {mistune-3.3.2 → mistune-3.3.3}/benchmark/cases/std_links.txt +0 -0
  45. {mistune-3.3.2 → mistune-3.3.3}/docs/Makefile +0 -0
  46. {mistune-3.3.2 → mistune-3.3.3}/docs/_static/dark-icon.svg +0 -0
  47. {mistune-3.3.2 → mistune-3.3.3}/docs/_static/light-icon.svg +0 -0
  48. {mistune-3.3.2 → mistune-3.3.3}/docs/_static/logo-black.svg +0 -0
  49. {mistune-3.3.2 → mistune-3.3.3}/docs/_static/logo-white.svg +0 -0
  50. {mistune-3.3.2 → mistune-3.3.3}/docs/_templates/partials/extra-foot.html +0 -0
  51. {mistune-3.3.2 → mistune-3.3.3}/docs/_templates/partials/globaltoc-above.html +0 -0
  52. {mistune-3.3.2 → mistune-3.3.3}/docs/advanced.rst +0 -0
  53. {mistune-3.3.2 → mistune-3.3.3}/docs/api.rst +0 -0
  54. {mistune-3.3.2 → mistune-3.3.3}/docs/cli.rst +0 -0
  55. {mistune-3.3.2 → mistune-3.3.3}/docs/community.rst +0 -0
  56. {mistune-3.3.2 → mistune-3.3.3}/docs/conf.py +0 -0
  57. {mistune-3.3.2 → mistune-3.3.3}/docs/directives.rst +0 -0
  58. {mistune-3.3.2 → mistune-3.3.3}/docs/guide.rst +0 -0
  59. {mistune-3.3.2 → mistune-3.3.3}/docs/index.rst +0 -0
  60. {mistune-3.3.2 → mistune-3.3.3}/docs/plugins.rst +0 -0
  61. {mistune-3.3.2 → mistune-3.3.3}/docs/renderers.rst +0 -0
  62. {mistune-3.3.2 → mistune-3.3.3}/docs/upgrade.rst +0 -0
  63. {mistune-3.3.2 → mistune-3.3.3}/pyproject.toml +0 -0
  64. {mistune-3.3.2 → mistune-3.3.3}/setup.cfg +0 -0
  65. {mistune-3.3.2 → mistune-3.3.3}/setup.py +0 -0
  66. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/__main__.py +0 -0
  67. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/block_parser.py +0 -0
  68. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/directives/__init__.py +0 -0
  69. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/directives/_base.py +0 -0
  70. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/directives/_fenced.py +0 -0
  71. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/directives/_rst.py +0 -0
  72. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/directives/admonition.py +0 -0
  73. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/directives/image.py +0 -0
  74. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/directives/include.py +0 -0
  75. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/directives/toc.py +0 -0
  76. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/helpers.py +0 -0
  77. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/list_parser.py +0 -0
  78. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/markdown.py +0 -0
  79. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/__init__.py +0 -0
  80. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/abbr.py +0 -0
  81. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/def_list.py +0 -0
  82. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/math.py +0 -0
  83. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/speedup.py +0 -0
  84. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/table.py +0 -0
  85. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/task_lists.py +0 -0
  86. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/plugins/url.py +0 -0
  87. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/py.typed +0 -0
  88. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/renderers/__init__.py +0 -0
  89. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/toc.py +0 -0
  90. {mistune-3.3.2 → mistune-3.3.3}/src/mistune/util.py +0 -0
  91. {mistune-3.3.2 → mistune-3.3.3}/src/mistune.egg-info/SOURCES.txt +0 -0
  92. {mistune-3.3.2 → mistune-3.3.3}/src/mistune.egg-info/dependency_links.txt +0 -0
  93. {mistune-3.3.2 → mistune-3.3.3}/src/mistune.egg-info/entry_points.txt +0 -0
  94. {mistune-3.3.2 → mistune-3.3.3}/src/mistune.egg-info/requires.txt +0 -0
  95. {mistune-3.3.2 → mistune-3.3.3}/src/mistune.egg-info/top_level.txt +0 -0
  96. {mistune-3.3.2 → mistune-3.3.3}/tests/__init__.py +0 -0
  97. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/__init__.py +0 -0
  98. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/abbr.txt +0 -0
  99. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/cli.txt +0 -0
  100. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/commonmark.json +0 -0
  101. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/def_list.txt +0 -0
  102. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/fenced_admonition.txt +0 -0
  103. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/fenced_figure.txt +0 -0
  104. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/fenced_image.txt +0 -0
  105. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/fenced_toc.txt +0 -0
  106. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/footnotes.txt +0 -0
  107. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/hook_toc.txt +0 -0
  108. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/include/hello.md +0 -0
  109. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/include/hello.txt +0 -0
  110. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/include/text.html +0 -0
  111. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/include/text.md +0 -0
  112. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/insert.txt +0 -0
  113. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/mark.txt +0 -0
  114. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/math.txt +0 -0
  115. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/renderer_markdown.txt +0 -0
  116. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/rst_admonition.txt +0 -0
  117. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/rst_toc.txt +0 -0
  118. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/ruby.txt +0 -0
  119. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/spoiler.txt +0 -0
  120. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/strikethrough.txt +0 -0
  121. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/subscript.txt +0 -0
  122. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/superscript.txt +0 -0
  123. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/table.txt +0 -0
  124. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/task_lists.txt +0 -0
  125. {mistune-3.3.2 → mistune-3.3.3}/tests/fixtures/url.txt +0 -0
  126. {mistune-3.3.2 → mistune-3.3.3}/tests/test_cli.py +0 -0
  127. {mistune-3.3.2 → mistune-3.3.3}/tests/test_directives.py +0 -0
  128. {mistune-3.3.2 → mistune-3.3.3}/tests/test_hooks.py +0 -0
  129. {mistune-3.3.2 → mistune-3.3.3}/tests/test_plugins.py +0 -0
  130. {mistune-3.3.2 → mistune-3.3.3}/tests/test_security_image.py +0 -0
  131. {mistune-3.3.2 → mistune-3.3.3}/tests/test_security_include.py +0 -0
  132. {mistune-3.3.2 → mistune-3.3.3}/tests/test_security_ref_links.py +0 -0
  133. {mistune-3.3.2 → mistune-3.3.3}/tests/test_security_toc.py +0 -0
  134. {mistune-3.3.2 → mistune-3.3.3}/tests/test_syntax.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mistune
3
- Version: 3.3.2
3
+ Version: 3.3.3
4
4
  Summary: A sane and fast Markdown parser with useful plugins and renderers
5
5
  Author-email: Hsiaoming Yang <me@lepture.com>
6
6
  License: BSD-3-Clause
@@ -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
 
@@ -97,5 +97,5 @@ __all__ = [
97
97
  "markdown",
98
98
  ]
99
99
 
100
- __version__ = "3.3.2"
100
+ __version__ = "3.3.3"
101
101
  __homepage__ = "https://mistune.lepture.com/"
@@ -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__(self, hard_wrap: bool = False) -> None:
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
- text, end_pos = _parse_link_text(state, pos)
136
- if text is None:
137
- if end_pos > state.no_close_bracket_before:
138
- state.no_close_bracket_before = end_pos
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 text is None:
164
+ if label is not None:
144
165
  text = label
166
+ text_start = pos
167
+ text_end = end_pos - 1
145
168
 
146
- assert text is not None
169
+ body_end_pos = end_pos
147
170
 
148
- if end_pos >= len(state.src) and label is None:
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
- if not is_image and self._contains_nested_link(text, state):
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
- return None
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(state.tokens, "emphasis" in self.rules)
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(tokens: List[Dict[str, Any]], enabled: bool) -> List[Dict[str, Any]]:
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(parts: List[Dict[str, Any]], delimiters: List[_Delimiter]) -> None:
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
- return (opener.length + closer.length) % 3 != 0 or opener.length % 3 == 0 and closer.length % 3 == 0
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 _parse_link_text(state: InlineState, pos: int) -> Tuple[Optional[str], int]:
794
- close_pos = _find_closing_bracket(state, pos)
795
- if close_pos is None:
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].get(pos)
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.get(pos)
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
- assert label is not None
818
- assert end_pos is not None
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
- if end_pos >= len(src):
821
- ref_links = state.env.get("ref_links")
822
- return bool(ref_links and unikey(label) in ref_links)
823
-
824
- marker = src[end_pos]
825
- if marker == "(":
826
- _attrs, new_pos = parse_link(src, end_pos + 1)
827
- return bool(new_pos)
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
- if marker == "[":
830
- label2, new_pos = parse_link_label(src, end_pos + 1)
831
- if not new_pos:
832
- return False
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
- return bool(ref_links and unikey(label) in ref_links)
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": notes.index(key) + 1}})
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
- text = m.group(0)[1:-2]
19
- items = text.split(")")
20
- tokens = []
21
- for item in items:
22
- rb, rt = item.split("(")
23
- tokens.append({"type": "ruby", "raw": rb, "attrs": {"rt": rt}})
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
- return parse_ruby(inline, next_match, state)
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">!\s*(?P<spoiler_text>.+?)\s*!<"
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
- text = m.group("spoiler_text")
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 m.end()
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()