mdformat-py-edu-fr 0.1.7__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.
Potentially problematic release.
This version of mdformat-py-edu-fr might be problematic. Click here for more details.
- mdformat_py_edu_fr-0.1.7/LICENSE.txt +27 -0
- mdformat_py_edu_fr-0.1.7/PKG-INFO +41 -0
- mdformat_py_edu_fr-0.1.7/README.md +23 -0
- mdformat_py_edu_fr-0.1.7/pyproject.toml +68 -0
- mdformat_py_edu_fr-0.1.7/src/mdformat_myst_pef/LICENSE +21 -0
- mdformat_py_edu_fr-0.1.7/src/mdformat_myst_pef/__init__.py +3 -0
- mdformat_py_edu_fr-0.1.7/src/mdformat_myst_pef/_directives.py +139 -0
- mdformat_py_edu_fr-0.1.7/src/mdformat_myst_pef/plugin.py +251 -0
- mdformat_py_edu_fr-0.1.7/src/mdformat_myst_pef/py.typed +1 -0
- mdformat_py_edu_fr-0.1.7/src/mdformat_py_edu_fr/__init__.py +118 -0
- mdformat_py_edu_fr-0.1.7/src/mdformat_py_edu_fr/__main__.py +100 -0
- mdformat_py_edu_fr-0.1.7/src/mdformat_py_edu_fr/format_with_jupytext.py +67 -0
- mdformat_py_edu_fr-0.1.7/src/mdformat_py_edu_fr/jupytext.toml +18 -0
- mdformat_py_edu_fr-0.1.7/src/mdformat_py_edu_fr/util_ruff.py +30 -0
- mdformat_py_edu_fr-0.1.7/tests/__init__.py +0 -0
- mdformat_py_edu_fr-0.1.7/tests/conftest.py +48 -0
- mdformat_py_edu_fr-0.1.7/tests/examples/formatted/23-la-spirale.md +54 -0
- mdformat_py_edu_fr-0.1.7/tests/examples/formatted/characteristics.md +234 -0
- mdformat_py_edu_fr-0.1.7/tests/examples/formatted/exercice-if.md +40 -0
- mdformat_py_edu_fr-0.1.7/tests/examples/formatted/index-intro-prog.md +23 -0
- mdformat_py_edu_fr-0.1.7/tests/examples/formatted/pres-pydata2025.md +657 -0
- mdformat_py_edu_fr-0.1.7/tests/examples/ruff_error.md +18 -0
- mdformat_py_edu_fr-0.1.7/tests/examples/unformatted/23-la-spirale.md +54 -0
- mdformat_py_edu_fr-0.1.7/tests/examples/unformatted/characteristics.md +234 -0
- mdformat_py_edu_fr-0.1.7/tests/examples/unformatted/exercice-if.md +45 -0
- mdformat_py_edu_fr-0.1.7/tests/examples/unformatted/index-intro-prog.md +24 -0
- mdformat_py_edu_fr-0.1.7/tests/examples/unformatted/pres-pydata2025.md +624 -0
- mdformat_py_edu_fr-0.1.7/tests/fixtures_ruff.txt +9 -0
- mdformat_py_edu_fr-0.1.7/tests/mdformat_myst_pef/LICENSE +21 -0
- mdformat_py_edu_fr-0.1.7/tests/mdformat_myst_pef/__init__.py +0 -0
- mdformat_py_edu_fr-0.1.7/tests/mdformat_myst_pef/data/commonmark_spec_v0.29.json +5194 -0
- mdformat_py_edu_fr-0.1.7/tests/mdformat_myst_pef/data/fixtures.md +506 -0
- mdformat_py_edu_fr-0.1.7/tests/mdformat_myst_pef/data/fixtures_unsupported.md +92 -0
- mdformat_py_edu_fr-0.1.7/tests/mdformat_myst_pef/pre-commit-test.md +46 -0
- mdformat_py_edu_fr-0.1.7/tests/mdformat_myst_pef/test_commonmark_compliancy.py +29 -0
- mdformat_py_edu_fr-0.1.7/tests/mdformat_myst_pef/test_mdformat_myst.py +35 -0
- mdformat_py_edu_fr-0.1.7/tests/test_cli_simple.py +32 -0
- mdformat_py_edu_fr-0.1.7/tests/test_examples.py +179 -0
- mdformat_py_edu_fr-0.1.7/tests/test_exclude.py +84 -0
- mdformat_py_edu_fr-0.1.7/tests/test_format_jupyter.py +37 -0
- mdformat_py_edu_fr-0.1.7/tests/test_ruff_error.py +12 -0
- mdformat_py_edu_fr-0.1.7/tests/test_util_ruff.py +24 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Copyright (c) 2025, py-edu-fr developers and Pierre Augier
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
8
|
+
list of conditions and the following disclaimer.
|
|
9
|
+
|
|
10
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
|
12
|
+
and/or other materials provided with the distribution.
|
|
13
|
+
|
|
14
|
+
3. Neither the name of the copyright holder nor the names of its contributors
|
|
15
|
+
may be used to endorse or promote products derived from this software without
|
|
16
|
+
specific prior written permission.
|
|
17
|
+
|
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
19
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
20
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
23
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
24
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
25
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
26
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
27
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mdformat-py-edu-fr
|
|
3
|
+
Version: 0.1.7
|
|
4
|
+
Summary: Tiny wrapper around mdformat for the py-edu-fr project
|
|
5
|
+
Author-Email: Pierre Augier <pierre.augier@univ-grenoble-alpes.fr>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
License-File: LICENSE.txt
|
|
8
|
+
Requires-Python: >=3.13
|
|
9
|
+
Requires-Dist: jupytext
|
|
10
|
+
Requires-Dist: mdformat>=0.7.0
|
|
11
|
+
Requires-Dist: mdit-py-plugins>=0.3.0
|
|
12
|
+
Requires-Dist: mdformat-frontmatter>=0.3.2
|
|
13
|
+
Requires-Dist: mdformat-footnote>=0.1.1
|
|
14
|
+
Requires-Dist: mdformat-gfm>=1.0.0
|
|
15
|
+
Requires-Dist: ruamel.yaml>=0.16.0
|
|
16
|
+
Requires-Dist: ruff
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Tiny wrapper around mdformat for the py-edu-fr project
|
|
20
|
+
|
|
21
|
+
We needed a specific markdown formatter for the
|
|
22
|
+
[py-edu-fr project](https://python-cnrs.netlify.app/edu).
|
|
23
|
+
|
|
24
|
+
mdformat-py-edu-fr is based on mdformat, mdformat-myst and Jupytext.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
$ mdformat-py-edu-fr -h
|
|
28
|
+
usage: mdformat-py-edu-fr [-h] [--version] [--check] [--exclude PATTERN] [--verbose] [paths ...]
|
|
29
|
+
|
|
30
|
+
Format Markdown files for py-edu-fr project
|
|
31
|
+
|
|
32
|
+
positional arguments:
|
|
33
|
+
paths Files or directories to format (if omitted, reads from stdin)
|
|
34
|
+
|
|
35
|
+
options:
|
|
36
|
+
-h, --help show this help message and exit
|
|
37
|
+
--version show program's version number and exit
|
|
38
|
+
--check Check if files are formatted without modifying them (exit code 1 if changes needed)
|
|
39
|
+
--exclude PATTERN Glob pattern to exclude files/directories (can be specified multiple times)
|
|
40
|
+
--verbose, -v Print detailed information about processing
|
|
41
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Tiny wrapper around mdformat for the py-edu-fr project
|
|
2
|
+
|
|
3
|
+
We needed a specific markdown formatter for the
|
|
4
|
+
[py-edu-fr project](https://python-cnrs.netlify.app/edu).
|
|
5
|
+
|
|
6
|
+
mdformat-py-edu-fr is based on mdformat, mdformat-myst and Jupytext.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
$ mdformat-py-edu-fr -h
|
|
10
|
+
usage: mdformat-py-edu-fr [-h] [--version] [--check] [--exclude PATTERN] [--verbose] [paths ...]
|
|
11
|
+
|
|
12
|
+
Format Markdown files for py-edu-fr project
|
|
13
|
+
|
|
14
|
+
positional arguments:
|
|
15
|
+
paths Files or directories to format (if omitted, reads from stdin)
|
|
16
|
+
|
|
17
|
+
options:
|
|
18
|
+
-h, --help show this help message and exit
|
|
19
|
+
--version show program's version number and exit
|
|
20
|
+
--check Check if files are formatted without modifying them (exit code 1 if changes needed)
|
|
21
|
+
--exclude PATTERN Glob pattern to exclude files/directories (can be specified multiple times)
|
|
22
|
+
--verbose, -v Print detailed information about processing
|
|
23
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"pdm-backend@git+https://github.com/paugier/pdm-backend",
|
|
4
|
+
]
|
|
5
|
+
build-backend = "pdm.backend"
|
|
6
|
+
|
|
7
|
+
[project]
|
|
8
|
+
name = "mdformat-py-edu-fr"
|
|
9
|
+
description = "Tiny wrapper around mdformat for the py-edu-fr project"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Pierre Augier", email = "pierre.augier@univ-grenoble-alpes.fr" },
|
|
12
|
+
]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"jupytext",
|
|
15
|
+
"mdformat >=0.7.0",
|
|
16
|
+
"mdit-py-plugins >=0.3.0",
|
|
17
|
+
"mdformat-frontmatter >=0.3.2",
|
|
18
|
+
"mdformat-footnote >=0.1.1",
|
|
19
|
+
"mdformat-gfm >=1.0.0",
|
|
20
|
+
"ruamel.yaml >=0.16.0",
|
|
21
|
+
"ruff",
|
|
22
|
+
]
|
|
23
|
+
requires-python = ">=3.13"
|
|
24
|
+
readme = "README.md"
|
|
25
|
+
license = "BSD-3-Clause"
|
|
26
|
+
license-files = [
|
|
27
|
+
"LICENSE.txt",
|
|
28
|
+
]
|
|
29
|
+
dynamic = []
|
|
30
|
+
version = "0.1.7"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
mdformat-py-edu-fr = "mdformat_py_edu_fr.__main__:main"
|
|
34
|
+
|
|
35
|
+
[project.entry-points."mdformat.parser_extension"]
|
|
36
|
+
myst = "mdformat_myst_pef.plugin"
|
|
37
|
+
|
|
38
|
+
[tool.pdm]
|
|
39
|
+
distribution = true
|
|
40
|
+
|
|
41
|
+
[tool.pdm.version]
|
|
42
|
+
source = "scm"
|
|
43
|
+
|
|
44
|
+
[tool.pdm.options]
|
|
45
|
+
lock = [
|
|
46
|
+
"-G",
|
|
47
|
+
":all",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
testpaths = [
|
|
52
|
+
"tests",
|
|
53
|
+
]
|
|
54
|
+
addopts = "--pdbcls=IPython.terminal.debugger:TerminalPdb"
|
|
55
|
+
|
|
56
|
+
[dependency-groups]
|
|
57
|
+
format = [
|
|
58
|
+
"ruff",
|
|
59
|
+
]
|
|
60
|
+
test = [
|
|
61
|
+
"pytest",
|
|
62
|
+
]
|
|
63
|
+
dev = [
|
|
64
|
+
"mdformat-py-edu-fr[format]",
|
|
65
|
+
"mdformat-py-edu-fr[test]",
|
|
66
|
+
"ipython",
|
|
67
|
+
"ipdb",
|
|
68
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Taneli Hukkinen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, MutableMapping, Sequence
|
|
4
|
+
import io
|
|
5
|
+
|
|
6
|
+
from markdown_it import MarkdownIt
|
|
7
|
+
from mdformat.renderer import LOGGER, RenderContext, RenderTreeNode
|
|
8
|
+
import ruamel.yaml
|
|
9
|
+
|
|
10
|
+
yaml = ruamel.yaml.YAML()
|
|
11
|
+
yaml.indent(mapping=2, sequence=4, offset=2)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def longest_consecutive_sequence(seq: str, char: str) -> int:
|
|
15
|
+
"""Return length of the longest consecutive sequence of `char` characters
|
|
16
|
+
in string `seq`."""
|
|
17
|
+
assert len(char) == 1
|
|
18
|
+
longest = 0
|
|
19
|
+
current_streak = 0
|
|
20
|
+
for c in seq:
|
|
21
|
+
if c == char:
|
|
22
|
+
current_streak += 1
|
|
23
|
+
else:
|
|
24
|
+
current_streak = 0
|
|
25
|
+
if current_streak > longest:
|
|
26
|
+
longest = current_streak
|
|
27
|
+
return longest
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def fence(node: "RenderTreeNode", context: "RenderContext") -> str:
|
|
31
|
+
"""Render fences (and directives).
|
|
32
|
+
|
|
33
|
+
Copied from upstream `mdformat` core and should be kept up-to-date
|
|
34
|
+
if upstream introduces changes. Note that only two lines are added
|
|
35
|
+
to the upstream implementation, i.e. the condition that calls
|
|
36
|
+
`format_directive_content` function.
|
|
37
|
+
"""
|
|
38
|
+
info_str = node.info.strip()
|
|
39
|
+
lang = info_str.split(maxsplit=1)[0] if info_str else ""
|
|
40
|
+
code_block = node.content
|
|
41
|
+
|
|
42
|
+
# Info strings of backtick code fences can not contain backticks or tildes.
|
|
43
|
+
# If that is the case, we make a tilde code fence instead.
|
|
44
|
+
if "`" in info_str or "~" in info_str:
|
|
45
|
+
fence_char = "~"
|
|
46
|
+
else:
|
|
47
|
+
fence_char = "`"
|
|
48
|
+
|
|
49
|
+
# Format the code block using enabled codeformatter funcs
|
|
50
|
+
if lang in context.options.get("codeformatters", {}):
|
|
51
|
+
fmt_func = context.options["codeformatters"][lang]
|
|
52
|
+
try:
|
|
53
|
+
code_block = fmt_func(code_block, info_str)
|
|
54
|
+
except Exception:
|
|
55
|
+
# Swallow exceptions so that formatter errors (e.g. due to
|
|
56
|
+
# invalid code) do not crash mdformat.
|
|
57
|
+
assert node.map is not None, "A fence token must have `map` attribute set"
|
|
58
|
+
LOGGER.warning(
|
|
59
|
+
f"Failed formatting content of a {lang} code block "
|
|
60
|
+
f"(line {node.map[0] + 1} before formatting)"
|
|
61
|
+
)
|
|
62
|
+
# This "elif" is the *only* thing added to the upstream `fence` implementation!
|
|
63
|
+
elif lang.startswith("{") and lang.endswith("}"):
|
|
64
|
+
code_block = format_directive_content(code_block)
|
|
65
|
+
|
|
66
|
+
# The code block must not include as long or longer sequence of `fence_char`s
|
|
67
|
+
# as the fence string itself
|
|
68
|
+
fence_len = max(3, longest_consecutive_sequence(code_block, fence_char) + 1)
|
|
69
|
+
fence_str = fence_char * fence_len
|
|
70
|
+
|
|
71
|
+
return f"{fence_str}{info_str}\n{code_block}{fence_str}"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def format_directive_content(raw_content: str) -> str:
|
|
75
|
+
parse_result = parse_opts_and_content(raw_content)
|
|
76
|
+
if not parse_result:
|
|
77
|
+
return raw_content
|
|
78
|
+
unformatted_yaml, content = parse_result
|
|
79
|
+
dump_stream = io.StringIO()
|
|
80
|
+
try:
|
|
81
|
+
parsed = yaml.load(unformatted_yaml)
|
|
82
|
+
yaml.dump(parsed, stream=dump_stream)
|
|
83
|
+
except ruamel.yaml.YAMLError:
|
|
84
|
+
LOGGER.warning("Invalid YAML in MyST directive options.")
|
|
85
|
+
return raw_content
|
|
86
|
+
formatted_yaml = dump_stream.getvalue()
|
|
87
|
+
|
|
88
|
+
# Remove the YAML closing tag if added by `ruamel.yaml`
|
|
89
|
+
if formatted_yaml.endswith("\n...\n"):
|
|
90
|
+
formatted_yaml = formatted_yaml[:-4]
|
|
91
|
+
|
|
92
|
+
# Convert empty YAML to most concise form
|
|
93
|
+
if formatted_yaml == "null\n":
|
|
94
|
+
formatted_yaml = ""
|
|
95
|
+
|
|
96
|
+
formatted = "---\n" + formatted_yaml + "---\n"
|
|
97
|
+
if content:
|
|
98
|
+
formatted += content + "\n"
|
|
99
|
+
return formatted
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def parse_opts_and_content(raw_content: str) -> tuple[str, str] | None:
|
|
103
|
+
if not raw_content:
|
|
104
|
+
return None
|
|
105
|
+
lines = raw_content.splitlines()
|
|
106
|
+
line = lines.pop(0)
|
|
107
|
+
yaml_lines = []
|
|
108
|
+
if all(c == "-" for c in line) and len(line) >= 3:
|
|
109
|
+
while lines:
|
|
110
|
+
line = lines.pop(0)
|
|
111
|
+
if all(c == "-" for c in line) and len(line) >= 3:
|
|
112
|
+
break
|
|
113
|
+
yaml_lines.append(line)
|
|
114
|
+
elif line.lstrip().startswith(":"):
|
|
115
|
+
yaml_lines.append(line.lstrip()[1:])
|
|
116
|
+
while lines:
|
|
117
|
+
if not lines[0].lstrip().startswith(":"):
|
|
118
|
+
break
|
|
119
|
+
line = lines.pop(0).lstrip()[1:]
|
|
120
|
+
yaml_lines.append(line)
|
|
121
|
+
else:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
first_line_is_empty_but_second_line_isnt = (
|
|
125
|
+
len(lines) >= 2 and not lines[0].strip() and lines[1].strip()
|
|
126
|
+
)
|
|
127
|
+
exactly_one_empty_line = len(lines) == 1 and not lines[0].strip()
|
|
128
|
+
if first_line_is_empty_but_second_line_isnt or exactly_one_empty_line:
|
|
129
|
+
lines.pop(0)
|
|
130
|
+
|
|
131
|
+
unformatted_yaml = "\n".join(yaml_lines)
|
|
132
|
+
content = "\n".join(lines)
|
|
133
|
+
return unformatted_yaml, content
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def render_fence_html(
|
|
137
|
+
self: MarkdownIt, tokens: Sequence, idx: int, options: Mapping, env: MutableMapping
|
|
138
|
+
) -> str:
|
|
139
|
+
return ""
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import textwrap
|
|
5
|
+
|
|
6
|
+
from markdown_it import MarkdownIt
|
|
7
|
+
import mdformat.plugins
|
|
8
|
+
from mdformat.renderer import RenderContext, RenderTreeNode
|
|
9
|
+
from mdit_py_plugins.dollarmath import dollarmath_plugin
|
|
10
|
+
from mdit_py_plugins.myst_blocks import myst_block_plugin
|
|
11
|
+
from mdit_py_plugins.myst_role import myst_role_plugin
|
|
12
|
+
from mdit_py_plugins.container import container_plugin
|
|
13
|
+
|
|
14
|
+
from mdformat_myst_pef._directives import fence, render_fence_html
|
|
15
|
+
|
|
16
|
+
_TARGET_PATTERN = re.compile(r"^\s*\(.+\)=\s*$")
|
|
17
|
+
_ROLE_NAME_PATTERN = re.compile(r"({[a-zA-Z0-9_\-+:]+})")
|
|
18
|
+
_YAML_HEADER_PATTERN = re.compile(r"(?m)(^:\w+: .*$\n?)+|^---$\n(?s:.).*\n---\n")
|
|
19
|
+
|
|
20
|
+
container_names = [
|
|
21
|
+
"admonition",
|
|
22
|
+
"attention",
|
|
23
|
+
"caution",
|
|
24
|
+
"danger",
|
|
25
|
+
"div",
|
|
26
|
+
"dropdown",
|
|
27
|
+
"embed",
|
|
28
|
+
"error",
|
|
29
|
+
"exercise",
|
|
30
|
+
"exercise-end",
|
|
31
|
+
"exercise-start",
|
|
32
|
+
"figure",
|
|
33
|
+
"glossary",
|
|
34
|
+
"grid",
|
|
35
|
+
"grid-item",
|
|
36
|
+
"grid-item-card",
|
|
37
|
+
"hint",
|
|
38
|
+
"image",
|
|
39
|
+
"important",
|
|
40
|
+
"include",
|
|
41
|
+
"index",
|
|
42
|
+
"literal-include",
|
|
43
|
+
"margin",
|
|
44
|
+
"math",
|
|
45
|
+
"note",
|
|
46
|
+
"prf:algorithm",
|
|
47
|
+
"prf:assumption",
|
|
48
|
+
"prf:axiom",
|
|
49
|
+
"prf:conjecture",
|
|
50
|
+
"prf:corollary",
|
|
51
|
+
"prf:criterion",
|
|
52
|
+
"prf:definition",
|
|
53
|
+
"prf:example",
|
|
54
|
+
"prf:lemma",
|
|
55
|
+
"prf:observation",
|
|
56
|
+
"prf:proof",
|
|
57
|
+
"prf:property",
|
|
58
|
+
"prf:proposition",
|
|
59
|
+
"prf:remark",
|
|
60
|
+
"prf:theorem",
|
|
61
|
+
"seealso",
|
|
62
|
+
"show-index",
|
|
63
|
+
"sidebar",
|
|
64
|
+
"solution",
|
|
65
|
+
"solution-end",
|
|
66
|
+
"solution-start",
|
|
67
|
+
"span",
|
|
68
|
+
"tab-item",
|
|
69
|
+
"tab-set",
|
|
70
|
+
"table",
|
|
71
|
+
"tip",
|
|
72
|
+
"todo",
|
|
73
|
+
"topics",
|
|
74
|
+
"warning",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def update_mdit(mdit: MarkdownIt) -> None:
|
|
79
|
+
# Enable mdformat-tables plugin
|
|
80
|
+
tables_plugin = mdformat.plugins.PARSER_EXTENSIONS["tables"]
|
|
81
|
+
if tables_plugin not in mdit.options["parser_extension"]:
|
|
82
|
+
mdit.options["parser_extension"].append(tables_plugin)
|
|
83
|
+
tables_plugin.update_mdit(mdit)
|
|
84
|
+
|
|
85
|
+
# Enable mdformat-frontmatter plugin
|
|
86
|
+
frontmatter_plugin = mdformat.plugins.PARSER_EXTENSIONS["frontmatter"]
|
|
87
|
+
if frontmatter_plugin not in mdit.options["parser_extension"]:
|
|
88
|
+
mdit.options["parser_extension"].append(frontmatter_plugin)
|
|
89
|
+
frontmatter_plugin.update_mdit(mdit)
|
|
90
|
+
|
|
91
|
+
# Enable mdformat-footnote plugin
|
|
92
|
+
footnote_plugin = mdformat.plugins.PARSER_EXTENSIONS["footnote"]
|
|
93
|
+
if footnote_plugin not in mdit.options["parser_extension"]:
|
|
94
|
+
mdit.options["parser_extension"].append(footnote_plugin)
|
|
95
|
+
footnote_plugin.update_mdit(mdit)
|
|
96
|
+
|
|
97
|
+
# Enable MyST role markdown-it extension
|
|
98
|
+
mdit.use(myst_role_plugin)
|
|
99
|
+
|
|
100
|
+
# Enable MyST block markdown-it extension (including "LineComment",
|
|
101
|
+
# "BlockBreak" and "Target" syntaxes)
|
|
102
|
+
mdit.use(myst_block_plugin)
|
|
103
|
+
|
|
104
|
+
# Enable dollarmath markdown-it extension
|
|
105
|
+
mdit.use(dollarmath_plugin)
|
|
106
|
+
|
|
107
|
+
# Trick `mdformat`s AST validation by removing HTML rendering of code
|
|
108
|
+
# blocks and fences. Directives are parsed as code fences and we
|
|
109
|
+
# modify them in ways that don't break MyST AST but do break
|
|
110
|
+
# CommonMark AST, so we need to do this to make validation pass.
|
|
111
|
+
mdit.add_render_rule("fence", render_fence_html)
|
|
112
|
+
mdit.add_render_rule("code_block", render_fence_html)
|
|
113
|
+
|
|
114
|
+
for name in container_names:
|
|
115
|
+
container_plugin(mdit, name="{" + name + "}", marker=":")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def container_renderer(
|
|
119
|
+
node: RenderTreeNode, context: RenderContext, *args, **kwargs
|
|
120
|
+
) -> str:
|
|
121
|
+
children = node.children
|
|
122
|
+
paragraphs = []
|
|
123
|
+
if children:
|
|
124
|
+
# Look at the tokens forming the first paragraph and see if
|
|
125
|
+
# they form a YAML header. This could be stricter: there
|
|
126
|
+
# should be exactly three tokens: paragraph open, YAML
|
|
127
|
+
# header, paragraph end.
|
|
128
|
+
tokens = children[0].to_tokens()
|
|
129
|
+
if all(
|
|
130
|
+
token.type in {"paragraph_open", "paragraph_close"}
|
|
131
|
+
or _YAML_HEADER_PATTERN.fullmatch(token.content)
|
|
132
|
+
for token in tokens
|
|
133
|
+
):
|
|
134
|
+
paragraphs.append(
|
|
135
|
+
"\n".join(token.content.strip() for token in tokens if token.content)
|
|
136
|
+
)
|
|
137
|
+
# and skip that first paragraph
|
|
138
|
+
children = children[1:]
|
|
139
|
+
|
|
140
|
+
paragraphs.extend(child.render(context) for child in children)
|
|
141
|
+
|
|
142
|
+
return node.markup + node.info + "\n" + "\n\n".join(paragraphs) + "\n" + node.markup
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _role_renderer(node: RenderTreeNode, context: RenderContext) -> str:
|
|
146
|
+
role_name = "{" + node.meta["name"] + "}"
|
|
147
|
+
role_content = f"`{node.content}`"
|
|
148
|
+
return role_name + role_content
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _comment_renderer(node: RenderTreeNode, context: RenderContext) -> str:
|
|
152
|
+
return "%" + node.content.replace("\n", "\n%")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _blockbreak_renderer(node: RenderTreeNode, context: RenderContext) -> str:
|
|
156
|
+
text = "+++"
|
|
157
|
+
if node.content:
|
|
158
|
+
text += f" {node.content}"
|
|
159
|
+
return text
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _target_renderer(node: RenderTreeNode, context: RenderContext) -> str:
|
|
163
|
+
return f"({node.content})="
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _math_inline_renderer(node: RenderTreeNode, context: RenderContext) -> str:
|
|
167
|
+
return f"${node.content}$"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _math_block_renderer(node: RenderTreeNode, context: RenderContext) -> str:
|
|
171
|
+
indent_width = context.env.get("indent_width", 0)
|
|
172
|
+
if indent_width > 0:
|
|
173
|
+
return f"$${textwrap.dedent(node.content)}$$"
|
|
174
|
+
return f"$${node.content}$$"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _math_block_label_renderer(node: RenderTreeNode, context: RenderContext) -> str:
|
|
178
|
+
return f"{_math_block_renderer(node, context)} ({node.info})"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _math_block_safe_blockquote_renderer(
|
|
182
|
+
node: RenderTreeNode, context: RenderContext
|
|
183
|
+
) -> str:
|
|
184
|
+
marker = "> "
|
|
185
|
+
with context.indented(len(marker)):
|
|
186
|
+
lines = []
|
|
187
|
+
for i, child in enumerate(node.children):
|
|
188
|
+
if child.type in ("math_block", "math_block_label"):
|
|
189
|
+
lines.append(child.render(context))
|
|
190
|
+
else:
|
|
191
|
+
lines.extend(child.render(context).splitlines())
|
|
192
|
+
if i < (len(node.children) - 1):
|
|
193
|
+
lines.append("")
|
|
194
|
+
if not lines:
|
|
195
|
+
return ">"
|
|
196
|
+
quoted_lines = (f"{marker}{line}" if line else ">" for line in lines)
|
|
197
|
+
quoted_str = "\n".join(quoted_lines)
|
|
198
|
+
return quoted_str
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _render_children(node: RenderTreeNode, context: RenderContext) -> str:
|
|
202
|
+
return "\n\n".join(child.render(context) for child in node.children)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _escape_paragraph(text: str, node: RenderTreeNode, context: RenderContext) -> str:
|
|
206
|
+
lines = text.split("\n")
|
|
207
|
+
|
|
208
|
+
for i in range(len(lines)):
|
|
209
|
+
# Three or more "+" chars are interpreted as a block break. Escape them.
|
|
210
|
+
space_removed = lines[i].replace(" ", "")
|
|
211
|
+
if space_removed.startswith("+++"):
|
|
212
|
+
lines[i] = lines[i].replace("+", "\\+", 1)
|
|
213
|
+
|
|
214
|
+
# A line starting with "%" is a comment. Escape.
|
|
215
|
+
if lines[i].startswith("%"):
|
|
216
|
+
lines[i] = f"\\{lines[i]}"
|
|
217
|
+
|
|
218
|
+
# Escape lines that look like targets
|
|
219
|
+
if _TARGET_PATTERN.search(lines[i]):
|
|
220
|
+
lines[i] = lines[i].replace("(", "\\(", 1)
|
|
221
|
+
|
|
222
|
+
return "\n".join(lines)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _escape_text(text: str, node: RenderTreeNode, context: RenderContext) -> str:
|
|
226
|
+
# Escape MyST role names
|
|
227
|
+
text = _ROLE_NAME_PATTERN.sub(r"\\\1", text)
|
|
228
|
+
|
|
229
|
+
# Escape inline and block dollarmath
|
|
230
|
+
text = text.replace("$", "\\$")
|
|
231
|
+
|
|
232
|
+
return text
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
RENDERERS = {
|
|
236
|
+
"blockquote": _math_block_safe_blockquote_renderer,
|
|
237
|
+
"myst_role": _role_renderer,
|
|
238
|
+
"myst_line_comment": _comment_renderer,
|
|
239
|
+
"myst_block_break": _blockbreak_renderer,
|
|
240
|
+
"myst_target": _target_renderer,
|
|
241
|
+
"math_inline": _math_inline_renderer,
|
|
242
|
+
"math_block_label": _math_block_label_renderer,
|
|
243
|
+
"math_block": _math_block_renderer,
|
|
244
|
+
"fence": fence,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
for name in container_names:
|
|
249
|
+
RENDERERS["container_{" + name + "}"] = container_renderer
|
|
250
|
+
|
|
251
|
+
POSTPROCESSORS = {"paragraph": _escape_paragraph, "text": _escape_text}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker file for PEP 561
|