mdit-py-plugins 0.6.0__tar.gz → 0.7.0__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.
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/PKG-INFO +4 -4
- mdit_py_plugins-0.7.0/mdit_py_plugins/__init__.py +1 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/admon/index.py +7 -2
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/anchors/index.py +17 -4
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/attrs/index.py +95 -15
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/attrs/parse.py +16 -2
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/colon_fence.py +1 -1
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/container/index.py +2 -2
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/dollarmath/index.py +54 -11
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/field_list/__init__.py +3 -2
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/footnote/index.py +2 -2
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/front_matter/index.py +10 -3
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/gfm_autolink/index.py +6 -3
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/myst_blocks/index.py +1 -1
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/myst_role/index.py +3 -8
- mdit_py_plugins-0.7.0/mdit_py_plugins/section_ref/__init__.py +12 -0
- mdit_py_plugins-0.7.0/mdit_py_plugins/section_ref/index.py +137 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/tasklists/__init__.py +3 -1
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/texmath/index.py +53 -33
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/pyproject.toml +5 -3
- mdit_py_plugins-0.6.0/.coveragerc +0 -4
- mdit_py_plugins-0.6.0/.github/dependabot.yml +0 -19
- mdit_py_plugins-0.6.0/.github/workflows/copilot-setup-steps.yml +0 -43
- mdit_py_plugins-0.6.0/.github/workflows/tests.yml +0 -87
- mdit_py_plugins-0.6.0/.gitignore +0 -143
- mdit_py_plugins-0.6.0/.pre-commit-config.yaml +0 -34
- mdit_py_plugins-0.6.0/.readthedocs.yml +0 -17
- mdit_py_plugins-0.6.0/AGENTS.md +0 -373
- mdit_py_plugins-0.6.0/CHANGELOG.md +0 -155
- mdit_py_plugins-0.6.0/codecov.yml +0 -10
- mdit_py_plugins-0.6.0/mdit_py_plugins/__init__.py +0 -1
- mdit_py_plugins-0.6.0/tox.ini +0 -30
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/LICENSE +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/README.md +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/admon/LICENSE +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/admon/__init__.py +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/admon/port.yaml +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/amsmath/__init__.py +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/anchors/__init__.py +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/attrs/__init__.py +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/container/LICENSE +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/container/README.md +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/container/__init__.py +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/container/port.yaml +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/deflist/LICENSE +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/deflist/README.md +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/deflist/__init__.py +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/deflist/index.py +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/deflist/port.yaml +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/dollarmath/__init__.py +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/footnote/LICENSE +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/footnote/__init__.py +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/footnote/port.yaml +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/front_matter/LICENSE +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/front_matter/__init__.py +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/front_matter/port.yaml +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/gfm/__init__.py +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/gfm_autolink/__init__.py +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/gfm_autolink/_match.py +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/myst_blocks/__init__.py +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/myst_role/__init__.py +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/py.typed +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/subscript/__init__.py +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/subscript/port.yaml +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/substitution.py +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/superscript/__init__.py +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/superscript/index.py +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/tasklists/port.yaml +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/texmath/LICENSE +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/texmath/README.md +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/texmath/__init__.py +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/texmath/port.yaml +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/utils.py +0 -0
- {mdit_py_plugins-0.6.0 → mdit_py_plugins-0.7.0}/mdit_py_plugins/wordcount/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mdit-py-plugins
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Collection of plugins for markdown-it-py
|
|
5
5
|
Keywords: markdown,markdown-it,lexer,parser,development
|
|
6
6
|
Author-email: Chris Sewell <chrisj_sewell@hotmail.com>
|
|
@@ -10,17 +10,17 @@ Classifier: Development Status :: 5 - Production/Stable
|
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
15
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
18
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
19
19
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
20
20
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
21
|
Classifier: Topic :: Text Processing :: Markup
|
|
22
22
|
License-File: LICENSE
|
|
23
|
-
Requires-Dist: markdown-it-py>=
|
|
23
|
+
Requires-Dist: markdown-it-py>=3.0.0,<5.0.0
|
|
24
24
|
Requires-Dist: pre-commit ; extra == "code-style"
|
|
25
25
|
Requires-Dist: myst-parser ; extra == "rtd"
|
|
26
26
|
Requires-Dist: sphinx-book-theme ; extra == "rtd"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.7.0"
|
|
@@ -20,7 +20,12 @@ if TYPE_CHECKING:
|
|
|
20
20
|
|
|
21
21
|
def _get_multiple_tags(params: str) -> tuple[list[str], str]:
|
|
22
22
|
"""Check for multiple tags when the title is double quoted."""
|
|
23
|
-
|
|
23
|
+
# This matches what r'^\s*(?P<tokens>[^"]+)\s+"(?P<title>.*)"\S*$' matches,
|
|
24
|
+
# but the tokens end on a non-space and only the last quote can close the title,
|
|
25
|
+
# so a long run of spaces or quotes cannot make it backtrack quadratically (#143).
|
|
26
|
+
re_tags = re.compile(
|
|
27
|
+
r'^\s*(?P<tokens>[^"\s](?:[^"]*[^"\s])?)\s+"(?P<title>.*)"[^\s"]*$'
|
|
28
|
+
)
|
|
24
29
|
match = re_tags.match(params)
|
|
25
30
|
if match:
|
|
26
31
|
tags = match["tokens"].strip().split(" ")
|
|
@@ -183,7 +188,7 @@ def admonition(state: StateBlock, startLine: int, endLine: int, silent: bool) ->
|
|
|
183
188
|
return True
|
|
184
189
|
|
|
185
190
|
|
|
186
|
-
def admon_plugin(md: MarkdownIt, render:
|
|
191
|
+
def admon_plugin(md: MarkdownIt, render: Callable[..., str] | None = None) -> None:
|
|
187
192
|
"""Plugin to use
|
|
188
193
|
`python-markdown style admonitions
|
|
189
194
|
<https://python-markdown.github.io/extensions/admonition>`_.
|
|
@@ -29,6 +29,10 @@ def anchors_plugin(
|
|
|
29
29
|
|
|
30
30
|
<h1 id="title-string">Title String <a class="header-anchor" href="#title-string">¶</a></h1>
|
|
31
31
|
|
|
32
|
+
Explicit heading IDs (e.g. ``{#id}`` from ``attrs_block_plugin``) are preserved,
|
|
33
|
+
at any level, and generated IDs avoid collisions with them.
|
|
34
|
+
Explicit IDs are not de-duplicated, and an empty explicit ID counts as no ID.
|
|
35
|
+
|
|
32
36
|
:param min_level: minimum header level to apply anchors
|
|
33
37
|
:param max_level: maximum header level to apply anchors
|
|
34
38
|
:param slug_func: function to convert title text to id slug.
|
|
@@ -66,7 +70,14 @@ def _make_anchors_func(
|
|
|
66
70
|
permalinkSpace: bool,
|
|
67
71
|
) -> Callable[[StateCore], None]:
|
|
68
72
|
def _anchor_func(state: StateCore) -> None:
|
|
69
|
-
|
|
73
|
+
# reserve every explicit heading id (at any level) up front,
|
|
74
|
+
# so a generated slug never collides with one, whatever the order;
|
|
75
|
+
# an empty id is treated as absent
|
|
76
|
+
slugs: set[str] = {
|
|
77
|
+
str(heading_id)
|
|
78
|
+
for token in state.tokens
|
|
79
|
+
if token.type == "heading_open" and (heading_id := token.attrGet("id"))
|
|
80
|
+
}
|
|
70
81
|
for idx, token in enumerate(state.tokens):
|
|
71
82
|
if token.type != "heading_open":
|
|
72
83
|
continue
|
|
@@ -80,8 +91,10 @@ def _make_anchors_func(
|
|
|
80
91
|
for child in inline_token.children
|
|
81
92
|
if child.type in ["text", "code_inline"]
|
|
82
93
|
)
|
|
83
|
-
slug =
|
|
84
|
-
|
|
94
|
+
slug = token.attrGet("id")
|
|
95
|
+
if not slug:
|
|
96
|
+
slug = unique_slug(slug_func(title), slugs)
|
|
97
|
+
token.attrSet("id", slug)
|
|
85
98
|
|
|
86
99
|
if permalink:
|
|
87
100
|
link_open = Token(
|
|
@@ -116,7 +129,7 @@ def _make_anchors_func(
|
|
|
116
129
|
|
|
117
130
|
|
|
118
131
|
def slugify(title: str) -> str:
|
|
119
|
-
return re.sub(r"[^\w\u4e00-\u9fff\- ]", "", title.
|
|
132
|
+
return re.sub(r"[^\w\u4e00-\u9fff\- ]", "", title.lower().replace(" ", "-"))
|
|
120
133
|
|
|
121
134
|
|
|
122
135
|
def unique_slug(slug: str, slugs: set[str]) -> str:
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from collections.abc import Sequence
|
|
4
4
|
from functools import partial
|
|
5
|
+
import re
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
8
|
from markdown_it import MarkdownIt
|
|
@@ -41,8 +42,14 @@ def attrs_plugin(
|
|
|
41
42
|
- `key="value"` or `key=value` specifies a key-value attribute.
|
|
42
43
|
Quotes are not needed when the value consists entirely of
|
|
43
44
|
ASCII alphanumeric characters or `_` or `:` or `-`.
|
|
44
|
-
Backslash escapes may be used inside quoted values
|
|
45
|
-
|
|
45
|
+
Backslash escapes may be used inside quoted values,
|
|
46
|
+
to allow a ``"`` character within the value.
|
|
47
|
+
Note that the backslash is retained in the value:
|
|
48
|
+
``{k="a\\"b"}`` gives ``k`` the value ``a\\"b``.
|
|
49
|
+
- `%` begins a comment, which ends with the next `%`.
|
|
50
|
+
If no further `%` occurs in the rest of the line (for a block attribute)
|
|
51
|
+
or of the inline content (for an inline attribute),
|
|
52
|
+
the comment instead ends at the next `}`, which also ends the attribute.
|
|
46
53
|
|
|
47
54
|
Multiple attribute blocks are merged.
|
|
48
55
|
|
|
@@ -56,10 +63,24 @@ def attrs_plugin(
|
|
|
56
63
|
:param allowed: A list of allowed attribute names.
|
|
57
64
|
If not ``None``, any attributes not in this list will be removed
|
|
58
65
|
and placed in the token's meta under the key "insecure_attrs".
|
|
66
|
+
If ``None`` (the default), event-handler attributes
|
|
67
|
+
(any name starting with ``on``, such as ``onclick``) are still removed,
|
|
68
|
+
as a baseline protection against script injection.
|
|
69
|
+
Other attributes, including ``style``, are kept.
|
|
70
|
+
Whatever ``allowed`` says, the URL-valued attributes
|
|
71
|
+
``href``, ``src``, ``action``, ``formaction`` and ``xlink:href``
|
|
72
|
+
are removed when ``md.validateLink`` rejects their value
|
|
73
|
+
(for example a ``javascript:`` URL), as for Markdown links and images.
|
|
74
|
+
This is not a full sanitiser: to safely render untrusted input,
|
|
75
|
+
pass an explicit ``allowed`` list (e.g. ``("id", "class")``).
|
|
59
76
|
"""
|
|
60
77
|
|
|
61
78
|
if spans:
|
|
62
|
-
md.inline.ruler.after(
|
|
79
|
+
md.inline.ruler.after(
|
|
80
|
+
span_after,
|
|
81
|
+
"span",
|
|
82
|
+
partial(_span_rule, allowed=None if allowed is None else set(allowed)),
|
|
83
|
+
)
|
|
63
84
|
if after:
|
|
64
85
|
md.inline.ruler.push(
|
|
65
86
|
"attr",
|
|
@@ -91,6 +112,16 @@ def attrs_block_plugin(md: MarkdownIt, *, allowed: Sequence[str] | None = None)
|
|
|
91
112
|
:param allowed: A list of allowed attribute names.
|
|
92
113
|
If not ``None``, any attributes not in this list will be removed
|
|
93
114
|
and placed in the token's meta under the key "insecure_attrs".
|
|
115
|
+
If ``None`` (the default), event-handler attributes
|
|
116
|
+
(any name starting with ``on``, such as ``onclick``) are still removed,
|
|
117
|
+
as a baseline protection against script injection.
|
|
118
|
+
Other attributes, including ``style``, are kept.
|
|
119
|
+
Whatever ``allowed`` says, the URL-valued attributes
|
|
120
|
+
``href``, ``src``, ``action``, ``formaction`` and ``xlink:href``
|
|
121
|
+
are removed when ``md.validateLink`` rejects their value
|
|
122
|
+
(for example a ``javascript:`` URL), as for Markdown links and images.
|
|
123
|
+
This is not a full sanitiser: to safely render untrusted input,
|
|
124
|
+
pass an explicit ``allowed`` list (e.g. ``("id", "class")``).
|
|
94
125
|
"""
|
|
95
126
|
md.block.ruler.before("fence", "attr", _attr_block_rule)
|
|
96
127
|
md.core.ruler.after(
|
|
@@ -115,7 +146,9 @@ def _find_opening(tokens: Sequence[Token], index: int) -> int | None:
|
|
|
115
146
|
return None
|
|
116
147
|
|
|
117
148
|
|
|
118
|
-
def _span_rule(
|
|
149
|
+
def _span_rule(
|
|
150
|
+
state: StateInline, silent: bool, *, allowed: set[str] | None = None
|
|
151
|
+
) -> bool:
|
|
119
152
|
if state.src[state.pos] != "[":
|
|
120
153
|
return False
|
|
121
154
|
|
|
@@ -144,7 +177,7 @@ def _span_rule(state: StateInline, silent: bool) -> bool:
|
|
|
144
177
|
state.pos = labelStart
|
|
145
178
|
state.posMax = labelEnd
|
|
146
179
|
token = state.push("span_open", "span", 1)
|
|
147
|
-
|
|
180
|
+
_add_attrs(state.md, token, attrs, allowed)
|
|
148
181
|
state.md.inline.tokenize(state)
|
|
149
182
|
token = state.push("span_close", "span", -1)
|
|
150
183
|
|
|
@@ -175,9 +208,9 @@ def _attr_inline_rule(
|
|
|
175
208
|
state.pos += new_pos + 1
|
|
176
209
|
if not silent:
|
|
177
210
|
attr_token = state.tokens[token_index]
|
|
178
|
-
if "class" in attrs and "class" in
|
|
179
|
-
attrs["class"] = f"{
|
|
180
|
-
_add_attrs(attr_token, attrs, allowed)
|
|
211
|
+
if "class" in attrs and "class" in attr_token.attrs:
|
|
212
|
+
attrs["class"] = f"{attr_token.attrs['class']} {attrs['class']}"
|
|
213
|
+
_add_attrs(state.md, attr_token, attrs, allowed)
|
|
181
214
|
return True
|
|
182
215
|
|
|
183
216
|
|
|
@@ -238,7 +271,8 @@ def _attr_resolve_block_rule(state: StateCore, *, allowed: set[str] | None) -> N
|
|
|
238
271
|
i += 1
|
|
239
272
|
continue
|
|
240
273
|
|
|
241
|
-
|
|
274
|
+
# skip closing tokens; attributes would be rendered into the closing tag
|
|
275
|
+
if i + 1 < len_tokens and state.tokens[i + 1].nesting >= 0:
|
|
242
276
|
next_token = state.tokens[i + 1]
|
|
243
277
|
|
|
244
278
|
# classes are appended
|
|
@@ -253,23 +287,69 @@ def _attr_resolve_block_rule(state: StateCore, *, allowed: set[str] | None) -> N
|
|
|
253
287
|
if key == "class" or key not in next_token.attrs:
|
|
254
288
|
next_token.attrs[key] = value
|
|
255
289
|
else:
|
|
256
|
-
_add_attrs(next_token, state.tokens[i].attrs, allowed)
|
|
290
|
+
_add_attrs(state.md, next_token, state.tokens[i].attrs, allowed)
|
|
257
291
|
|
|
258
292
|
state.tokens.pop(i)
|
|
259
293
|
len_tokens -= 1
|
|
260
294
|
|
|
261
295
|
|
|
296
|
+
_URL_ATTRS = frozenset(("href", "src", "action", "formaction", "xlink:href"))
|
|
297
|
+
"""Attributes whose value is a URL, checked with ``md.validateLink``."""
|
|
298
|
+
|
|
299
|
+
_URL_IGNORED_RE = re.compile(r"[\t\n\r]")
|
|
300
|
+
_URL_STRIP = "".join(chr(i) for i in range(0x21))
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _is_insecure_attr(key: str) -> bool:
|
|
304
|
+
"""Return True for event-handler attributes (``on*``, e.g. ``onclick``).
|
|
305
|
+
|
|
306
|
+
They execute JavaScript, so they are removed by default
|
|
307
|
+
when no explicit ``allowed`` list is given.
|
|
308
|
+
"""
|
|
309
|
+
return key.lower().startswith("on")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _is_valid_url(md: MarkdownIt, value: Any) -> bool:
|
|
313
|
+
"""Return True if ``md.validateLink`` accepts a URL-valued attribute.
|
|
314
|
+
|
|
315
|
+
Browsers ignore ASCII tabs and newlines anywhere in a URL,
|
|
316
|
+
and control characters and spaces around it, before reading its scheme
|
|
317
|
+
(so ``java<TAB>script:`` is a ``javascript:`` URL).
|
|
318
|
+
The value is emitted as written, so those characters are removed
|
|
319
|
+
before it is normalised and validated.
|
|
320
|
+
"""
|
|
321
|
+
url = _URL_IGNORED_RE.sub("", str(value)).strip(_URL_STRIP)
|
|
322
|
+
return md.validateLink(md.normalizeLink(url))
|
|
323
|
+
|
|
324
|
+
|
|
262
325
|
def _add_attrs(
|
|
326
|
+
md: MarkdownIt,
|
|
263
327
|
token: Token,
|
|
264
328
|
attrs: dict[str, Any],
|
|
265
329
|
allowed: set[str] | None,
|
|
266
330
|
) -> None:
|
|
267
|
-
"""Add attributes to a token, skipping any disallowed attributes.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
)
|
|
331
|
+
"""Add attributes to a token, skipping any disallowed attributes.
|
|
332
|
+
|
|
333
|
+
When ``allowed`` is not ``None`` only those names are kept.
|
|
334
|
+
When it is ``None`` (the default) event-handler (``on*``) attributes
|
|
335
|
+
are still removed, so untrusted input cannot inject script.
|
|
336
|
+
In both cases, a URL-valued attribute (``href``, ``src``, ...)
|
|
337
|
+
is removed if ``md.validateLink`` rejects it.
|
|
338
|
+
Anything removed is preserved in ``token.meta["insecure_attrs"]``.
|
|
339
|
+
"""
|
|
340
|
+
if allowed is not None:
|
|
341
|
+
disallowed = {k: v for k, v in attrs.items() if k not in allowed}
|
|
342
|
+
else:
|
|
343
|
+
disallowed = {k: v for k, v in attrs.items() if _is_insecure_attr(k)}
|
|
344
|
+
disallowed.update(
|
|
345
|
+
(k, v)
|
|
346
|
+
for k, v in attrs.items()
|
|
347
|
+
if k not in disallowed and k.lower() in _URL_ATTRS and not _is_valid_url(md, v)
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
if disallowed:
|
|
271
351
|
token.meta["insecure_attrs"] = disallowed
|
|
272
|
-
attrs = {k: v for k, v in attrs.items() if k in
|
|
352
|
+
attrs = {k: v for k, v in attrs.items() if k not in disallowed}
|
|
273
353
|
|
|
274
354
|
# attributes takes precedence over existing attributes
|
|
275
355
|
token.attrs.update(attrs)
|
|
@@ -100,17 +100,31 @@ def parse(string: str) -> tuple[int, dict[str, str]]:
|
|
|
100
100
|
"""Parse attributes from start of string.
|
|
101
101
|
|
|
102
102
|
:returns: (length of parsed string, dict of attributes)
|
|
103
|
+
:raises ParseError: if the attributes are malformed,
|
|
104
|
+
or the string ends before the closing `}`
|
|
103
105
|
"""
|
|
104
106
|
pos = 0
|
|
105
107
|
state: State = State.START
|
|
106
108
|
tokens = TokenState()
|
|
109
|
+
# found once, so that checking for a later `%` at each `}` does not
|
|
110
|
+
# rescan the rest of the string (which would be quadratic in its length)
|
|
111
|
+
last_percent = string.rfind("%")
|
|
107
112
|
while pos < len(string):
|
|
108
|
-
|
|
113
|
+
if (
|
|
114
|
+
state == State.SCANNING_COMMENT
|
|
115
|
+
and string[pos] == "}"
|
|
116
|
+
and pos > last_percent
|
|
117
|
+
):
|
|
118
|
+
# a comment ends at the next `%`, but there is none left to close it,
|
|
119
|
+
# so this `}` ends both the comment and the attributes
|
|
120
|
+
state = State.DONE
|
|
121
|
+
else:
|
|
122
|
+
state = HANDLERS[state](string[pos], pos, tokens)
|
|
109
123
|
if state == State.DONE:
|
|
110
124
|
return pos, tokens.compile(string)
|
|
111
125
|
pos = pos + 1
|
|
112
126
|
|
|
113
|
-
|
|
127
|
+
raise ParseError("Attributes not terminated", pos)
|
|
114
128
|
|
|
115
129
|
|
|
116
130
|
def handle_start(char: str, pos: int, tokens: TokenState) -> State:
|
|
@@ -21,8 +21,8 @@ def container_plugin(
|
|
|
21
21
|
md: MarkdownIt,
|
|
22
22
|
name: str,
|
|
23
23
|
marker: str = ":",
|
|
24
|
-
validate:
|
|
25
|
-
render:
|
|
24
|
+
validate: Callable[[str, str], bool] | None = None,
|
|
25
|
+
render: Callable[..., str] | None = None,
|
|
26
26
|
) -> None:
|
|
27
27
|
"""Plugin ported from
|
|
28
28
|
`markdown-it-container <https://github.com/markdown-it/markdown-it-container>`__.
|
|
@@ -49,9 +49,28 @@ def dollarmath_plugin(
|
|
|
49
49
|
:param label_normalizer: Function to normalize the label,
|
|
50
50
|
by default replaces whitespace with `-`
|
|
51
51
|
:param renderer: Function to render content: `(str, {"display_mode": bool}) -> str`,
|
|
52
|
-
by default escapes HTML
|
|
52
|
+
by default escapes HTML.
|
|
53
|
+
Its return value is placed inside the element as is,
|
|
54
|
+
so a custom renderer is responsible for escaping it (the default renderer does).
|
|
53
55
|
:param label_renderer: Function to render labels, by default creates anchor
|
|
54
56
|
|
|
57
|
+
The plugin does not typeset the math.
|
|
58
|
+
Each token becomes an HTML element holding the rendered content,
|
|
59
|
+
and typesetting is left to a library such as MathJax or KaTeX:
|
|
60
|
+
|
|
61
|
+
- ``math_inline`` (``$a$``):
|
|
62
|
+
``<span class="math inline">``, with ``display_mode=False``
|
|
63
|
+
- ``math_inline_double`` (``$$a$$`` inside a paragraph, with ``double_inline``):
|
|
64
|
+
``<div class="math inline">``, with ``display_mode=True``
|
|
65
|
+
- ``math_block`` (``$$a$$`` on its own lines):
|
|
66
|
+
``<div class="math block">``, with ``display_mode=True``
|
|
67
|
+
- ``math_block_label`` (``$$a$$ (eq1)``):
|
|
68
|
+
``<div id="eq1" class="math block">``, starting with the label anchor,
|
|
69
|
+
with ``display_mode=True``
|
|
70
|
+
|
|
71
|
+
The tag says how to display the math (``div`` for display, ``span`` for inline),
|
|
72
|
+
and the class says where it was written (``inline`` inside a paragraph,
|
|
73
|
+
``block`` on its own lines), as in executablebooks/markdown-it-dollarmath.
|
|
55
74
|
"""
|
|
56
75
|
if label_normalizer is None:
|
|
57
76
|
label_normalizer = lambda label: re.sub(r"\s+", "-", label) # noqa: E731
|
|
@@ -79,7 +98,7 @@ def dollarmath_plugin(
|
|
|
79
98
|
if label_renderer is None:
|
|
80
99
|
_label_renderer = ( # noqa: E731
|
|
81
100
|
lambda label: (
|
|
82
|
-
f'<a href="#{label}" class="mathlabel" title="Permalink to this equation">¶</a>'
|
|
101
|
+
f'<a href="#{escapeHtml(label)}" class="mathlabel" title="Permalink to this equation">¶</a>'
|
|
83
102
|
)
|
|
84
103
|
)
|
|
85
104
|
else:
|
|
@@ -123,7 +142,7 @@ def dollarmath_plugin(
|
|
|
123
142
|
env: EnvType,
|
|
124
143
|
) -> str:
|
|
125
144
|
content = _renderer(str(tokens[idx].content).strip(), {"display_mode": True})
|
|
126
|
-
_id = tokens[idx].info
|
|
145
|
+
_id = escapeHtml(tokens[idx].info)
|
|
127
146
|
label = _label_renderer(tokens[idx].info)
|
|
128
147
|
return f'<div id="{_id}" class="math block">\n{label}\n{content}\n</div>\n'
|
|
129
148
|
|
|
@@ -287,6 +306,22 @@ def math_inline_dollar(
|
|
|
287
306
|
DOLLAR_EQNO_REV = re.compile(r"^\s*\)([^)$\r\n]+?)\(\s*\${2}")
|
|
288
307
|
|
|
289
308
|
|
|
309
|
+
def _math_block_content(
|
|
310
|
+
state: StateBlock, startLine: int, startPos: int, endLine: int, end: int
|
|
311
|
+
) -> str:
|
|
312
|
+
"""Extract math body, omitting parent container prefixes on each line."""
|
|
313
|
+
content_start = startPos + 2
|
|
314
|
+
if startLine == endLine:
|
|
315
|
+
return state.src[content_start:end]
|
|
316
|
+
|
|
317
|
+
parts: list[str] = [state.src[content_start : state.eMarks[startLine]], "\n"]
|
|
318
|
+
for line in range(startLine + 1, endLine):
|
|
319
|
+
parts.append(state.src[state.bMarks[line] : state.eMarks[line]])
|
|
320
|
+
parts.append("\n")
|
|
321
|
+
parts.append(state.src[state.bMarks[endLine] : end])
|
|
322
|
+
return "".join(parts)
|
|
323
|
+
|
|
324
|
+
|
|
290
325
|
def math_block_dollar(
|
|
291
326
|
allow_labels: bool = True,
|
|
292
327
|
label_normalizer: Callable[[str], str] | None = None,
|
|
@@ -322,13 +357,21 @@ def math_block_dollar(
|
|
|
322
357
|
if lineText.strip().endswith("$$"):
|
|
323
358
|
haveEndMarker = True
|
|
324
359
|
end = end - 2 - (len(lineText) - len(lineText.strip()))
|
|
325
|
-
|
|
326
|
-
#
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
360
|
+
else:
|
|
361
|
+
# a closing marker present but not at end of line and not a label
|
|
362
|
+
# suffix is ambiguous, so don't greedily scan subsequent lines for it
|
|
363
|
+
trailing_close = "$$" in lineText[2:]
|
|
364
|
+
if allow_labels:
|
|
365
|
+
# reverse the line and match
|
|
366
|
+
eqnoMatch = DOLLAR_EQNO_REV.match(lineText[::-1])
|
|
367
|
+
if eqnoMatch:
|
|
368
|
+
haveEndMarker = True
|
|
369
|
+
label = eqnoMatch.group(1)[::-1]
|
|
370
|
+
end = end - eqnoMatch.end()
|
|
371
|
+
elif trailing_close:
|
|
372
|
+
return False
|
|
373
|
+
elif trailing_close:
|
|
374
|
+
return False
|
|
332
375
|
|
|
333
376
|
# search for end of block on subsequent line
|
|
334
377
|
if not haveEndMarker:
|
|
@@ -365,7 +408,7 @@ def math_block_dollar(
|
|
|
365
408
|
|
|
366
409
|
token = state.push("math_block_label" if label else "math_block", "math", 0)
|
|
367
410
|
token.block = True
|
|
368
|
-
token.content = state
|
|
411
|
+
token.content = _math_block_content(state, startLine, startPos, nextLine, end)
|
|
369
412
|
token.markup = "$$"
|
|
370
413
|
token.map = [startLine, state.line]
|
|
371
414
|
if label:
|
|
@@ -164,8 +164,9 @@ def _fieldlist_rule(
|
|
|
164
164
|
while _line < endLine:
|
|
165
165
|
# if start_of_content < end_of_content, then non-empty line
|
|
166
166
|
if (state.bMarks[_line] + state.tShift[_line]) < state.eMarks[_line]:
|
|
167
|
-
if state.tShift[_line] <=
|
|
168
|
-
# the line
|
|
167
|
+
if state.tShift[_line] <= state.blkIndent:
|
|
168
|
+
# the line is not indented relative to the field marker,
|
|
169
|
+
# so it's the end of the field body
|
|
169
170
|
break
|
|
170
171
|
block_indent = (
|
|
171
172
|
state.tShift[_line]
|
|
@@ -227,6 +227,8 @@ def footnote_inline(state: StateInline, silent: bool) -> bool:
|
|
|
227
227
|
footnoteId = len(refs)
|
|
228
228
|
|
|
229
229
|
tokens: list[Token] = []
|
|
230
|
+
# Reserve the ID before parsing content that may allocate more footnotes.
|
|
231
|
+
refs[footnoteId] = {"content": state.src[labelStart:labelEnd], "tokens": tokens}
|
|
230
232
|
state.md.inline.parse(
|
|
231
233
|
state.src[labelStart:labelEnd], state.md, state.env, tokens
|
|
232
234
|
)
|
|
@@ -234,8 +236,6 @@ def footnote_inline(state: StateInline, silent: bool) -> bool:
|
|
|
234
236
|
token = state.push("footnote_ref", "", 0)
|
|
235
237
|
token.meta = {"id": footnoteId}
|
|
236
238
|
|
|
237
|
-
refs[footnoteId] = {"content": state.src[labelStart:labelEnd], "tokens": tokens}
|
|
238
|
-
|
|
239
239
|
state.pos = labelEnd + 1
|
|
240
240
|
state.posMax = maximum
|
|
241
241
|
return True
|
|
@@ -69,12 +69,19 @@ def _front_matter_rule(
|
|
|
69
69
|
# unclosed block should be autoclosed by end of document.
|
|
70
70
|
return False
|
|
71
71
|
|
|
72
|
-
if state.src[start:maximum] == "...":
|
|
73
|
-
break
|
|
74
|
-
|
|
75
72
|
start = state.bMarks[nextLine] + state.tShift[nextLine]
|
|
76
73
|
maximum = state.eMarks[nextLine]
|
|
77
74
|
|
|
75
|
+
if (
|
|
76
|
+
state.sCount[nextLine] == 0
|
|
77
|
+
and state.src.startswith("...", start)
|
|
78
|
+
and state.skipSpaces(start + 3) >= maximum
|
|
79
|
+
):
|
|
80
|
+
# YAML document-end marker: `...` at column 0, trailing spaces allowed.
|
|
81
|
+
# It is checked on the current line and consumed, so it is not content.
|
|
82
|
+
auto_closed = True
|
|
83
|
+
break
|
|
84
|
+
|
|
78
85
|
if start < maximum and state.sCount[nextLine] < state.blkIndent:
|
|
79
86
|
# non-empty line with negative indent should stop the list:
|
|
80
87
|
# - ```
|
|
@@ -159,7 +159,10 @@ def _www_inline_rule(state: StateInline, silent: bool) -> bool:
|
|
|
159
159
|
label = src[pos : pos + length]
|
|
160
160
|
|
|
161
161
|
if silent:
|
|
162
|
-
|
|
162
|
+
# Autolinks are only recognised when emitting tokens. When skipping
|
|
163
|
+
# silently (e.g. scanning a link label), report no match so the parser
|
|
164
|
+
# steps over this text as plain characters (#160).
|
|
165
|
+
return False
|
|
163
166
|
|
|
164
167
|
full_url = state.md.normalizeLink(url)
|
|
165
168
|
if not state.md.validateLink(full_url):
|
|
@@ -223,7 +226,7 @@ def _protocol_rule(state: StateInline, silent: bool) -> bool:
|
|
|
223
226
|
label = combined[:total_len]
|
|
224
227
|
|
|
225
228
|
if silent:
|
|
226
|
-
return
|
|
229
|
+
return False # see _www_inline_rule
|
|
227
230
|
return _create_autolink(state, bscan_len, total_len, full_url, label)
|
|
228
231
|
|
|
229
232
|
|
|
@@ -291,5 +294,5 @@ def _email_rule(state: StateInline, silent: bool) -> bool:
|
|
|
291
294
|
url = "mailto:" + email_text
|
|
292
295
|
|
|
293
296
|
if silent:
|
|
294
|
-
return
|
|
297
|
+
return False # see _www_inline_rule
|
|
295
298
|
return _create_autolink(state, local_len, total_len, url, email_text)
|
|
@@ -152,7 +152,7 @@ def render_myst_target(
|
|
|
152
152
|
options: OptionsDict,
|
|
153
153
|
env: EnvType,
|
|
154
154
|
) -> str:
|
|
155
|
-
label = tokens[idx].content
|
|
155
|
+
label = escapeHtml(tokens[idx].content)
|
|
156
156
|
class_name = "myst-target"
|
|
157
157
|
target = f'<a href="#{label}">({label})=</a>'
|
|
158
158
|
return f'<div class="{class_name}">{target}</div>'
|
|
@@ -21,20 +21,15 @@ def myst_role_plugin(md: MarkdownIt) -> None:
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def myst_role(state: StateInline, silent: bool) -> bool:
|
|
24
|
+
# note ``\{`` escaping is handled by the core ``escape`` rule,
|
|
25
|
+
# which runs before this rule
|
|
26
|
+
|
|
24
27
|
# check name
|
|
25
28
|
match = VALID_NAME_PATTERN.match(state.src[state.pos :])
|
|
26
29
|
if not match:
|
|
27
30
|
return False
|
|
28
31
|
name = match.group(1)
|
|
29
32
|
|
|
30
|
-
# check for starting backslash escape
|
|
31
|
-
try:
|
|
32
|
-
if state.src[state.pos - 1] == "\\":
|
|
33
|
-
# escaped (this could be improved in the case of edge case '\\{')
|
|
34
|
-
return False
|
|
35
|
-
except IndexError:
|
|
36
|
-
pass
|
|
37
|
-
|
|
38
33
|
# scan opening tick length
|
|
39
34
|
start = pos = state.pos + match.end()
|
|
40
35
|
try:
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Section reference plugin for markdown-it-py.
|
|
2
|
+
|
|
3
|
+
Captures section-sign references such as ``§1``, ``§1.1`` and ``§2.3.4``
|
|
4
|
+
into dedicated ``section_ref`` tokens, so downstream renderers
|
|
5
|
+
(e.g. MyST-Parser) can resolve them to numbered heading cross-references.
|
|
6
|
+
|
|
7
|
+
Requires markdown-it-py >= 4.1.0.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .index import section_ref_plugin
|
|
11
|
+
|
|
12
|
+
__all__ = ("section_ref_plugin",)
|