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.

Files changed (42) hide show
  1. mdformat_py_edu_fr-0.1.7/LICENSE.txt +27 -0
  2. mdformat_py_edu_fr-0.1.7/PKG-INFO +41 -0
  3. mdformat_py_edu_fr-0.1.7/README.md +23 -0
  4. mdformat_py_edu_fr-0.1.7/pyproject.toml +68 -0
  5. mdformat_py_edu_fr-0.1.7/src/mdformat_myst_pef/LICENSE +21 -0
  6. mdformat_py_edu_fr-0.1.7/src/mdformat_myst_pef/__init__.py +3 -0
  7. mdformat_py_edu_fr-0.1.7/src/mdformat_myst_pef/_directives.py +139 -0
  8. mdformat_py_edu_fr-0.1.7/src/mdformat_myst_pef/plugin.py +251 -0
  9. mdformat_py_edu_fr-0.1.7/src/mdformat_myst_pef/py.typed +1 -0
  10. mdformat_py_edu_fr-0.1.7/src/mdformat_py_edu_fr/__init__.py +118 -0
  11. mdformat_py_edu_fr-0.1.7/src/mdformat_py_edu_fr/__main__.py +100 -0
  12. mdformat_py_edu_fr-0.1.7/src/mdformat_py_edu_fr/format_with_jupytext.py +67 -0
  13. mdformat_py_edu_fr-0.1.7/src/mdformat_py_edu_fr/jupytext.toml +18 -0
  14. mdformat_py_edu_fr-0.1.7/src/mdformat_py_edu_fr/util_ruff.py +30 -0
  15. mdformat_py_edu_fr-0.1.7/tests/__init__.py +0 -0
  16. mdformat_py_edu_fr-0.1.7/tests/conftest.py +48 -0
  17. mdformat_py_edu_fr-0.1.7/tests/examples/formatted/23-la-spirale.md +54 -0
  18. mdformat_py_edu_fr-0.1.7/tests/examples/formatted/characteristics.md +234 -0
  19. mdformat_py_edu_fr-0.1.7/tests/examples/formatted/exercice-if.md +40 -0
  20. mdformat_py_edu_fr-0.1.7/tests/examples/formatted/index-intro-prog.md +23 -0
  21. mdformat_py_edu_fr-0.1.7/tests/examples/formatted/pres-pydata2025.md +657 -0
  22. mdformat_py_edu_fr-0.1.7/tests/examples/ruff_error.md +18 -0
  23. mdformat_py_edu_fr-0.1.7/tests/examples/unformatted/23-la-spirale.md +54 -0
  24. mdformat_py_edu_fr-0.1.7/tests/examples/unformatted/characteristics.md +234 -0
  25. mdformat_py_edu_fr-0.1.7/tests/examples/unformatted/exercice-if.md +45 -0
  26. mdformat_py_edu_fr-0.1.7/tests/examples/unformatted/index-intro-prog.md +24 -0
  27. mdformat_py_edu_fr-0.1.7/tests/examples/unformatted/pres-pydata2025.md +624 -0
  28. mdformat_py_edu_fr-0.1.7/tests/fixtures_ruff.txt +9 -0
  29. mdformat_py_edu_fr-0.1.7/tests/mdformat_myst_pef/LICENSE +21 -0
  30. mdformat_py_edu_fr-0.1.7/tests/mdformat_myst_pef/__init__.py +0 -0
  31. mdformat_py_edu_fr-0.1.7/tests/mdformat_myst_pef/data/commonmark_spec_v0.29.json +5194 -0
  32. mdformat_py_edu_fr-0.1.7/tests/mdformat_myst_pef/data/fixtures.md +506 -0
  33. mdformat_py_edu_fr-0.1.7/tests/mdformat_myst_pef/data/fixtures_unsupported.md +92 -0
  34. mdformat_py_edu_fr-0.1.7/tests/mdformat_myst_pef/pre-commit-test.md +46 -0
  35. mdformat_py_edu_fr-0.1.7/tests/mdformat_myst_pef/test_commonmark_compliancy.py +29 -0
  36. mdformat_py_edu_fr-0.1.7/tests/mdformat_myst_pef/test_mdformat_myst.py +35 -0
  37. mdformat_py_edu_fr-0.1.7/tests/test_cli_simple.py +32 -0
  38. mdformat_py_edu_fr-0.1.7/tests/test_examples.py +179 -0
  39. mdformat_py_edu_fr-0.1.7/tests/test_exclude.py +84 -0
  40. mdformat_py_edu_fr-0.1.7/tests/test_format_jupyter.py +37 -0
  41. mdformat_py_edu_fr-0.1.7/tests/test_ruff_error.py +12 -0
  42. 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,3 @@
1
+ """Mdformat plugin for MyST compatibility."""
2
+
3
+ __version__ = "0.2.2"
@@ -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