Implementations¶
markstay has three cores across Python, JavaScript, and Rust, plus a remark tree adapter that reuses the JavaScript core. A shared, language-neutral conformance corpus checks the specification, including the §8 content hash and §9 recovery scoring. The 0.14.0 package family implements specification v1.10; optional capabilities and each runner's scope are listed below.
| Language | Package | Install | Source |
|---|---|---|---|
| Python | markstay (PyPI) |
pip install markstay |
markstaymd/markstay-py |
| JavaScript | markstay (npm) |
npm install markstay |
markstaymd/markstay-core |
| JavaScript (remark) | remark-stay (npm) |
npm install remark-stay |
markstaymd/remark-stay |
| Rust | markstay (crates.io) |
cargo add markstay |
markstaymd/markstay-rs |
The same package family includes rehype-stay,
which emits HTML ids from block stays, and
plate-stay, the Plate bridge.
What each one is¶
- Python (
markstay) , the reference implementation. The parser-free core (hashing, marker grammar, segmentation, lint, recovery) plus amarkstayCLI; CommonMark-tree attachment (§5.2) is an optionalmarkstay[commonmark]extra. - JavaScript (
markstay) , a second, independent zero-dependency core (Node built-ins only) covering the same surface, with amarkstayCLI that also ships the write side: mint ids, stamp a document, refresh drifted hashes, repair duplicate ids. remark-stay, a unified/remark adapter that does §5.2 CommonMark-tree attachment over the mdast, reusing the JS core. Use it inside an existing remark pipeline.- Rust (
markstay) , a third-language core that is the strongest portability evidence (statically typed, UTF-8 native, explicit about bytes vs code points). It isno_std+allocwith zero runtime dependencies, ships a single static CLI binary, and is the source for a future WASM build.
Child-block identity is optional, and only one implements it¶
Version 1.3 lets a direct list item
carry its own stay under the reserved subhash key, and
version 1.6 lets a GFM table body row
do the same, on the same key and the same ladder. The Python reference is the only
implementation that segments and resolves child blocks, behind --child-blocks on the
CLI and child_blocks=True in the API, and that is not a gap:
§16 makes segmenting and resolving them a tool's
choice.
What §16 does not make optional is three compatibility rules. Two bind a writer:
a marker with a subhash never receives a container hash, and never makes its block
count as stamped. The three core writers apply both without requiring child segmentation.
The third arrived with version 1.6 and binds every reader: such a marker must never
be reported as the stay of the block containing it. Ignoring it is fine; attributing it
to the container is not, because that hands a consumer a real id bound to the wrong
block with nothing in the output saying so. Rows are why it is now an error rather than
an omission: a row marker sits mid-block, so a whole table's rows could otherwise be
reported as one block's stays. Core conformance vectors pin these rules; adapter
tests check the relevant reader behavior. plate-stay refuses child-marked input
because its bridge cannot carry the child's identity through conversion.
Write safety in version 1.8¶
§3.4 requires block markers to be inserted on their own line. Its same-line carrier checks apply when a writer implements list-item or table-row identity:
| Package | Marker-writing scope |
|---|---|
Python markstay |
Applies §3.4 to list-item and row carriers with child_blocks=True; the CLI reports each refused carrier. |
JavaScript and Rust markstay |
Write block markers on separate lines; do not implement child carriers. |
remark-stay |
Reads stays and records annotations in file.data.stay; never writes marker comments. |
rehype-stay |
Sets hProperties.id for HTML emission; never writes marker comments. |
plate-stay |
fromPlate writes block markers on separate lines; serializeStay delegates to it. List-item and table wrappers are refused. |
Only Python reaches these child-carrier checks. Existing same-line block markers remain readable, and the core writers may refresh their hashes or replace duplicate ids in place. These rules do not guarantee unchanged rendering for arbitrary Markdown; see compatibility.
With markdown-it-py installed, the Python linters also emit the optional
OUTSIDE_SUBSET advisory when §5.1 and §5.2 disagree on a document's segmentation.
It is informational, does not block a commit, and its absence is not a rendering
guarantee. The JavaScript and Rust cores and the adapters do not emit this advisory.
One corpus, four full runners¶
The canonical and packaged Python runners each verify 469 core vectors and 32
optional rows vectors. JavaScript and Rust each verify 469 core vectors
and explicitly assert that they decline the 32-vector rows profile. These four
full runners fail on an unknown profile or missing expected vectors.
remark-stay has a separate category-routed parity harness over parse, lint,
diff, anchors, and resolve, plus the tree-specific tier. It inventories core
categories, but does not load or assert a decline for the optional rows profile.
It is not one of the four full runners. The other two adapters have their own
integration suites.
The row profile checks §3.4's placement decisions. Rendering comparisons require
parsers and run in a separate evaluation; they are not language-neutral corpus
assertions. The parser-dependent OUTSIDE_SUBSET advisory is likewise excluded
from core finding comparisons and tested in Python's own suites.
The CLI linters (Python, JavaScript, and Rust) exit non-zero on any error-level finding, so they fit a pre-commit hook or an agent's post-edit check.
To wire markstay into a repo as a commit hook rather than call it from code, see Get started. For the design rationale behind the spec these implementations cover, see the specification and the reference linter findings.