markstitch 0.1.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.
- markstitch-0.1.0/.gitignore +13 -0
- markstitch-0.1.0/LICENSE +21 -0
- markstitch-0.1.0/PKG-INFO +365 -0
- markstitch-0.1.0/README.md +351 -0
- markstitch-0.1.0/docs/compatibility.md +142 -0
- markstitch-0.1.0/docs/releasing.md +47 -0
- markstitch-0.1.0/examples/live_validation.py +211 -0
- markstitch-0.1.0/examples/parse_document.py +18 -0
- markstitch-0.1.0/examples/tracker.py +37 -0
- markstitch-0.1.0/markstitch/__init__.py +153 -0
- markstitch-0.1.0/markstitch/attributes.py +56 -0
- markstitch-0.1.0/markstitch/blocks.py +236 -0
- markstitch-0.1.0/markstitch/core.py +245 -0
- markstitch-0.1.0/markstitch/extensions.py +323 -0
- markstitch-0.1.0/markstitch/inline.py +211 -0
- markstitch-0.1.0/markstitch/parser/__init__.py +2 -0
- markstitch-0.1.0/markstitch/parser/_directives.py +311 -0
- markstitch-0.1.0/markstitch/parser/_engine.py +324 -0
- markstitch-0.1.0/markstitch/parser/_inline.py +327 -0
- markstitch-0.1.0/markstitch/parser/_rules.py +154 -0
- markstitch-0.1.0/markstitch/parser/_syntax.py +216 -0
- markstitch-0.1.0/markstitch/parser/_tables.py +175 -0
- markstitch-0.1.0/markstitch/parser/parse.py +18 -0
- markstitch-0.1.0/markstitch/py.typed +0 -0
- markstitch-0.1.0/markstitch/tables.py +222 -0
- markstitch-0.1.0/markstitch/templates.py +118 -0
- markstitch-0.1.0/package-lock.json +1218 -0
- markstitch-0.1.0/package.json +11 -0
- markstitch-0.1.0/pyproject.toml +166 -0
- markstitch-0.1.0/tests/parser/test_code_boundaries.py +74 -0
- markstitch-0.1.0/tests/parser/test_depth_limits.py +46 -0
- markstitch-0.1.0/tests/parser/test_image_alt.py +53 -0
- markstitch-0.1.0/tests/parser/test_inline_boundaries.py +30 -0
- markstitch-0.1.0/tests/parser/test_link_destinations.py +191 -0
- markstitch-0.1.0/tests/parser/test_malformed_directives.py +54 -0
- markstitch-0.1.0/tests/parser/test_nested_fallback.py +86 -0
- markstitch-0.1.0/tests/parser/test_optional_dependency.py +18 -0
- markstitch-0.1.0/tests/parser/test_parser.py +74 -0
- markstitch-0.1.0/tests/parser/test_parser_edges.py +281 -0
- markstitch-0.1.0/tests/parser/test_parser_yfm.py +262 -0
- markstitch-0.1.0/tests/parser/test_reference_definitions.py +209 -0
- markstitch-0.1.0/tests/parser/test_script_spacing.py +24 -0
- markstitch-0.1.0/tests/parser/test_table_code.py +66 -0
- markstitch-0.1.0/tests/parser/test_table_styles.py +49 -0
- markstitch-0.1.0/tests/render.cjs +22 -0
- markstitch-0.1.0/tests/test_attributes.py +78 -0
- markstitch-0.1.0/tests/test_convenience.py +34 -0
- markstitch-0.1.0/tests/test_document.py +13 -0
- markstitch-0.1.0/tests/test_extensions.py +138 -0
- markstitch-0.1.0/tests/test_grid_sort.py +47 -0
- markstitch-0.1.0/tests/test_options.py +45 -0
- markstitch-0.1.0/tests/test_reference_labels.py +41 -0
- markstitch-0.1.0/tests/test_render.py +326 -0
- markstitch-0.1.0/tests/test_spans.py +39 -0
- markstitch-0.1.0/tests/test_tables.py +50 -0
- markstitch-0.1.0/tests/test_templates.py +92 -0
- markstitch-0.1.0/tests/test_tracker.py +93 -0
- markstitch-0.1.0/tests/test_tree.py +96 -0
- markstitch-0.1.0/tests/test_validation.py +81 -0
- markstitch-0.1.0/uv.lock +943 -0
markstitch-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Valery Pavlov
|
|
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,365 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: markstitch
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Compose, parse, and edit YFM documents in Python
|
|
5
|
+
Project-URL: Repository, https://github.com/LerikP/markstitch
|
|
6
|
+
Project-URL: Issues, https://github.com/LerikP/markstitch/issues
|
|
7
|
+
Author: Valery Pavlov
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Python: <4.0,>=3.12
|
|
11
|
+
Provides-Extra: parser
|
|
12
|
+
Requires-Dist: markdown-it-py>=4.0.0; extra == 'parser'
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Markstitch
|
|
16
|
+
|
|
17
|
+
Compose, parse, and edit YFM documents in Python.
|
|
18
|
+
|
|
19
|
+
Build documents for **Yandex Tracker**, Yandex Wiki, and Diplodoc using Python objects.
|
|
20
|
+
Requires Python 3.12+. Import the library as `markstitch`. Document generation has no
|
|
21
|
+
runtime dependencies; parsing requires the `parser` extra:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
git clone https://github.com/LerikP/markstitch.git
|
|
25
|
+
cd markstitch
|
|
26
|
+
uv sync --locked --extra parser
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The package is not published on PyPI yet. To install from a local checkout:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
pip install ".[parser]"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
From the project directory:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
uv sync --locked
|
|
41
|
+
uv run python examples/tracker.py
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from markstitch import YFM, Header3, Link, NumberedList
|
|
46
|
+
|
|
47
|
+
document = YFM(
|
|
48
|
+
Header3("Test1"),
|
|
49
|
+
NumberedList("One", "Two", "Three"),
|
|
50
|
+
Link(text="link", href="example.com"),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
text = document.to_yfm()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Output:
|
|
57
|
+
|
|
58
|
+
```markdown
|
|
59
|
+
### Test1
|
|
60
|
+
|
|
61
|
+
1. One
|
|
62
|
+
2. Two
|
|
63
|
+
3. Three
|
|
64
|
+
|
|
65
|
+
[link](example.com)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Pass `text` to any description or comment field that accepts YFM.
|
|
69
|
+
The library produces a string; network requests and attachment uploads are the caller's responsibility.
|
|
70
|
+
|
|
71
|
+
## Composition
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from markstitch import (
|
|
75
|
+
YFM,
|
|
76
|
+
Bold,
|
|
77
|
+
BulletList,
|
|
78
|
+
Cell,
|
|
79
|
+
Checklist,
|
|
80
|
+
CodeBlock,
|
|
81
|
+
Cut,
|
|
82
|
+
Header2,
|
|
83
|
+
Link,
|
|
84
|
+
ListItem,
|
|
85
|
+
Note,
|
|
86
|
+
Paragraph,
|
|
87
|
+
Row,
|
|
88
|
+
Tab,
|
|
89
|
+
Tabs,
|
|
90
|
+
Task,
|
|
91
|
+
YFMTable,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
document = YFM(
|
|
95
|
+
Header2("Results", anchor="results", collapsible=True),
|
|
96
|
+
Paragraph("Status: ", Bold("ready"), ". ", Link("Report", "https://example.com")),
|
|
97
|
+
Note("Verify after deployment", kind="warning", title="Important"),
|
|
98
|
+
Cut(
|
|
99
|
+
"Details",
|
|
100
|
+
BulletList(ListItem("First stage", BulletList("Substep")), "Second stage"),
|
|
101
|
+
CodeBlock("print('hello')", language="python", line_numbers=True),
|
|
102
|
+
),
|
|
103
|
+
Tabs(Tab("Linux", "Instructions"), Tab("macOS", "Instructions")),
|
|
104
|
+
Checklist(Task("Implementation", checked=True), Task("Verification")),
|
|
105
|
+
YFMTable(
|
|
106
|
+
Row(Bold("Stage"), Bold("Steps")),
|
|
107
|
+
Row("Run", Cell(BulletList("Install", "Execute"))),
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- `YFM(*elements)` and block containers separate their children with a blank line.
|
|
113
|
+
- `Paragraph(*parts)` and `Text(*parts)` join inline elements **without inserting spaces**.
|
|
114
|
+
- `ListItem(*blocks)` nests multiple blocks inside a single list item.
|
|
115
|
+
- `Heading(text, level=1)` and `Header1` through `Header6` accept inline content,
|
|
116
|
+
`anchor=`, and `collapsible=`.
|
|
117
|
+
- `NumberedList(*items, start=1)` calculates indentation from the number's width, including after `9.`.
|
|
118
|
+
- `Table(headers, rows, align=...)` creates a simple table; `YFMTable(Row(...), ...)` creates a multiline table.
|
|
119
|
+
- All nodes support `.to_yfm(profile=...)`. `str(element)` uses the Tracker profile.
|
|
120
|
+
- The returned string has no automatically appended trailing newline.
|
|
121
|
+
- Repeated rendering does not mutate the tree.
|
|
122
|
+
|
|
123
|
+
## Parsing and tree traversal
|
|
124
|
+
|
|
125
|
+
The optional `parser` extra uses `markdown-it-py>=4.0.0`.
|
|
126
|
+
When working from source, run `uv sync --locked --extra parser`.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from markstitch import Link, parse
|
|
130
|
+
|
|
131
|
+
document = parse(
|
|
132
|
+
"### Report\n\nSee **[result](/old)**",
|
|
133
|
+
profile="tracker",
|
|
134
|
+
strict=True,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Immediate children: Heading and Paragraph.
|
|
138
|
+
top_level = list(document)
|
|
139
|
+
|
|
140
|
+
# Depth-first traversal, visiting each parent before its children.
|
|
141
|
+
for node in document.walk():
|
|
142
|
+
if isinstance(node, Link):
|
|
143
|
+
node.href = "/new"
|
|
144
|
+
|
|
145
|
+
updated = document.to_yfm()
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The equivalent class method is `YFM.from_yfm(source, profile="wiki", strict=False)`.
|
|
149
|
+
Both return a regular `YFM` document, using the same node types as document construction.
|
|
150
|
+
The profile is stored in `document.profile` and used by `.to_yfm()` unless explicitly overridden.
|
|
151
|
+
|
|
152
|
+
### Tree behavior
|
|
153
|
+
|
|
154
|
+
- `iter(element)` / `element.iter_children()` return immediate child **elements**.
|
|
155
|
+
`walk()` visits all elements in depth-first preorder, including the root.
|
|
156
|
+
- Traversal returns the original objects. Changing `Link.href`, `Heading.text`, `Task.checked`,
|
|
157
|
+
or other fields affects subsequent serialization.
|
|
158
|
+
- Traversal includes inline content in headings and links, `YFMTable` rows and cells,
|
|
159
|
+
regular `Table` headers and cells, and both branches of `If` / `InlineIf`.
|
|
160
|
+
- URLs, CSS attributes, and other metadata are not child nodes. Strings passed to constructors
|
|
161
|
+
remain scalar values on their owning node. Parsed plain text becomes `Text` nodes visible
|
|
162
|
+
to `walk()`, with its parts stored in `Text.children`.
|
|
163
|
+
- `Row` is an iterable structural node whose `.to_yfm()` returns a table row fragment.
|
|
164
|
+
Full span validation happens when serializing the entire `YFMTable`.
|
|
165
|
+
- A shared node used twice is visited twice. Cycles raise `ValueError`.
|
|
166
|
+
Traversal is iterative, so tree depth does not consume the Python call stack.
|
|
167
|
+
- Custom nodes can override `iter_children()` to participate in traversal.
|
|
168
|
+
|
|
169
|
+
### Parser behavior
|
|
170
|
+
|
|
171
|
+
The parser supports CommonMark blocks and inline formatting, the library's YFM node syntax,
|
|
172
|
+
both table types, spans and attributes, profile-specific macros, block and inline templates,
|
|
173
|
+
link definitions, terms, and footnotes.
|
|
174
|
+
|
|
175
|
+
- By default, unknown or profile-unsupported constructs are preserved as `Raw` / `RawInline`.
|
|
176
|
+
If an unknown argument prevents part of a block from being parsed correctly, the entire
|
|
177
|
+
original block is preserved.
|
|
178
|
+
- With `strict=True`, these cases raise `ParseError` instead. The message includes the reason
|
|
179
|
+
and, for block errors, the line number within the parsed fragment.
|
|
180
|
+
- The parser normalizes list and formatting markers, item numbers, indentation, blank lines,
|
|
181
|
+
line endings, and attributes. Original formatting is **not preserved byte for byte**.
|
|
182
|
+
Unknown raw fragments retain their internal text; separators follow the usual `YFM` behavior.
|
|
183
|
+
- Tight and loose lists remain distinct through
|
|
184
|
+
`BulletList(..., loose=True)` / `NumberedList(..., loose=True)`.
|
|
185
|
+
- A shortcut reference `[label]` with a definition resolves to `Link`;
|
|
186
|
+
the explicit form `[text][label]` remains a `ReferenceLink`.
|
|
187
|
+
- Recursive parsing is limited to 64 internal levels. Exceeding this limit raises `ParseError`,
|
|
188
|
+
even in non-strict mode.
|
|
189
|
+
- The parser does not execute templates or fetch includes, images, or files.
|
|
190
|
+
**Parsing is not sanitization:** preserved `Raw` nodes may contain original HTML/YFM markup.
|
|
191
|
+
Safe display is the responsibility of the target renderer.
|
|
192
|
+
|
|
193
|
+
Runnable example: `uv run --extra parser python examples/parse_document.py`.
|
|
194
|
+
|
|
195
|
+
## Profiles
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from markstitch import Profile
|
|
199
|
+
|
|
200
|
+
text = document.to_yfm() # Tracker by default
|
|
201
|
+
text = document.to_yfm(profile=Profile.WIKI) # or "wiki"
|
|
202
|
+
text = document.to_yfm(profile=Profile.DIPLODOC) # or "diplodoc"
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Profiles select syntax and validate extension support. For example, Tracker/Wiki image
|
|
206
|
+
dimensions appear inside the link as `=100x200`, while Diplodoc uses
|
|
207
|
+
`{width=100 height=200}` attributes. Checklist serialization also differs.
|
|
208
|
+
|
|
209
|
+
Features without confirmed support in a profile raise `UnsupportedFeatureError`,
|
|
210
|
+
including in deeply nested nodes. Invalid values raise `ValueError`; incompatible content
|
|
211
|
+
types raise `TypeError`. Validation happens during `.to_yfm()`, not node construction.
|
|
212
|
+
|
|
213
|
+
**Profiles follow published documentation; they do not detect server capabilities.**
|
|
214
|
+
The target service's editor version and enabled plugins may differ.
|
|
215
|
+
|
|
216
|
+
The `Grid.sort` parameter is an optional **column slug**, for example
|
|
217
|
+
`Grid(grid_id, sort="case")`. It defaults to `None`, which omits the attribute.
|
|
218
|
+
Boolean values are rejected: the Wiki widget interprets `sort="1"` as sorting by a column
|
|
219
|
+
named `1`, not as enabling sorting.
|
|
220
|
+
|
|
221
|
+
Dynamic cell formatting is configured separately on the Wiki resource: a string column
|
|
222
|
+
needs `format="yfm"` when created. `format=null` means plain text, even if the entire grid
|
|
223
|
+
has `rich_text_format="yfm"`. `Grid` embeds a resource by ID without changing its column schema.
|
|
224
|
+
|
|
225
|
+
## Text and raw markup
|
|
226
|
+
|
|
227
|
+
Plain strings are escaped. Use nodes such as `Bold`, `Link`, `Color`, and `Code` for formatting.
|
|
228
|
+
Use `Raw` (block) or `RawInline` (inline) for trusted, preformatted markup.
|
|
229
|
+
`RawHTML` wraps trusted HTML/CSS in a Tracker/Wiki HTML block; it is **not a sanitizer**.
|
|
230
|
+
|
|
231
|
+
`If.expression` is a Diplodoc condition explicitly written by the document author.
|
|
232
|
+
`Variable`, `For`, `If`, and `Include` only generate syntax: they neither execute templates
|
|
233
|
+
nor read files. Do not pass untrusted input to `Raw`, `RawInline`, `RawHTML`, or `If.expression`.
|
|
234
|
+
Rendering, allowed iframe domains, and HTML policies are controlled by the target service.
|
|
235
|
+
|
|
236
|
+
URLs may be relative or use the `http`, `https`, or `mailto` scheme.
|
|
237
|
+
Other schemes, including `tel`, are rejected when serializing typed nodes.
|
|
238
|
+
|
|
239
|
+
Place `TermDefinition` nodes at the end of the document, as required by Diplodoc.
|
|
240
|
+
`ReferenceLink` / `ReferenceImage` nodes need a matching `LinkDefinition`.
|
|
241
|
+
|
|
242
|
+
## Table attributes and SVG (Diplodoc)
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
from markstitch import Attributes, Cell, Image, Row, YFMTable
|
|
246
|
+
|
|
247
|
+
table = YFMTable(
|
|
248
|
+
Row(
|
|
249
|
+
Cell("Value", align="center", attributes=Attributes(style={"width": "300px"})),
|
|
250
|
+
attributes=Attributes(classes=("summary",)),
|
|
251
|
+
),
|
|
252
|
+
attributes=Attributes(id="report", extra={"data-kind": "summary"}),
|
|
253
|
+
)
|
|
254
|
+
text = table.to_yfm(profile="diplodoc")
|
|
255
|
+
image = Image("_images/icon.svg", width=40, inline=False)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
`Attributes` supports `id`, `classes`, CSS declarations in `style`, and additional
|
|
259
|
+
`data-*`, `aria-*`, and `title` attributes through `extra`. CSS values can be keywords,
|
|
260
|
+
dimensions, or colors; functions, URLs, embedded declarations, and markup delimiters
|
|
261
|
+
are rejected. Attribute handling in HTML depends on the renderer version:
|
|
262
|
+
see [pinned renderer limitations](docs/compatibility.md#renderer).
|
|
263
|
+
|
|
264
|
+
The document author should supply trusted values for `classes` and `style`. Do not pass
|
|
265
|
+
user input to them: CSS classes may activate JavaScript behavior in the target service,
|
|
266
|
+
and CSS syntax validation does not make arbitrary styling safe.
|
|
267
|
+
`Attributes` is not a sanitizer for user-supplied CSS or HTML attributes.
|
|
268
|
+
|
|
269
|
+
Use `Span.LEFT` and `Span.ABOVE` to merge cells. To continue a multi-column rectangle
|
|
270
|
+
on the next row, put `Span.ABOVE` in its first column and `Span.LEFT` in the remaining columns.
|
|
271
|
+
The entire grid and each merged rectangle are validated: partial continuations, wider
|
|
272
|
+
lower rows, and L-shaped regions raise `ValueError`.
|
|
273
|
+
|
|
274
|
+
## Inline templates and slicing (Diplodoc)
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
from markstitch import Bold, InlineFor, InlineIf, Paragraph, Slice, Variable
|
|
278
|
+
|
|
279
|
+
template = Paragraph(
|
|
280
|
+
"Role: ",
|
|
281
|
+
InlineIf("user.admin == true", Bold("admin"), otherwise="guest"),
|
|
282
|
+
"; names: ",
|
|
283
|
+
InlineFor("user", "users", Variable(Slice("user.name", 0, 3)), ";"),
|
|
284
|
+
)
|
|
285
|
+
text = template.to_yfm(profile="diplodoc")
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`InlineIf` and `InlineFor` accept only inline elements. Pass an `InlineIf` as `otherwise`
|
|
289
|
+
to nest conditions. `Slice(source, begin, end=None)` supports integer indices, including
|
|
290
|
+
negative values, and another `Slice` as its source. Pass it to `Variable` with or without
|
|
291
|
+
filters, such as `filters=("length",)`. Diplodoc executes the template; the library only serializes it.
|
|
292
|
+
|
|
293
|
+
## Syntax coverage
|
|
294
|
+
|
|
295
|
+
See the [compatibility matrix and references](docs/compatibility.md) for built-in constructs,
|
|
296
|
+
optional plugins, and remaining limitations. Custom nodes can represent third-party syntax:
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
from markstitch import Element, Profile
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class CustomBlock(Element):
|
|
303
|
+
def _render(self, profile: Profile) -> str:
|
|
304
|
+
return "trusted preformatted markup"
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Inherit from `Inline` to define a custom inline element.
|
|
308
|
+
|
|
309
|
+
## Checks and builds
|
|
310
|
+
|
|
311
|
+
```sh
|
|
312
|
+
uv sync --locked --extra parser
|
|
313
|
+
uv run ruff format --check .
|
|
314
|
+
uv run ruff check .
|
|
315
|
+
uv run --extra parser mypy markstitch
|
|
316
|
+
uv run --extra parser ty check markstitch
|
|
317
|
+
uv run --extra parser pyrefly check markstitch
|
|
318
|
+
# Python behavior without Node.js:
|
|
319
|
+
uv run --extra parser pytest -m 'not renderer'
|
|
320
|
+
|
|
321
|
+
# Integration with the pinned YFM renderer (requires Node.js 24+):
|
|
322
|
+
npm ci --registry=https://registry.npmjs.org/ --ignore-scripts --no-audit --no-fund
|
|
323
|
+
uv run --extra parser pytest
|
|
324
|
+
|
|
325
|
+
uv build
|
|
326
|
+
uv run twine check --strict dist/*
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Renderer tests use `@diplodoc/transform` to check HTML structure, nesting, spans,
|
|
330
|
+
attribute parsing, and escaping. Template tests explicitly enable Liquid and cover
|
|
331
|
+
both conditional branches, loops, slicing, and filters. Wiki-specific macros and
|
|
332
|
+
Tracker editor syntax are checked against their text contracts.
|
|
333
|
+
|
|
334
|
+
Ruff, mypy, ty, Pyrefly, pytest, and Twine are development dependencies with versions pinned in `uv.lock`.
|
|
335
|
+
Python dependencies come from PyPI; the renderer comes from npm.
|
|
336
|
+
|
|
337
|
+
## CI and publishing
|
|
338
|
+
|
|
339
|
+
GitHub Actions runs the full test suite on Python 3.12, 3.13, and 3.14 for pull requests
|
|
340
|
+
and pushes to `main`. It also checks formatting, lint, and types with mypy, ty, and Pyrefly, builds the wheel and
|
|
341
|
+
source distribution, validates their metadata, and smoke-tests both installed distributions
|
|
342
|
+
with and without the parser extra. Successful builds expose a `python-distributions` artifact.
|
|
343
|
+
|
|
344
|
+
Publishing a GitHub Release runs the same checks and uploads the resulting distributions
|
|
345
|
+
to PyPI through Trusted Publishing. The release tag must match the package version exactly,
|
|
346
|
+
for example `v0.1.0` for version `0.1.0`. Pushing a commit or tag alone does not publish a package.
|
|
347
|
+
|
|
348
|
+
See [releasing](docs/releasing.md) for the one-time PyPI setup and release procedure.
|
|
349
|
+
|
|
350
|
+
## Package structure
|
|
351
|
+
|
|
352
|
+
- `core.py`: node interfaces, profiles, escaping, links, and document composition.
|
|
353
|
+
- `inline.py`: inline formatting, images, and inline code.
|
|
354
|
+
- `blocks.py`: headings, lists, code blocks, cuts, notes, and tabs.
|
|
355
|
+
- `tables.py`: simple and multiline tables.
|
|
356
|
+
- `attributes.py`: typed attributes for tables, rows, and cells.
|
|
357
|
+
- `extensions.py`: service macros, link definitions, terms, and footnotes.
|
|
358
|
+
- `templates.py`: Diplodoc template syntax generation.
|
|
359
|
+
- `parser/`: CommonMark/YFM tokenization and conversion to public nodes.
|
|
360
|
+
|
|
361
|
+
The API is inspired by [SnakeMD](https://www.snakemd.io/en/latest/); no source code was copied.
|
|
362
|
+
|
|
363
|
+
## License
|
|
364
|
+
|
|
365
|
+
Markstitch is licensed under the [MIT License](LICENSE).
|