myform 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.
- myform-0.7.0/PKG-INFO +189 -0
- myform-0.7.0/README.md +158 -0
- myform-0.7.0/myform/Ignore.py +191 -0
- myform-0.7.0/myform/Parser.py +802 -0
- myform-0.7.0/myform/Postprocessor.py +176 -0
- myform-0.7.0/myform/README.md +24 -0
- myform-0.7.0/myform/Renderer.py +472 -0
- myform-0.7.0/myform/Sembr.py +188 -0
- myform-0.7.0/myform/Spacer.py +172 -0
- myform-0.7.0/myform/__init__.py +234 -0
- myform-0.7.0/myform/assist/__init__.py +74 -0
- myform-0.7.0/myform/assist/base.py +165 -0
- myform-0.7.0/myform/assist/cache.py +123 -0
- myform-0.7.0/myform/assist/gateway.py +138 -0
- myform-0.7.0/myform/cli.py +249 -0
- myform-0.7.0/myform/config/__init__.py +45 -0
- myform-0.7.0/myform/config/extensions.py +134 -0
- myform-0.7.0/myform/config/options.py +416 -0
- myform-0.7.0/myform/constants.py +65 -0
- myform-0.7.0/myform/degrade/__init__.py +65 -0
- myform-0.7.0/myform/degrade/engine.py +486 -0
- myform-0.7.0/myform/degrade/lowerings.py +1231 -0
- myform-0.7.0/myform/degrade/registry.py +222 -0
- myform-0.7.0/myform/degrade/report.py +148 -0
- myform-0.7.0/myform/model/__init__.py +125 -0
- myform-0.7.0/myform/model/bridge.py +813 -0
- myform-0.7.0/myform/model/nodes.py +554 -0
- myform-0.7.0/myform/model/walk.py +99 -0
- myform-0.7.0/myform/myform.typ +17 -0
- myform-0.7.0/myform/options.py +200 -0
- myform-0.7.0/myform/plugin.py +81 -0
- myform-0.7.0/myform/py.typed +1 -0
- myform-0.7.0/myform/readers/__init__.py +23 -0
- myform-0.7.0/myform/readers/base.py +117 -0
- myform-0.7.0/myform/readers/gfm.py +29 -0
- myform-0.7.0/myform/readers/myst.py +30 -0
- myform-0.7.0/myform/scripts/__init__.py +5 -0
- myform-0.7.0/myform/scripts/sync_docs.py +284 -0
- myform-0.7.0/myform/scripts/write_schema.py +147 -0
- myform-0.7.0/myform/utils.py +149 -0
- myform-0.7.0/myform/writers/__init__.py +59 -0
- myform-0.7.0/myform/writers/base.py +117 -0
- myform-0.7.0/myform/writers/gfm.py +51 -0
- myform-0.7.0/myform/writers/myst.py +32 -0
- myform-0.7.0/myform/writers/rst.py +675 -0
- myform-0.7.0/myform/writers/typst.py +825 -0
- myform-0.7.0/pyproject.toml +228 -0
myform-0.7.0/PKG-INFO
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: myform
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: A typed translator that reads MyST and GFM and writes MyST, GFM, rST, and Typst.
|
|
5
|
+
Author: Robb Doering
|
|
6
|
+
Author-email: Robb Doering <robb@doering.ai>
|
|
7
|
+
License-Expression: MPL-2.0
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Framework :: Pytest
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
+
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Dist: mdformat>=1.0,<2
|
|
16
|
+
Requires-Dist: mdformat-deflist>=0.1.4
|
|
17
|
+
Requires-Dist: mdformat-footnote>=0.1.2
|
|
18
|
+
Requires-Dist: mdformat-front-matters>=2.0.0
|
|
19
|
+
Requires-Dist: mdformat-gfm>=1.0.0
|
|
20
|
+
Requires-Dist: mdit-py-plugins>=0.5.0
|
|
21
|
+
Requires-Dist: more-itertools>=10.7.0
|
|
22
|
+
Requires-Dist: my-basis>=0.8.4
|
|
23
|
+
Requires-Dist: pydantic>=2.11.7
|
|
24
|
+
Requires-Dist: regex>=2024.4.28
|
|
25
|
+
Requires-Dist: srsly>=2.5.3
|
|
26
|
+
Requires-Python: >=3.13
|
|
27
|
+
Project-URL: Homepage, https://gitlab.com/doering-ai/libs/myform
|
|
28
|
+
Project-URL: Repository, https://gitlab.com/doering-ai/libs/myform
|
|
29
|
+
Project-URL: Issues, https://gitlab.com/doering-ai/libs/myform/-/issues
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# myform
|
|
33
|
+
|
|
34
|
+
> **Status:** In development — the package has not had its first public release.
|
|
35
|
+
> Expect breaking changes.
|
|
36
|
+
|
|
37
|
+
  [](LICENSE) [](https://github.com/pre-commit/pre-commit) [](https://github.com/facebook/pyrefly) [](https://github.com/astral-sh/ruff)
|
|
38
|
+
|
|
39
|
+
`myform` is a universal docs-format translator built around one absolute, typed document tree.
|
|
40
|
+
It reads MyST Markdown and GitHub Flavored Markdown (GFM), then writes idiomatic MyST, GFM, Sphinx reStructuredText, or Typst.
|
|
41
|
+
Formatting is the identity case: `myform.convert(text, format, format)` runs through the same reader, typed tree, and writer as a cross-format conversion.
|
|
42
|
+
|
|
43
|
+
| Format | Read | Write |
|
|
44
|
+
| :---------------------- | :--: | :---: |
|
|
45
|
+
| MyST Markdown | Yes | Yes |
|
|
46
|
+
| GFM | Yes | Yes |
|
|
47
|
+
| Sphinx reStructuredText | No | Yes |
|
|
48
|
+
| Typst | No | Yes |
|
|
49
|
+
|
|
50
|
+
The two Markdown identity conversions remain the compatibility rail.
|
|
51
|
+
They produce byte-for-byte the same text as the existing mdformat pipelines, including the MyST opinions developed in this repository.
|
|
52
|
+
|
|
53
|
+
## Develop from source
|
|
54
|
+
|
|
55
|
+
Until the first `myform` release, install the checked-out source with `uv`:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
uv sync --all-extras
|
|
59
|
+
uv run myform --help
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
After the first public release, the package installation command will be:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
pip install myform
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Translate documents
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
uv run myform convert guide.md --from myst --to rst -o guide.rst
|
|
72
|
+
printf '# Hello\n' | uv run myform convert - --from gfm --to typst
|
|
73
|
+
uv run myform convert guide.md --from myst --to gfm --report json >guide.gfm
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Use `-` as the input path to read stdin.
|
|
77
|
+
Output goes to stdout unless `-o/--output` names a file.
|
|
78
|
+
Conversion reports always go to stderr, keeping document output safe for pipelines.
|
|
79
|
+
Choose `--report summary` (the default), `--report json`, or `--report none`.
|
|
80
|
+
|
|
81
|
+
## Format documents
|
|
82
|
+
|
|
83
|
+
The `format` verb performs an identity conversion:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
uv run myform format README.md
|
|
87
|
+
uv run myform format README.md --format myst -o README.formatted.md
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The Python API exposes the same contract:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
import myform
|
|
94
|
+
|
|
95
|
+
rst = myform.convert(source, 'myst', 'rst')
|
|
96
|
+
formatted = myform.convert(source, 'myst', 'myst')
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Configure translation
|
|
100
|
+
|
|
101
|
+
`myform` discovers the nearest `myform.yaml`, `.myform.yaml`, or `[tool.myform]` table by walking upward from the target path.
|
|
102
|
+
Within one directory, that order is also the precedence order.
|
|
103
|
+
The nearest directory wins rather than merging several files.
|
|
104
|
+
|
|
105
|
+
```yaml
|
|
106
|
+
degrade:
|
|
107
|
+
mode: annotate
|
|
108
|
+
comments: true
|
|
109
|
+
assist:
|
|
110
|
+
enabled: false
|
|
111
|
+
typst:
|
|
112
|
+
postformat: off
|
|
113
|
+
extensions: []
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Every non-native conversion is represented in the conversion report.
|
|
117
|
+
The default `annotate` degradation mode also leaves a provenance comment when the target supports comments; `silent` suppresses comments but not reports, while `strict` refuses carrying or dropping unsupported constructs.
|
|
118
|
+
|
|
119
|
+
The optional LM assist is degradation-only and disabled by default, which makes zero network calls.
|
|
120
|
+
When enabled, it uses the configured local OpenAI-compatible gateway and retains a deterministic fallback.
|
|
121
|
+
|
|
122
|
+
The committed [`myform.schema.json`](myform.schema.json) and [`myst-options.schema.json`](myst-options.schema.json) schemas describe the translator and mdformat-facade configuration surfaces.
|
|
123
|
+
Refresh or verify both with:
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
uv run myform-write-schema
|
|
127
|
+
uv run myform-write-schema --check
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Use the MyST mdformat facade
|
|
131
|
+
|
|
132
|
+
The original plugin remains available through mdformat's `myst` extension entry point:
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
uv run mdformat README.md
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
This repository began as a ground-up rewrite of [`mdformat-myst`](https://github.com/executablebooks/mdformat-myst), written to make uncompiled MyST documents look closer to the hand-formatted sources maintained by the MyST team.
|
|
139
|
+
That facade remains useful on its own:
|
|
140
|
+
|
|
141
|
+
1. [Targets](https://myst-parser.readthedocs.io/en/latest/syntax/cross-referencing.html#creating-explicit-targets) hug the headings or blocks they identify instead of being separated by an extra blank line.
|
|
142
|
+
|
|
143
|
+
2. [Directive options](https://myst-parser.readthedocs.io/en/latest/syntax/roles-and-directives.html) prefer the readable `:key: value` prefix form and escalate to a YAML block only when necessary.
|
|
144
|
+
|
|
145
|
+
3. Directive contents are formatted as first-class Markdown rather than treated as opaque text.
|
|
146
|
+
|
|
147
|
+
4. Parser and renderer rules are explicit, decorator-registered examples of a modern mdformat extension.
|
|
148
|
+
|
|
149
|
+
5. YAML values use `srsly`'s maintained `ruamel.yaml` integration.
|
|
150
|
+
|
|
151
|
+
6. The optional `sembr` mode reflows paragraphs to [one sentence per line](https://sembr.org/) without changing their rendered output:
|
|
152
|
+
|
|
153
|
+
```toml
|
|
154
|
+
[plugin.myst]
|
|
155
|
+
sembr = true
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Enable it per invocation with `mdformat --sembr <filename>`; `--no-sembr` overrides the TOML setting.
|
|
159
|
+
|
|
160
|
+
The facade also preserves established skip-formatting comments:
|
|
161
|
+
|
|
162
|
+
```md
|
|
163
|
+
<!-- myform-ignore -->
|
|
164
|
+
|
|
165
|
+
This next block is left exactly as written.
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
```md
|
|
169
|
+
<!-- myform-ignore-start -->
|
|
170
|
+
|
|
171
|
+
Every block in this range is preserved.
|
|
172
|
+
|
|
173
|
+
<!-- myform-ignore-end -->
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```md
|
|
177
|
+
<!-- myform-ignore-file -->
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The markers act at the document top level.
|
|
181
|
+
Their strings remain configurable through `[plugin.myst]`.
|
|
182
|
+
|
|
183
|
+
## Documentation
|
|
184
|
+
|
|
185
|
+
- [Documentation front door](docs/index.md)
|
|
186
|
+
- [API reference](docs/api.md)
|
|
187
|
+
- [Universal translator specification](docs/spec/myform-spec.md)
|
|
188
|
+
- [Typst ecosystem memo](docs/spec/typst-ecosystem-memo.md)
|
|
189
|
+
- [Release procedure](RELEASING.md)
|
myform-0.7.0/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# myform
|
|
2
|
+
|
|
3
|
+
> **Status:** In development — the package has not had its first public release.
|
|
4
|
+
> Expect breaking changes.
|
|
5
|
+
|
|
6
|
+
  [](LICENSE) [](https://github.com/pre-commit/pre-commit) [](https://github.com/facebook/pyrefly) [](https://github.com/astral-sh/ruff)
|
|
7
|
+
|
|
8
|
+
`myform` is a universal docs-format translator built around one absolute, typed document tree.
|
|
9
|
+
It reads MyST Markdown and GitHub Flavored Markdown (GFM), then writes idiomatic MyST, GFM, Sphinx reStructuredText, or Typst.
|
|
10
|
+
Formatting is the identity case: `myform.convert(text, format, format)` runs through the same reader, typed tree, and writer as a cross-format conversion.
|
|
11
|
+
|
|
12
|
+
| Format | Read | Write |
|
|
13
|
+
| :---------------------- | :--: | :---: |
|
|
14
|
+
| MyST Markdown | Yes | Yes |
|
|
15
|
+
| GFM | Yes | Yes |
|
|
16
|
+
| Sphinx reStructuredText | No | Yes |
|
|
17
|
+
| Typst | No | Yes |
|
|
18
|
+
|
|
19
|
+
The two Markdown identity conversions remain the compatibility rail.
|
|
20
|
+
They produce byte-for-byte the same text as the existing mdformat pipelines, including the MyST opinions developed in this repository.
|
|
21
|
+
|
|
22
|
+
## Develop from source
|
|
23
|
+
|
|
24
|
+
Until the first `myform` release, install the checked-out source with `uv`:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
uv sync --all-extras
|
|
28
|
+
uv run myform --help
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
After the first public release, the package installation command will be:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
pip install myform
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Translate documents
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
uv run myform convert guide.md --from myst --to rst -o guide.rst
|
|
41
|
+
printf '# Hello\n' | uv run myform convert - --from gfm --to typst
|
|
42
|
+
uv run myform convert guide.md --from myst --to gfm --report json >guide.gfm
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Use `-` as the input path to read stdin.
|
|
46
|
+
Output goes to stdout unless `-o/--output` names a file.
|
|
47
|
+
Conversion reports always go to stderr, keeping document output safe for pipelines.
|
|
48
|
+
Choose `--report summary` (the default), `--report json`, or `--report none`.
|
|
49
|
+
|
|
50
|
+
## Format documents
|
|
51
|
+
|
|
52
|
+
The `format` verb performs an identity conversion:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
uv run myform format README.md
|
|
56
|
+
uv run myform format README.md --format myst -o README.formatted.md
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The Python API exposes the same contract:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import myform
|
|
63
|
+
|
|
64
|
+
rst = myform.convert(source, 'myst', 'rst')
|
|
65
|
+
formatted = myform.convert(source, 'myst', 'myst')
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Configure translation
|
|
69
|
+
|
|
70
|
+
`myform` discovers the nearest `myform.yaml`, `.myform.yaml`, or `[tool.myform]` table by walking upward from the target path.
|
|
71
|
+
Within one directory, that order is also the precedence order.
|
|
72
|
+
The nearest directory wins rather than merging several files.
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
degrade:
|
|
76
|
+
mode: annotate
|
|
77
|
+
comments: true
|
|
78
|
+
assist:
|
|
79
|
+
enabled: false
|
|
80
|
+
typst:
|
|
81
|
+
postformat: off
|
|
82
|
+
extensions: []
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Every non-native conversion is represented in the conversion report.
|
|
86
|
+
The default `annotate` degradation mode also leaves a provenance comment when the target supports comments; `silent` suppresses comments but not reports, while `strict` refuses carrying or dropping unsupported constructs.
|
|
87
|
+
|
|
88
|
+
The optional LM assist is degradation-only and disabled by default, which makes zero network calls.
|
|
89
|
+
When enabled, it uses the configured local OpenAI-compatible gateway and retains a deterministic fallback.
|
|
90
|
+
|
|
91
|
+
The committed [`myform.schema.json`](myform.schema.json) and [`myst-options.schema.json`](myst-options.schema.json) schemas describe the translator and mdformat-facade configuration surfaces.
|
|
92
|
+
Refresh or verify both with:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
uv run myform-write-schema
|
|
96
|
+
uv run myform-write-schema --check
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Use the MyST mdformat facade
|
|
100
|
+
|
|
101
|
+
The original plugin remains available through mdformat's `myst` extension entry point:
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
uv run mdformat README.md
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
This repository began as a ground-up rewrite of [`mdformat-myst`](https://github.com/executablebooks/mdformat-myst), written to make uncompiled MyST documents look closer to the hand-formatted sources maintained by the MyST team.
|
|
108
|
+
That facade remains useful on its own:
|
|
109
|
+
|
|
110
|
+
1. [Targets](https://myst-parser.readthedocs.io/en/latest/syntax/cross-referencing.html#creating-explicit-targets) hug the headings or blocks they identify instead of being separated by an extra blank line.
|
|
111
|
+
|
|
112
|
+
2. [Directive options](https://myst-parser.readthedocs.io/en/latest/syntax/roles-and-directives.html) prefer the readable `:key: value` prefix form and escalate to a YAML block only when necessary.
|
|
113
|
+
|
|
114
|
+
3. Directive contents are formatted as first-class Markdown rather than treated as opaque text.
|
|
115
|
+
|
|
116
|
+
4. Parser and renderer rules are explicit, decorator-registered examples of a modern mdformat extension.
|
|
117
|
+
|
|
118
|
+
5. YAML values use `srsly`'s maintained `ruamel.yaml` integration.
|
|
119
|
+
|
|
120
|
+
6. The optional `sembr` mode reflows paragraphs to [one sentence per line](https://sembr.org/) without changing their rendered output:
|
|
121
|
+
|
|
122
|
+
```toml
|
|
123
|
+
[plugin.myst]
|
|
124
|
+
sembr = true
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Enable it per invocation with `mdformat --sembr <filename>`; `--no-sembr` overrides the TOML setting.
|
|
128
|
+
|
|
129
|
+
The facade also preserves established skip-formatting comments:
|
|
130
|
+
|
|
131
|
+
```md
|
|
132
|
+
<!-- myform-ignore -->
|
|
133
|
+
|
|
134
|
+
This next block is left exactly as written.
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```md
|
|
138
|
+
<!-- myform-ignore-start -->
|
|
139
|
+
|
|
140
|
+
Every block in this range is preserved.
|
|
141
|
+
|
|
142
|
+
<!-- myform-ignore-end -->
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```md
|
|
146
|
+
<!-- myform-ignore-file -->
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The markers act at the document top level.
|
|
150
|
+
Their strings remain configurable through `[plugin.myst]`.
|
|
151
|
+
|
|
152
|
+
## Documentation
|
|
153
|
+
|
|
154
|
+
- [Documentation front door](docs/index.md)
|
|
155
|
+
- [API reference](docs/api.md)
|
|
156
|
+
- [Universal translator specification](docs/spec/myform-spec.md)
|
|
157
|
+
- [Typst ecosystem memo](docs/spec/typst-ecosystem-memo.md)
|
|
158
|
+
- [Release procedure](RELEASING.md)
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Skip-formatting ("ignore") comments -- the mdformat #53 gap, filled for MyST.
|
|
2
|
+
|
|
3
|
+
mdformat core has no way to exempt content from formatting. This module adds the family every
|
|
4
|
+
other formatter ships, as configurable HTML comments (already valid CommonMark, so no new grammar):
|
|
5
|
+
|
|
6
|
+
- ``<!-- myform-ignore -->`` preserves the *next* block verbatim;
|
|
7
|
+
- ``<!-- myform-ignore-start -->`` ... ``<!-- myform-ignore-end -->`` a range;
|
|
8
|
+
- ``<!-- myform-ignore-file -->`` the whole file.
|
|
9
|
+
|
|
10
|
+
Markers act only at the **document top level**: one nested inside a list item or blockquote is left
|
|
11
|
+
as an ordinary comment and does not suppress formatting -- acting on it would unbalance the spliced
|
|
12
|
+
token tree and re-apply the container's prefix to the preserved source every run. The marker strings
|
|
13
|
+
are configurable via `MystOptions`.
|
|
14
|
+
|
|
15
|
+
The mechanism is a ``markdown-it`` core rule that runs after block parsing: the tokens of an ignored
|
|
16
|
+
span are replaced by a single ``myst_ignored`` token carrying the span's *original source*, which
|
|
17
|
+
`Renderer.render_myst_ignored` emits verbatim (an ``html_block`` carrier would ``lstrip`` the first
|
|
18
|
+
line and de-indent preserved code). The replacement is ephemeral -- internal to one render -- and
|
|
19
|
+
the emitted markdown is re-parsed fresh on the next run, so formatting stays idempotent (the marker
|
|
20
|
+
comments are themselves preserved and re-detected).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
############
|
|
24
|
+
### HEAD ###
|
|
25
|
+
############
|
|
26
|
+
### STANDARD
|
|
27
|
+
### EXTERNAL
|
|
28
|
+
import regex as re
|
|
29
|
+
from markdown_it.token import Token
|
|
30
|
+
from markdown_it.rules_core import StateCore
|
|
31
|
+
|
|
32
|
+
### INTERNAL
|
|
33
|
+
from .options import MystOptions
|
|
34
|
+
|
|
35
|
+
############
|
|
36
|
+
### DATA ###
|
|
37
|
+
############
|
|
38
|
+
#: Matches an HTML-comment block whose entire content is a single comment, capturing the inner text.
|
|
39
|
+
COMMENT = re.compile(r'\s*<!--\s*(?P<body>.*?)\s*-->\s*\Z', re.DOTALL)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
############
|
|
43
|
+
### BODY ###
|
|
44
|
+
############
|
|
45
|
+
class Ignore:
|
|
46
|
+
"""The ignore-comment core rule and its token-surgery helpers."""
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def apply(state: StateCore) -> None:
|
|
50
|
+
"""Core rule: replace ignore-marked spans with verbatim-source ``html_block`` tokens."""
|
|
51
|
+
opts = MystOptions.from_options(
|
|
52
|
+
state.md.options.get('mdformat', {}).get('plugin', {}).get('myst', {})
|
|
53
|
+
)
|
|
54
|
+
markers = (
|
|
55
|
+
opts.ignore_comment,
|
|
56
|
+
opts.ignore_start_comment,
|
|
57
|
+
opts.ignore_end_comment,
|
|
58
|
+
opts.ignore_file_comment,
|
|
59
|
+
)
|
|
60
|
+
# Fast path: nothing that could be a marker appears in the source at all.
|
|
61
|
+
if not any(marker in state.src for marker in markers):
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
tokens = state.tokens
|
|
65
|
+
src_lines = state.src.split('\n')
|
|
66
|
+
|
|
67
|
+
# File-level: any file marker (at any nesting) preserves the whole document, unchanged.
|
|
68
|
+
if any(Ignore._marker(tok, opts) == 'file' for tok in tokens):
|
|
69
|
+
state.tokens = [Ignore._verbatim(src_lines, 0, len(src_lines))]
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
new: list[Token] = []
|
|
73
|
+
i, n = 0, len(tokens)
|
|
74
|
+
while i < n:
|
|
75
|
+
tok = tokens[i]
|
|
76
|
+
kind = Ignore._marker(tok, opts)
|
|
77
|
+
|
|
78
|
+
# `next`/`start` markers act only at the document top level. A marker nested inside a
|
|
79
|
+
# container (list item, blockquote) is left as an ordinary comment: acting on it would
|
|
80
|
+
# unbalance the spliced token tree (crash) and re-apply the container prefix to the
|
|
81
|
+
# preserved source every run (unbounded ``> > x``). The verbatim carrier is always
|
|
82
|
+
# emitted at level 0, so container blocks it *contains* still round-trip (their source
|
|
83
|
+
# already carries the right prefixes).
|
|
84
|
+
if kind == 'next' and tok.level == 0:
|
|
85
|
+
new.append(tok) # keep the marker comment itself
|
|
86
|
+
i += 1
|
|
87
|
+
# The block to preserve is the next token (markdown-it emits no blank tokens between
|
|
88
|
+
# blocks). Skip if it is another marker, is nested, or has no source map.
|
|
89
|
+
nxt = tokens[i] if i < n else None
|
|
90
|
+
if nxt and nxt.level == 0 and nxt.map and Ignore._marker(nxt, opts) is None:
|
|
91
|
+
span, i = Ignore._consume_block(tokens, i)
|
|
92
|
+
Ignore._emit_verbatim(new, src_lines, span[0], span[1])
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
if kind == 'start' and tok.level == 0:
|
|
96
|
+
new.append(tok)
|
|
97
|
+
start_map = tok.map
|
|
98
|
+
start_line = start_map[1] if start_map else 0
|
|
99
|
+
# Match only a top-level end marker; a nested one would leave the splice unbalanced.
|
|
100
|
+
j = i + 1
|
|
101
|
+
while j < n and not (
|
|
102
|
+
tokens[j].level == 0 and Ignore._marker(tokens[j], opts) == 'end'
|
|
103
|
+
):
|
|
104
|
+
j += 1
|
|
105
|
+
if j < n: # matched end marker at top level
|
|
106
|
+
end_map = tokens[j].map
|
|
107
|
+
end_line = end_map[0] if end_map else len(src_lines)
|
|
108
|
+
Ignore._emit_verbatim(new, src_lines, start_line, end_line)
|
|
109
|
+
new.append(tokens[j]) # keep the end marker
|
|
110
|
+
i = j + 1
|
|
111
|
+
else: # unterminated range -> preserve to end of document
|
|
112
|
+
Ignore._emit_verbatim(new, src_lines, start_line, len(src_lines))
|
|
113
|
+
i = n
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
new.append(tok)
|
|
117
|
+
i += 1
|
|
118
|
+
|
|
119
|
+
state.tokens = new
|
|
120
|
+
|
|
121
|
+
# -------------------
|
|
122
|
+
# `.` Helpers
|
|
123
|
+
# -------------------
|
|
124
|
+
@staticmethod
|
|
125
|
+
def _marker(token: Token, opts: MystOptions) -> str | None:
|
|
126
|
+
"""Classify *token* as an ignore marker: ``next``/``start``/``end``/``file`` or ``None``."""
|
|
127
|
+
if token.type != 'html_block':
|
|
128
|
+
return None
|
|
129
|
+
match = COMMENT.fullmatch(token.content)
|
|
130
|
+
if not match:
|
|
131
|
+
return None
|
|
132
|
+
body = match['body']
|
|
133
|
+
if body == opts.ignore_file_comment:
|
|
134
|
+
return 'file'
|
|
135
|
+
if body == opts.ignore_start_comment:
|
|
136
|
+
return 'start'
|
|
137
|
+
if body == opts.ignore_end_comment:
|
|
138
|
+
return 'end'
|
|
139
|
+
if body == opts.ignore_comment:
|
|
140
|
+
return 'next'
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def _consume_block(tokens: list[Token], i: int) -> tuple[list[int], int]:
|
|
145
|
+
"""Return ``(source_span, next_index)`` for the block starting at ``tokens[i]``.
|
|
146
|
+
|
|
147
|
+
Walks the open/close nest to its matching close, taking the source span from the opener's
|
|
148
|
+
first line through the *maximum* end line of any token inside. This is required because the
|
|
149
|
+
plugin's ``myst_directive_open`` maps only the opener line (its closer sits on a separate
|
|
150
|
+
``myst_directive_close`` token), unlike stock markdown-it blocks whose opener spans the
|
|
151
|
+
whole block; the max-end walk is correct for both.
|
|
152
|
+
"""
|
|
153
|
+
start = tokens[i]
|
|
154
|
+
start_map = start.map or [0, 0]
|
|
155
|
+
first_line, last_line = start_map[0], start_map[1]
|
|
156
|
+
depth = start.nesting
|
|
157
|
+
j = i + 1
|
|
158
|
+
while depth > 0 and j < len(tokens):
|
|
159
|
+
child_map = tokens[j].map
|
|
160
|
+
if child_map:
|
|
161
|
+
last_line = max(last_line, child_map[1])
|
|
162
|
+
depth += tokens[j].nesting
|
|
163
|
+
j += 1
|
|
164
|
+
return [first_line, last_line], j
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def _emit_verbatim(new: list[Token], src_lines: list[str], start: int, end: int) -> None:
|
|
168
|
+
"""Append a verbatim carrier for ``src_lines[start:end]`` only when it has content.
|
|
169
|
+
|
|
170
|
+
Guards the degenerate empty span (e.g. a bare unterminated ``ignore-start`` at EOF) so no
|
|
171
|
+
empty block is emitted, which would otherwise add a stray trailing blank line.
|
|
172
|
+
"""
|
|
173
|
+
token = Ignore._verbatim(src_lines, start, end)
|
|
174
|
+
if token.content:
|
|
175
|
+
new.append(token)
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def _verbatim(src_lines: list[str], start_line: int, end_line: int) -> Token:
|
|
179
|
+
"""Build a ``myst_ignored`` token holding ``src_lines[start_line:end_line]`` verbatim.
|
|
180
|
+
|
|
181
|
+
Always emitted at the document top level (``level == 0``); its renderer
|
|
182
|
+
(`Renderer.render_myst_ignored`) returns the source untouched -- unlike an ``html_block``
|
|
183
|
+
carrier, whose renderer would ``lstrip`` the first line and de-indent preserved code.
|
|
184
|
+
"""
|
|
185
|
+
token = Token('myst_ignored', '', 0)
|
|
186
|
+
# ``strip('\n')`` drops the blank lines surrounding a range's content (between the markers)
|
|
187
|
+
# without touching leading *spaces*, so an indented code block keeps its indent.
|
|
188
|
+
token.content = '\n'.join(src_lines[start_line:end_line]).strip('\n')
|
|
189
|
+
token.block = True
|
|
190
|
+
token.map = [start_line, end_line]
|
|
191
|
+
return token
|