myform 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. myform/Ignore.py +191 -0
  2. myform/Parser.py +802 -0
  3. myform/Postprocessor.py +176 -0
  4. myform/README.md +24 -0
  5. myform/Renderer.py +472 -0
  6. myform/Sembr.py +188 -0
  7. myform/Spacer.py +172 -0
  8. myform/__init__.py +234 -0
  9. myform/assist/__init__.py +74 -0
  10. myform/assist/base.py +165 -0
  11. myform/assist/cache.py +123 -0
  12. myform/assist/gateway.py +138 -0
  13. myform/cli.py +249 -0
  14. myform/config/__init__.py +45 -0
  15. myform/config/extensions.py +134 -0
  16. myform/config/options.py +416 -0
  17. myform/constants.py +65 -0
  18. myform/degrade/__init__.py +65 -0
  19. myform/degrade/engine.py +486 -0
  20. myform/degrade/lowerings.py +1231 -0
  21. myform/degrade/registry.py +222 -0
  22. myform/degrade/report.py +148 -0
  23. myform/model/__init__.py +125 -0
  24. myform/model/bridge.py +813 -0
  25. myform/model/nodes.py +554 -0
  26. myform/model/walk.py +99 -0
  27. myform/myform.typ +17 -0
  28. myform/options.py +200 -0
  29. myform/plugin.py +81 -0
  30. myform/py.typed +1 -0
  31. myform/readers/__init__.py +23 -0
  32. myform/readers/base.py +117 -0
  33. myform/readers/gfm.py +29 -0
  34. myform/readers/myst.py +30 -0
  35. myform/scripts/__init__.py +5 -0
  36. myform/scripts/sync_docs.py +284 -0
  37. myform/scripts/write_schema.py +147 -0
  38. myform/utils.py +149 -0
  39. myform/writers/__init__.py +59 -0
  40. myform/writers/base.py +117 -0
  41. myform/writers/gfm.py +51 -0
  42. myform/writers/myst.py +32 -0
  43. myform/writers/rst.py +675 -0
  44. myform/writers/typst.py +825 -0
  45. myform-0.7.0.dist-info/METADATA +189 -0
  46. myform-0.7.0.dist-info/RECORD +48 -0
  47. myform-0.7.0.dist-info/WHEEL +4 -0
  48. myform-0.7.0.dist-info/entry_points.txt +8 -0
myform/Ignore.py ADDED
@@ -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