stenographers
v0.4.0
Published
Write an edited document back into the Markdown it was read from, keeping the author's bytes: a stenographer per node writes over the node's old slice in the author's style, every node that still means what it meant keeps its bytes, and only typed text is
Readme
stenographers
Write an edited document back into the Markdown it was read from, keeping the author's bytes. A stenographer is the writer of one kind of node with the old transcript in hand: it writes the node over its old slice in the author's style (the bullet character, the emphasis character, the indent, the fence), every node that still means what it meant keeps its bytes, and only typed text is escaped, for what its position could misread. It is not a serializer: mdast-util-to-markdown rewrites a document in its own style (measured on BelType's manual: 14.3 percent of the lines changed with the defaults), and a stenographer rewrites nothing it was not asked to.
Install
npm install stenographersWhat it is made of
Five entries (exports, no index):
stenographers/parse: the plain parse,parseMarkdown(text, markdown),markdownthe venue's options in core's shape ({ gfm: { autolinks, tables }, mark, supersub, figure, directives },directivesthe config'sdirectives:bindings over core's built-ins, theirtag,classNameanddefaultTitle): mdast-util-from-markdown with BelType's four syntax extensions (frontmatter, math, directive, GFM's parts in the order BelType registers them) and nothing else, the only parse whose positions are exact.stenographers/html: what Markdown means in HTML, mdast-util-to-hast's translation written as markup and parsed by the browser (hastOf,htmlOf,domOf), andmeans(node, mdast), the test that lets an unchanged node keep its bytes (a footnote reference's number, the document's count, is left out of it).stenographers/write:writeNode(element, { text, column, defs, markdown }), a node's new bytes over its old slice, for paragraph, heading, list (items inserted with their neighbour's marker, deleted with their gap, the list turned tight or loose, a task's box ticked), list item, figure, table (a cell rewritten alone, its padding kept where it fits; rows added from a neighbour's layout and deleted), blockquote (its>prefixes taken off and put back), directive container (read as core renders it, its tag, class and title paragraph; its fences kept, its label, content and title written, the title inside its attribute's quotes, any other attribute changed, added or removed alone), fenced code and inline content (text, em, strong, del, code, link and reference link, break, image, footnote reference, text directive, and BelType's==mark==,=k=mark==,~sub~and^sup^);writeFresh(element)for a block with no old bytes;escape. A node is read as the HTML element it means, never by a type name. A stenographer that cannot write its node answers null and its parent writes it; the last step is the lane change, the node written as raw HTML. A paragraph's or a heading's bytes are read back before they are given.stenographers/flow:writeFlow(blocks, old, file), a flow of blocks over its old bytes: the parent owns the gaps (the old gap before a block that follows the one it followed, a blank line otherwise; a deleted block's gap kept when it holds a definition or a comment; two lists that would read as one kept apart by an empty comment).stenographers/writer: Provenance's Markdown writer, by shape (markdownWriter({ markdown }):claims,opengivinglocate,write,shifted), withoutlineandwriteRun. A path is a JSON Pointer over the heading outline addressing a section's own run (its heading line and the blocks after it, up to the next heading of any rank); the value is the run's blocks, each[html, bytes at the base, unchanged, prior bytes, whether they stand]; onlyset. Bytes are counted here and nowhere else.
What it does not do
It holds no document, no file and no editor: what changed, and which old block a new one stands for, is the caller's (by node identity in an editor, by the bytes a block had in the writer). It imports no ProseMirror, no BelType and no Provenance: Provenance's writer is met by shape, as its spec asks. It needs a DOM (document, for <template> and element comparison): a page's, or in node one the caller installs. A table's columns and alignment do not change (the table changes lane) and a column is never re-padded; a directive binding's render hook is not run (its directive means the element core would give it without one), and a container's label is only its first paragraph after the title, told from the content by the content's count; a title holding markup changes lane; a parent's prefix is spaces only; a text run whose bytes are not its value (an escape, an entity) is written fresh once touched; a list's items pair with the old by meaning from both ends, so an item edited in the same change as an insertion beside it is written fresh, and an inserted item in an ordered list does not renumber the items below; a loose list turned tight keeps the blank lines inside its items, so it changes lane when they would keep it loose. The notations are read from values, as core reads them, so an escaped delimiter is a delimiter: typed text holding a pair of ^, ~ or ==, which no escape keeps text, makes its paragraph change lane.
Extracted from
The studio, beltype-studio-experiment's src/markdown/stenographers/ at commit 3a526b0 (branch round/markdown-1), as prosemirror-stores was: built and proved there against BelType's manual, the pages and Provenance, then moved when green. The plan is the studio's notes/markdown-plan.md, the record of its round notes/markdown-results.md. The studio keeps what is its own: the read into its schema, the file as a client of its authority, saving, the venue host and the Markdown pane.
Proved by
npm test, under node:test with happy-dom as the DOM (a dev dependency only): 109 tests. Over BelType's manual (fixtures/, its nine files as they were on 2026-10-29, the manual's GFM options): every paragraph, heading, list and list item, written from its style alone, is its slice byte for byte, and every other top-level block keeps its bytes; a key typed at the end of every top-level paragraph and heading reads back as the edited node with only the typed bytes changed (after a colon, the colon escaped too). The writer: the outline's paths and their escapes and ordinals, locate in bytes over UTF-8, every run of a file written again from its own value equal to its bytes, one paragraph changed changing only it, a malformed value refused. The list (test/list.test.js): items inserted, deleted, ticked, a list turned tight and loose, each in exact bytes and read back, on small lists and on every tight list of the manual. The figure (test/figure.test.js): core's figure read, none in a tight list item, caption, alt, source and id each rewritten alone. The notations (test/dialect.test.js): core's elements and classes, positions through escapes, typing inside and beside them, what they cannot carry written as HTML. The containers (test/table.test.js, test/quote.test.js, test/directive.test.js): every table and directive of the manual with one cell or one paragraph changed writes only those bytes; directives read as core renders them, titles and attributes written alone; quotes on small cases, lazy lines and nesting; each with the element serialized without the line ends between its elements. test/review.test.js: the review's findings, one case each. test/layers.test.js: every import is a sibling or a package of the Markdown parse.
