@gmb/bitmark-parser - v7.21.2
    Preparing search index...

    @gmb/bitmark-parser - v7.21.2

    A high-performance parser and generator for bitmark. Provides a programmatic API (powered by WebAssembly) and a CLI (prebuilt native binary per platform) for converting between bitmark markup and JSON.

    npm install @gmb/bitmark-parser
    
    import {
    convert,
    semanticTokens,
    breakscapeText,
    unbreakscapeText,
    info,
    version,
    } from "@gmb/bitmark-parser";

    // Convert bitmark to JSON — set inputFormat explicitly when known (skips
    // auto-detection); "auto" also works but costs an extra scan
    const json = convert("[.article]\nHello **bold**", {
    inputFormat: "bitmark",
    warnings: true,
    });
    const parsedJson = JSON.parse(json);
    console.log(parsedJson[0].bit.type);
    console.log(parsedJson[0].parser.warnings);
    console.log(parsedJson[0].bitmark);

    // Generate bitmark from JSON
    const bitmark = convert(json, { inputFormat: "json", outputFormat: "bitmark" });

    // Parser-derived highlighting in the LSP semantic-tokens shape
    const highlights = semanticTokens("[.article]\nHello **bold**", {
    tokensLayout: "absolute",
    });

    // Dump the lexer's token stream (debug tooling; bitmark input only)
    const tokens = JSON.parse(
    convert("[.article]\nHello **bold**", {
    inputFormat: "bitmark",
    outputFormat: "lex",
    }),
    );

    // Breakscape / unbreakscape text
    const escaped = breakscapeText("[!example]", {
    format: "bitmark++",
    location: "body",
    });
    const unescaped = unbreakscapeText(escaped, {
    format: "bitmark++",
    location: "body",
    });

    // Query supported bit types
    const bitInfo = info({ infoType: "list", format: "text" });

    // Get library version
    console.log(version());

    The package ships three wasm builds of the same engine and selects one at runtime — the API surface never changes:

    Feature Contents Size (approx.)
    full (Node default) everything: rich info metadata + built-in translations + the semantic diff + the incremental document (createDocument) ~1314 KB (~531 KB gzip)
    browser-full (browser default) the same conversions, diff and createDocument, without the metadata and translations ~1109 KB (~469 KB gzip)
    bitmark-json bitmark ↔ JSON only: convert/canonicalize/transform (formats auto/bitmark/json, plus the text, semantic-tokens and diagnostics outputs), the editor services, bit templates, info (JSON only), breakscape, text fragments — no diff, no createDocument ~761 KB (~318 KB gzip)

    The rule. Every build includes every feature, except the two browser-targeted variants, which each carry an explicit exclusion list. That is the whole story — a new capability is in every build unless it is named below:

    build features
    native CLI, daemon, docgen, schemagen, wasm full all
    wasm browser-full all except info-meta (bit + tag prose) and translations (the baked table)
    wasm bitmark-json all except the above and markup, mapping-report, diff, lex (with ast), info-text, document (the incremental document, ~25 KB gzip)

    So, not in bitmark-json: the markup formats (html, xml, xml-niso-iec, …) as input or output (they throw an unsupported error), the lex output format and the mappingReport option (same error), diff and createDocument (throw UnsupportedFeatureError), and the human-readable rendering of info — info({ format: "text" }) throws UnsupportedFeatureError there, so pass format: "json" (the JSON views are the contractual ones and are in every variant).

    full and browser-full convert identically — they differ only in what info can report: the META fields (bit and tag descriptions, group provenance and raw mapping patterns) and the built-in translations table. Bit titles and the bit-group / resource-group catalogs are in every variant, in English; register supplies the texts — descriptions and translations — to the lean ones (see Display names, descriptions and languages).

    Why the defaults differ: only the browser pays a download-latency cost for those strings, and a Node backend loading wasm from disk has the same requirements as the native CLI. But which channel an artifact ends up in is not knowable from the artifact — browser code is routinely package-managed and webpack-built — so both variants are available on both platforms and the defaults are conveniences only. If you bundle the Node entry for the browser, select browser-full explicitly.

    Migrating from ≤ 6.10: full used to mean what browser-full means now. Browser code that explicitly selected "full" should move to "browser-full" to keep its download size close to what it was; leave it on "full" to gain the metadata and built-in translations for ~62 KB gzip. Nothing else changes — the capabilities that were in full are in both.

    import { init, convert, variant } from "@gmb/bitmark-parser";

    await init({ feature: "bitmark-json" }); // load the minimal wasm
    convert("[.article]\nHello"); // works
    variant(); // "bitmark-json"

    // Upgrade in place: the interface stays valid while the full wasm loads —
    // calls keep being served by the active module, then swap atomically.
    await init({ feature: "full" });
    convert("[.article]\nHi", { outputFormat: "html" }); // now works

    Notes:

    • Node: with no explicit init, the first call lazily loads full synchronously — existing zero-init usage is unchanged. initSync({ feature }) selects a variant synchronously.
    • Browser: init() is required as before, and loads browser-full; only the selected variant's .wasm is fetched (all three small JS glue modules are in the bundle). Overlapping init calls: same-feature calls coalesce; a different-feature call supersedes an unfinished one (last call wins); every promise resolves once the finally-active module is live. init({}) is the same request as init(). In the browser, initSync(bytes, { feature }) instantiates from bytes you supply, and takes part in the same last-call-wins order: an init still loading when initSync runs never swaps in afterwards.
    • Supplying your own .wasm: glue and module are a pair, so name the feature the bytes belong to — init({ feature, module_or_path }). The back-compat positional form init(module_or_path) stays on full, since those bytes are bitmark_wasm_bg.wasm.
    • Workers: transformParallel workers load the active variant.
    • Legacy API: works on bitmark-json (it follows the active module) except its HTML-dependent members (e.g. convertHtmlTable), which throw.
    • A previously active module's reference is dropped on swap; wasm-bindgen's glue retains the instance until that variant is re-initialised, so switching back is cheap but the memory of an abandoned variant is only reclaimed on page/process end or its re-init.

    Errors are thrown, never returned. Every fallible WASM export raises a JavaScript Error whose message has one shape:

    <kind> at <path>: <message>
    

    A returned string is therefore always a result — there is no prefix or sentinel to test for, and every call site is a try/catch.

    <kind> is a stable kebab-case identifier — the part to match on. <path> locates the failure (a JSON pointer-ish path, offset N for a JSON syntax error, or empty) and <message> is human-readable prose that may change in any release. The kinds:

    kind meaning
    invalid-json the input string is not well-formed JSON
    compose the reverse composer rejected the JSON (shape / field / type)
    unsupported the requested conversion is not available (an output-only format used as input, or a format compiled out of this variant)
    patch-parse-error a patch or patch document is malformed
    patch-index-out-of-range a patch path addressed an array element that does not exist
    patch-type-mismatch a patch value's JSON type is incompatible with its target
    patch-conflict a patch document's entries contradict each other (a bit removed or moved twice, or an anchor that is itself removed or moved)
    document-version createDocument: a change's version is not newer than the document's (thrown as DocumentVersionError)
    document-range createDocument: a change's range is not in the text
    document-view createDocument: no open view has that id

    Every TypeScript wrapper propagates that Error unchanged — convert, transform, canonicalize, bitmarkToObjects, countBits, diff, … — as does the parallel worker pool, which rejects with the first failing bit's error. register re-wraps it as a RegisterError; a capability the loaded variant compiled out raises UnsupportedFeatureError before any wasm call.

    Convert between bitmark, JSON, and the mapped markup formats (html/xml/…).

    A JSON input may be an array or a single value, each entry a { bit: {…} } envelope (the forward output) or a bare bit object. An entry that is not a bit produces no bitmark: it is skipped and reported as a bit-invalid issue (or bit-type-invalid when it has a type the registry does not know) naming its JSON path ($[2] is not a bit: expected a bit object with "type" — ignored) on the warnings channel of the CLI (--warnings) and the daemon; the bits beside it still convert, and a document in which nothing is a bit converts to empty output. This convert returns a bare string and so cannot show the skip. Only malformed JSON text throws (invalid-json).

    Options:

    • inputFormat — "auto" (default), "bitmark", "json", "jsonl", or a mapping id ("html", "xml", …)
    • outputFormat — "auto" (default: the opposite direction), "jsonl" (the JSON entries one per line: compact, each line ending in \n, pretty does not apply; reads back like JSON, a bad line throws naming it, and "auto" detects it), "text" (lossy plain-text extraction; output-only), "semantic-tokens" / "diagnostics" / "lex" / "ast" (output-only, bitmark input only — see their sections), or as above
    • mode — "optimized" (default) or "full"
    • warnings — include validation warnings (default: false)
    • plainText — output text as plain text (default: false)
    • pretty — prettify the emitted JSON (default: false). For JSON output this is the whole document; for bitmark output it pretty-prints the embedded JSON bodies of json-format bits (cosmetic — they re-parse to the same value)
    • indent — indent size for pretty JSON (default: 2)
    • inlineStyle — bitmark output only: how inline marks are written — "classic" (default: **bold**, ==x==|bold|) or "toggle" (toggle tags, (+bold)x(-bold), which may overlap). A mark with only one spelling keeps it: semantic and highlight search are toggles in either style, the colour-baked names (highlightYellow) classic in either. A block whose toggle spelling would not read back exactly as its classic one is written classically. Also on canonicalize, transform, objectsToBitmark and the CLI's --inline-style
    • includeUnknownProperties — bitmark → JSON only (default: false): include unknown properties in the bit JSON. An unknown property ([@key:value] the bit's config cannot resolve, at the bit body/footer — never inside cards or consumed by a chain) is appended after all real keys as an array of strings (a valueless [@key] → true); a key colliding with a configured key of the bit is _-prefixed. A bare tag the bit does not declare ([!…], [?…], [#…], … on a bit whose config has no such tag) is an unknown too, under the key a declaring bit would use ([!x] → "instruction": ["x"]); a rejected resource attachment is not — it is still emitted, with a warning. Unknown properties are never converted back to bitmark, and each occurrence warns whether or not it is included. Also available on bitmarkToObjects and as the CLI's --include-unknown-properties
    • mappingReport — replace the converted output with a human-readable MAPPING REPORT of what each input construct mapped to, including the inputs that are NOT mapped (default: false). The report lists every input occurrence, grouped by bit (mapped → its output construct, carried → what consumed it, or NOT MAPPED), a per-name summary table, and a NOT-MAPPED inventory. Composite conversions (e.g. xml → json) report every leg, one section after the other. Most useful on foreign document import (inputFormat: "xml-niso-iec"), where NOT MAPPED means recorded loss. Also the CLI's --mapping-report

    Performance: inputFormat: "auto" detects the format by scanning the input for a bitmark bit header — for non-bitmark input that is a full pass over the whole input before parsing starts, and the detection is a heuristic (markup formats are never auto-detected). If you know the input format, set inputFormat explicitly. The same applies to canonicalize and transform, whose inputFormat also defaults to "auto".

    Input limits: JSON objects/arrays and markup elements may nest at most 256 levels deep. Deeper JSON is rejected like any other malformed JSON ("error: … nesting depth exceeds 256"); a deeper markup open tag is kept as literal text. The bound keeps a crafted document from exhausting the WASM stack — an instance that rejects a too-deep input stays usable for the next call.

    convert is the single string-based conversion surface — parsing is { inputFormat: "bitmark", outputFormat: "json" }, generating is { inputFormat: "json", outputFormat: "bitmark" }. For typed objects use bitmarkToObjects / objectsToBitmark (below), which pin their formats.

    For bitmark input, output is a JSON array of entries with:

    • bit — serialized bit payload
    • parser — parser metadata, errors / warnings, and infos (informational notices for recoveries that are not necessarily wrong: text kept as text because it only looks like an unclosed tag, tag-unclosed, or a formatting mark with no partner, formatting-unclosed — in bitmark malformed markup is text, not an error). A reference whose target is not in this document, text-tag-target-missing, is reported by diagnostics() only, never here. Each issue carries a stable machine-readable code ("tag-invalid", "tag-missing", …) and a scope ("bit", "card", "bit-chain" or "card-chain" — where the issue sits) beside its human-readable message, plus text and location. Branch on code and scope — a shipped code never changes meaning, whereas the message text is not contractual and may be reworded in any release. bitmark info error-codes lists every code with its severity and message template (see info below)
    • bitmark — source bitmark text (a convenience echo; every bit is fully described by bit alone)

    A region the parser could not read — an unknown bit type, or non-blank text that never opened a bit — comes back as a bit of type _error beside its parser.errors entry. It carries originalType (the header exactly as written between the level dots and the ], including a leading | for a commented-out bit and any :format / &resource suffix; absent when there was no header) and body (the raw text after it, as a plain string), so generating bitmark from it reproduces the original region.

    The other direction reports too. Going to bitmark (from JSON or from markup), an inline mark that has no bitmark form is dropped — the forward parser would not read it back as that mark, so writing it would corrupt the surrounding text. The text it decorated is always kept, and the loss is reported: mark-dropped when the whole mark goes, mark-reduced when it survives in a lesser form (its value cleared, or one of its segments dropped). Both arrive on the conversion's warnings, like every other issue. In practice they are rare — a href holding a line terminator, a mark kind the grammar has no tag for — so treat either as a signal that the JSON holds something bitmark cannot express, not as routine noise. Likewise list-number-invalid: a list start or item value that is not a valid number for its list (4000 on a Roman list), or a value on a bullet or task item, is ignored and reported.

    Reading bitmark, an inline property list (==text==|link:…|?hint|) reports two things (PLAN-217): text-tag-cardinality-exceeded when a child is given more often than it may be (|?a|?b| — the later value wins), and text-tag-value-invalid when a value does not fit its format but is kept (|symbol:…|width:wide|).

    A reference (==see==|►sec-1|) must find an anchor with its value ([▼sec-1], or an inline image's |▼sec-1|) in the same document — one input is one document. A customer reference (|customer►sec-1|) must likewise find a customer anchor ([customer▼sec-1], |customer▼sec-1|); these carry a source format's own ids, while ▼ / ► stay the platform's. An unmatched value is noted once, as the info text-tag-target-missing, by diagnostics() on bitmark source (PLAN-229, PLAN-271): a diagnostic only, never in a conversion's output. An empty reference names nothing and is not noted. A reference into another document is not wrong, only unverifiable, so it is a notice, not a warning.

    convert, returning { output, …extras } — the same conversion, run once, with each extra asked for by a flag in the options. output is exactly what convert returns, and it throws exactly as convert does. Future extras are one more flag and one more field each.

    • bitSpans — true adds result.bitSpans, where each top-level bit's text is in the input and in the output, for linking the panes that show them bit by bit — whichever side of the conversion a pane is on:
    const { output, bitSpans } = convertWithDetails(source, {
    inputFormat: "bitmark",
    outputFormat: "html",
    bitSpans: true,
    });
    // bitSpans = { positionEncoding: "utf-16",
    // spans: [{ index, inputStart, inputEnd, outputStart, outputEnd }, …] }
    for (const { index, inputStart, inputEnd, outputStart, outputEnd } of bitSpans!.spans) {
    console.log(index, source.slice(inputStart, inputEnd), output.slice(outputStart, outputEnd));
    }
    • index is the bit's position in the input: its splitBits index for bitmark input, its entry index in the top-level array for JSON input, its position among the bits the import finds for markup input. An entry that is not a bit gets no span, so the indexes can have gaps.
    • Each side is the bit's text, tight: never the separators between bits (\n, blank lines, ,) nor the JSON array's [ / ]. Both are counted in positionEncoding — "utf-16" by default, so output.slice(outputStart, outputEnd) is the bit — or "utf-8" bytes.
    • inputStart / inputEnd: for bitmark input, the bit's splitBits start / end; for JSON, its top-level entry; for markup, the element it was read from — one that holds the next bit's element (a NISO section around its paragraphs) ends where that element starts. Converting an output back gives input spans equal to the output spans it was written with.
    • Output spans are reported for the document outputs: bitmark, json (compact or pretty), text, and the markup formats (html, xml, …). In bitmark output they equal splitBits(output)'s start / end.
    • A bit that writes nothing (an empty or _error bit in text) gets a zero-width output span where its text would start.
    • Only top-level bits get spans. For any other output (lex, ast, semantic-tokens, diagnostics, a mapping report) bitSpans is present with spans: []; without the flag it is absent.

    The CLI's --bit-spans FILE and the daemon's convert option bitSpans: true (reply field bitSpans) return the same object.

    • bitAnchors — true adds result.bitAnchors: fixed points inside each bit, for keeping linked panes aligned inside a long bit, where the bit spans alone are only its two ends:
    const { output, bitAnchors } = convertWithDetails(source, {
    inputFormat: "bitmark",
    outputFormat: "json",
    pretty: true,
    bitAnchors: true,
    });
    // bitAnchors = { positionEncoding: "utf-16",
    // anchors: [{ index, inputOffset, outputOffset }, …] }
    for (const { index, inputOffset, outputOffset } of bitAnchors!.anchors) {
    console.log(index, source.slice(inputOffset).split("\n")[0], output.slice(outputOffset, outputOffset + 40));
    }
    • An anchor pairs a source node's first character (inputOffset) with the first character of the value it produced (outputOffset): each body, footer and card block (paragraph, heading, list, image…), each list item, each card / side / variant / footer divider (====, --, ++, ==== footer ====) and each bit- or card-level tag (its [, to the value it wrote — for a repeatable tag, the element). Inline nodes have none.
    • Each bit also ends with an anchor: its last visible character, paired with the closing } of its bit value, so the parser / bitmark fields written after it fall between bits. It is always the bit's last anchor, so a consumer can tell it from the starts.
    • index is the bit, as in bitSpans. Anchors are ordered by index, then inputOffset; output offsets need not be in order — the configuration can write a tag's value before the body — so a consumer pairing panes keeps an in-order subset.
    • Reported for bitmark input to json (compact or pretty) or jsonl, and to HTML or XML (every markup output, compact or pretty), where an anchor's output offset is the < of the element the node produced and a bit's end is the < of its element's closing tag. A literal-format body is text, not an element, so it has none. For any other conversion bitAnchors is present with anchors: [], and without the flag it is absent. Offsets are counted in positionEncoding.

    The CLI's --bit-anchors FILE and the daemon's convert option bitAnchors: true (reply field bitAnchors) return the same object.

    A handle on one text for an editor (PLAN-271): send it the editor's changes and each open view — one output of the text — answers with the edits that turn its previous output into the new one. Only the bits a change touched are converted again, so a keystroke in a large document costs milliseconds where re-converting it costs hundreds; the views' outputs are still exactly what convert writes. The contract is .zen/specs/API-IDC-incremental-document.tsp.

    import { createDocument } from "@gmb/bitmark-parser";

    const doc = createDocument(model.getValue(), { version: model.getVersionId() });
    const json = doc.openView({ outputFormat: "json", pretty: true, bitSpans: true });
    jsonModel.setValue(json.output!);

    model.onDidChangeContent((e) => {
    // Monaco's ranges are 1-based; LSP's (and the handle's) are 0-based.
    const changes = e.changes.map((c) => ({
    range: {
    start: { line: c.range.startLineNumber - 1, character: c.range.startColumn - 1 },
    end: { line: c.range.endLineNumber - 1, character: c.range.endColumn - 1 },
    },
    text: c.text,
    }));
    const update = doc.applyChanges(model.getVersionId(), changes);
    for (const view of update.views) {
    // Every range is in the view's previous output: apply them together.
    jsonModel.applyEdits(view.edits.map(toMonacoEdit));
    }
    monaco.editor.setModelMarkers(model, "bitmark", doc.diagnostics().diagnostics.map(toMarker));
    });
    // … later
    doc.free();
    • options: inputFormat ("bitmark" by default; "json", "jsonl" and the markup formats too), version (the editor's document version, default 0) and positionEncoding ("utf-16" by default) — every position in and out is counted in it.
    • openView(options) — options are convertWithDetails' (outputFormat: json, jsonl, text, bitmark or a markup format, plus mode, pretty, indent, spacesAroundValues, inlineStyle, includeUnknownProperties, bitSpans, bitAnchors). Returns a ViewUpdate with the whole output. closeView(view) closes it.
    • applyChanges(version, changes) — LSP content changes ({ range, text }, or { text } for the whole text), applied in order. Returns { views, inputError? }: one ViewUpdate per open view, with its edits (LSP TextEdits, ranges in its previous output) and, when asked for, its whole bitSpans / bitAnchors. A version not newer than the document's throws DocumentVersionError and changes nothing: re-send the text with setText(version, text), whose updates hold whole outputs.
    • JSON or markup input that does not parse (or parses but does not convert) changes no view: views is empty and inputError ({ message, range }) says where the text fails; every view keeps its last good output. When the text converts again, the edits lead from those last good outputs.
    • semanticTokens(range?, { tokensLayout }) — the tokens on range's lines (or of the whole text), and diagnostics() — the whole document's, the link check included. Bitmark input only.
    • JSON input is read in its entries and jsonl in its lines, so a change inside one entry converts only it; a markup export read back (each bit an element of its own) likewise. Other markup — a NISO document, whose bits come from nested elements — converts whole on every change, with the same answers.
    • The handle lives in wasm memory: call free() when done (a FinalizationRegistry frees a forgotten one when it is collected). It stays bound to the wasm variant that was active when it was created. Not in the bitmark-json variant (UnsupportedFeatureError).

    Re-emit input in its own format in canonical form — the single same-format surface. mode: "optimized" (default, omit natural defaults) or "full" (all keys).

    Apply a patch document to a document, bit by bit, and re-emit it. transformParallel is the worker-thread variant returning a Promise<string>. Each bit is converted on its own and the parts are joined as convert joins them: with no patch and no hook, transform writes exactly what convert writes (PLAN-271), hooks or not.

    • patch — a PatchDocument, or the shorthand: a bare array of entries applied to every bit (build entries with patchEntry). Either form may be a JSON string. An entry's path is in one of two languages, told apart by its first character (one language per call; a mixed call is refused):
      • a JSON path addresses the bit JSON (title, quizzes[0].choices[1].choice, table.data[1][1], id[-1]) and the value is a JSON value;
      • a bitmark path addresses the bit as an author writes it and the value is bitmark source (see Bitmark paths below). Bitmark paths need bitmark input
    • a skipped entry — a bad path, an index out of range, a value the engine refuses, a precondition the target no longer meets — throws PatchError (diagnostics: { path, kind, severity, message }[]) rather than returning a document with the edit silently missing. Warnings (severity: "warning": patch-base-changed, patch-already-resolved) report changes that were applied and never throw
    • patch may also be a string of bitmark change proposals — see Change proposals below — applied to the input
    • applyProposals — apply the change proposals the input itself carries and mark each applied one resolved, recording date (default: today, UTC) and by
    • preHook / postHook — per-bit hooks; the pre-hook may return patches, an output-format override or drop. Hooks see the original bits, in order, before the document reshapes anything; a hook's patches apply after the document's
    • inputFormat, outputFormat, mode, pretty, indent, spacesAroundValues — as for convert. outputFormat (default "json") takes every per-bit format: "json", "jsonl", "bitmark", "text" or a mapping id such as "html" (per-bit text outputs are joined with newlines). The whole-document outputs "lex", "ast", "semantic-tokens" and "diagnostics" are refused with an unsupported error in every driver — use convert. An explicit inputFormat is honoured as given, patch or hooks or not; it is never re-detected from the content

    A patch document addresses bits by their position (or id) in the input before any entry is applied, so it reads like a diff. An inserted or replacing bit is a JSON bit, or a string holding one bit of bitmark source:

    {
    "version": 1,
    "entries": [
    {
    "select": { "index": 3, "id": "mc-014" },
    "patch": [{ "path": "title", "op": "set", "value": "New" }]
    },
    { "select": { "index": 9 }, "remove": true },
    { "select": { "index": 5 }, "bit": "[.note]\nIn its place" },
    {
    "insert": { "before": 4 },
    "bit": { "type": "article", "body": "Inserted" }
    },
    { "insert": { "before": 12 }, "bit": "[.article]\nAt the end of 12 bits" },
    { "select": { "index": 7 }, "move": { "before": 2 } },
    { "select": "all", "patch": [{ "lang": "de" }] }
    ]
    }

    A position (before: n) is "before input bit n", n from 0 to the bit count — the count is the end. It is a slot of the input, so it stays valid when bit n is itself removed or moved: it is where that bit was. A select with a bit and no insert replaces the bit in place; the slot keeps its patches. A bit may be the target of at most one remove, move or replacement entry: a second claim of any kind is rejected, naming the second of the conflicting entries. diff produces such a document from two versions of a file (below).

    A bitmark path spells the thing it names the way an author writes it, so the same path works for every bit type and no JSON key name is needed:

    Path Addresses
    [@id], [!], [#], [&image] the bit's first such tag; [@id][-1] the last, [-][1] the second [-…]
    [&image][@width] [@width] in the run after [&image] — a chain is spelled like the chain
    $body the bit's own text (tags, cards and footer untouched)
    $raw everything after the bit header
    $card[1] the whole second card; $card[1][0] its first side, $card[1][0][1] a variant
    $card[1][!], $card[1][-][1] tags inside a card, by occurrence
    $card[1]$body, $card[1]$raw a card's own text / whole content
    $footer the ==== footer ==== section

    Values are bitmark source, breakscaped as an author would write it (a bare ] or a trailing ^ in a tag value is refused — use breakscapeText with location: "tag" for user data, and add subLocation: "attrValue" when the value is an inline-attr chain value such as a link: href); null is a valueless tag ([@x]). The ops are JSON Patch's (RFC 6902), with one addition. add puts the value where the path points: a member — $body, $raw, $footer, a section's content — is created or replaced; an indexed path ($card[1][-][1], $card[2]) gets the value INSERTED before the element there, an index equal to the count being the end ($card[2] on a two-card bit is a new last card, as is a bare $card); an unindexed tag path adds another occurrence after the last one on its own line ($card[1][-] twice gives white, green, blue, red). With add every slot is reachable, so a reorder is remove + add. remove removes a tag (a run head takes its whole run — [&image:u][@width:1] minus [&image] leaves nothing behind), a section, a body. replace replaces a tag's value, a section's content or a scope's text, and needs an existing target; on a tag, a value that is one whole tag ("[## Heading]" at [#]) replaces the tag as written, marker included. test changes nothing: the value at the path must still read as the value (whitespace runs aside), else the entry is refused as patch-conflict — the target changed since the entry was written. substitute, the one op outside the RFC, swaps the entry's find fragment, found exactly once in the addressed text, for the value. For add on a tag, a value that is one whole tag ("[+ orange]") goes in as written — any sigil in any slot; other text is the value of a tag of the path's own kind.

    On JSON paths the same four RFC ops apply: add creates or replaces a key, or inserts before an array index (id[-], the RFC's -, is the end); test compares a JSON value by equality (a string with whitespace runs aside; absent reads as empty); substitute is bitmark-only.

    Every edit is a splice of the bit's own source: the parser's tree is the address map, the rest of the bit keeps its formatting, and an edit that would change the structure outside the addressed scope — a ==== text ==== or ==== footer ==== that would swallow the cards after it, a bit header in a value — is refused with the bit untouched. convert(…, { outputFormat: "ast" }) shows the tree a path resolves against.

    transform(doc, {
    outputFormat: "bitmark",
    patch: [
    { path: "$card[1][-]", op: "append", value: "green" },
    { path: "$card[0][!]", op: "set", value: "Milk (cold)" },
    { path: "$body", op: "set", value: "Think **twice**." },
    ],
    });

    Semantic diff of two documents. Both sides are re-canonicalized and compared as bit JSON, so tag order, spacing, breakscaping and omitted defaults are not differences; every real change is rendered as the canonical bitmark an author would type. The inputs may be bitmark or JSON, and need not share a format.

    @@ bit 1 -> 2 (id mc-014) [.multiple-choice] changed
    instruction[0]
    -[!Pick one]
    +[!Pick exactly one]
    quizzes[0]
    ====
    [-Bonn]
    -[-Berlin]
    +[+Berlin]
    @@ bit 4 (id hero) [.image] removed
    -[.image]
    -[@id:hero]
    -[&image:https://example.com/hero.png]

    Bits are paired by id, then by sourceBB on the same sourceRL, then by exact content, then by text similarity; a bit whose position changed among the kept ones is reported moved. Empty output means the documents are semantically equal.

    • outputFormat — "bitmark" (default; the text above), "json" (one object per aligned bit, see diffBits), "patch" (a patch document that turns a into b — feed it to transform), or "bitmark-patch" (the same patch as change-proposal bits, one per change — see Change proposals)
    • paths — the path language of a "patch" output: "json" (default) or "bitmark" (bitmark inputs). A bitmark-path patch is derived from A's source and verified: each bit's entries are applied to A and kept only when the result means B — fine tag and text edits first, whole cards next, the bit's whole content last
    • pairLeftovers — after every other stage, pair the bits still unpaired by type, in order, when both sides have as many of that type; a bit with an id takes no part (default: true)
    • inputFormat — applies to both sides (default: "auto", sniffed per side)
    • context — unchanged lines kept around each change (default: 3)
    • locate — add source line numbers to bit positions (bitmark inputs; default: false)
    • similarity — pairing threshold for bits with no id, box or exact match, 0..1 (default: 0.6; 1 disables)
    • bboxTolerance — pixels per coordinate for the sourceBB stage (default: 8)
    • spacesAroundValues — in the rendered bitmark lines (default: 0)

    The text layout and the JSON shape are stable contracts. A key the generator writes only together with others (a heading's level, inside its title tag) is shown in place, as the line reads (-[#Heading] / +[##Heading]). An unknown property is shown as a JSON line, since the generator cannot render it, and cannot be re-applied. In the JSON ops / revert, a value present on both sides is a replace and a new key an add. Not available on the bitmark-json variant (UnsupportedFeatureError).

    A change proposal is a bit that carries an edit to another bit — written by a validator, a reviewer or diff(…, { outputFormat: "bitmark-patch" }), applied by transform. The bit types are config: [.bit-diff], and [.review-request-for-review-note] (a review note with a proposal). Each ==== patch ==== card (or plain ====) is one edit, applied in card order:

    [.review-request-for-review-note]
    [@diffRef:mc1]
    [!Fix the choices and the hint]
    ==== patch ====
    [@diffOp:substitute][@diffAt:body][@diffFind:think twice.]
    --
    think three times.
    ==== patch ====
    [@diffOp:add][@diffAt:card/1/-/1]
    --
    [+ orange]
    ==== patch ====
    [@diffOp:replace][@diffAt:raw]
    --
    [% item ][% lead ]
    Anything goes here: [tags], **marks**, -- and ++ lines.
    ^====
    [! A card ]
    • @diffRef — the target: an id, or #n for the n-th bit of the document (counting from 0; n equal to the bit count is the position after the last bit, for an add at bit only)
    • @diffBase — the target's fingerprint when the proposal was written; a mismatch on apply is a patch-base-changed warning (the test cards decide)
    • @diffOp — add, remove, replace, test or substitute; at the path bit also move
    • @diffAt — a bitmark path, bracket-free so it fits in a tag: card/1/-/1 is $card[1][-][1], %/1 is [%][1], &image/@alt is [&image][@alt], body is $body, raw is $raw; bit is the whole target bit (bit/after: the slot after it, for add and move)
    • @diffFind — for substitute: the text to find and replace
    • the value — after --, written exactly as it appears in the target (for test: the value expected there): the side is read verbatim, so tags, marks, -- and ++ need no escaping. Two escapes, for what would otherwise end the card: a line starting with ==== (a value holding cards) is written ^====, and a line starting with [. (a value holding a whole bit) is written [^. — one ^ more per level, the same rule as plain-text bodies

    A whole-bit card (@diffAt:bit, the last card of its proposal) is the proposal's document-level op, with the same powers as a patch document: remove removes the target; replace replaces it, header included (a type change); add puts the value — a whole bit — before it; move puts the target before the bit the value names (#0: first; #n with n the bit count: last). Positions (#n) always mean the input's, whatever earlier proposals did.

    To put a new or moved bit AFTER a bit instead, write the path bit/after (bit/before is accepted as the same as bit): an add lands after the target, a move after the bit its value names. @diffRef still names the bit the proposal is about — a transcript added after its audio bit:

    [.bit-diff]
    [@diffRef:audio-1]
    ==== patch ====
    [@diffOp:add][@diffAt:bit/after]
    --
    [^.audio-transcript]
    The words of the interview.

    A proposal applies as a whole or not at all: a failed card, a precondition the target no longer meets, an edit that would leave the target with a new error or warning (a tag its bit does not allow), or a whole-bit card the document engine refuses (a bit removed or replaced by an earlier proposal, a bit moved twice) leaves the target untouched and throws PatchError naming the card. With applyProposals, each applied proposal is marked resolved (@resolved, @resolvedDate, @resolvedBy); a resolved one is not applied again.

    const fixes = diff(oldDoc, newDoc, { outputFormat: "bitmark-patch" });
    transform(oldDoc, { patch: fixes, outputFormat: "bitmark" }); // means newDoc
    transform(book, { applyProposals: true, by: "validator", outputFormat: "bitmark" });

    The typed form of diff: one BitDiff per aligned bit, unchanged ones included so the alignment is complete — status (unchanged | changed | moved | removed | added), the a/b positions, ops (a patch turning the A bit into the B bit) and revert, the parser issue codes gained or lost, and the rendered hunks.

    splitBits returns one { index, start, end, byteStart, byteEnd, source } per bit. start / end locate the bit's text, source, counted in options.positionEncoding — "utf-16" by default, so input.slice(start, end) === source — or "utf-8" bytes. byteStart / byteEnd are UTF-8 byte offsets that tile the document (each bit runs to the next bit's start), for chunking.

    splitBits splits bitmark source into its bits without full parsing; countBits counts the bits of a bitmark, JSON or JSONL input. A JSON input may be an array or a single value, each entry a { bit: {…} } envelope or a bare bit object (the four shapes convert accepts). An entry that is not a bit is not counted — countBits, transform and diff work on the recognised bits and have no warnings channel; convert reports the skip.

    Parser-derived highlighting of bitmark source in the LSP semantic-tokens shape — the typed form of convert(input, { inputFormat: "bitmark", outputFormat: "semantic-tokens" }). The parser runs (never the validator; diagnostics stay on their own channel) and the AST is walked loss-lessly, so every character of the source is covered exactly once and the highlighting agrees with the JSON the same input produces: an unpaired ** is text, an unclosed [!tag is a tag flagged unclosed that ends at the ==== / -- after it, the body of a bit whose body format is not bitmark ([.code], ==== text ====) is plain text. Bitmark input only.

    Options (SemanticTokensOptions, also ConvertOptions keys):

    • positionEncoding — "utf-16" (default; the LSP default, what browser editors index by) or "utf-8" (bytes). Echoed in the response.
    • tokensLayout — "lsp" (default): data, the LSP relative encoding, five integers per token (deltaLine, deltaStart, length, tokenType, tokenModifiers; the type is a legend index, the modifiers a bitset); or "absolute": tokens, one { line, start, length, type, modifiers } per token, 0-based, by name. Both carry legend and positionEncoding.

    Lines break at \n, \r\n and \r; no token contains a line terminator. The legend orders are stable (a change is semver-major; new entries may be appended). Token types name sigil classes and syntax positions, never a bit or tag name; each has a recommended standard super type so an editor's default theme colours it — paste the table into a VS Code extension's semanticTokenTypes:

    type covers super type
    frontmatter the region before the first bit comment
    bitSigil [., level dots, |, :, &, ] of a bit header keyword
    bitType / bitFormat / bitResourceType the header's three fields type / modifier / type
    tagSigil every tag's own syntax ([@, [!, [##, :, ], …) keyword
    propertyKey / resourceType a property tag's key / a resource tag's type property / type
    tagText plain text inside any tag value string
    cardDivider / sideDivider / variantDivider / footerDivider / textDivider ====, --, ++, ==== footer ====, ==== text ==== operator
    paragraphBreak a | paragraph break operator
    headingSigil / heading # / the heading's plain text keyword / string
    listMarker • , •1 , •+ , the indent keyword
    imageSigil / imageSrc |image: / the source keyword / string
    markSigil a mark's open and close markers operator
    bold / italic / highlight / light / inline the marked text (in body text and bitmark++ values; a string value is one literal tagText) string
    attrSigil / attrKey / attrValue an attr chain's | and : / key / value operator / property / string
    url a bare URL the parser auto-linked string
    text plain body / card / footer text, and every top-level gap (none)
    plainText a plain region (after ==== text ====, or a raw body) string

    Modifiers, in bit order: the tag classes property, resource, title, reference, anchor, item, instruction, hint, true, false, gap, mark, solution (every token of a tag of that class, sigils included — exactly one per tag); comment (every token of a commented-out bit); unclosed (tokens inside a parser unclosed-mark / unclosed-tag notice); bold, italic, highlight, light (the text of an inline mark ==…==\|…\| whose chain carries that bare key — style it as you style the bold / italic / highlight / light token types). The full contract is .zen/specs/API-SEM-semantic-tokens.tsp.

    Every issue the JSON parser envelope would carry for bitmark source, as LSP 3.17 Diagnostics with editor positions — the typed form of convert(input, { inputFormat: "bitmark", outputFormat: "diagnostics" }). Parse and validate, no serialization: cheap enough for every keystroke. The validator owns every issue, so this is exactly the envelope's set — the same codes, the same spans, the same order (bit by bit) — with the envelope's buckets as LSP severities: errors → 1 (DiagnosticSeverity.Error), warnings → 2, infos → 3. Bitmark input only.

    import { diagnostics, DiagnosticSeverity } from "@gmb/bitmark-parser";

    const { positionEncoding, diagnostics: list } = diagnostics(source);
    // list[i] = {
    // range: { start: { line, character }, end: { line, character } }, // 0-based, UTF-16 units
    // severity: 2, // DiagnosticSeverity.Warning
    // code: "tag-invalid", // the contract — branch on this
    // source: "bitmark",
    // message: "[@nope] is not valid for [.article]", // prose, may change
    // data: { bit: 0, scope: "bit" }, // the bit's index in the JSON array; the issue's scope
    // }

    Option: positionEncoding — "utf-16" (default) or "utf-8", as for semanticTokens; echoed in the response. The types (Diagnostic, Range, Position, DiagnosticSeverity) are written field for field as the protocol defines them, so a Diagnostic assigns to Monaco's marker input and to vscode-languageserver-types without a cast, and the package keeps its zero runtime dependencies. In every wasm variant. The contract is .zen/specs/API-EDT-editor-services.tsp.

    The editor position queries, in LSP 3.17 shapes (PLAN-196, PLAN-202). Positions are 0-based { line, character } in positionEncoding units — "utf-16" by default, as for semanticTokens.

    complete returns an LSP CompletionList of everything valid at the cursor:

    where the cursor is what you get
    a bit header's type ([.art) every bit type (kind: Class), with its title
    a header's :format / &resource the body formats / the resource types this bit allows
    a tag, or body / card / footer text the tags valid in THAT scope — the bit body, a card variant, a chain — minus those already at their maximum, the ones the scope still requires sorted first (the first preselected)
    a tag's value an enum tag's vocabulary, a boolean tag's true / false
    after a tag's ] that tag's chain children
    an inline attribute chain (==x==|bo) the attribute keys, and the closed value set of one that has it
    a line holding a prefix of one (empty, =, ==== f) the structural lines the bit's card set allows there (====, --, ++), ==== footer ====, ==== text ==== — each documented
    bitmark text (body text, a bitmark++ tag's value), after none, = or == the inline mark ==…==|…| as a snippet (insertTextFormat: 2); never in a string value or a plain region

    Items carry kind, detail, tags: [1] when deprecated, sortText, insertText, and — in LSP's own data slot — the info record behind them, so you can build UI from fields rather than prose. isIncomplete is always false: every candidate for the context comes back and filtering by the typed prefix is the editor's job. A half-typed [@wi, [.art or ==x==\|bo resolves as if it were closed. A header resource attachment ([.flashcard&image]) is offered at the bit's level until it is there; a tag whose label is only its sigil (%, !) leads its detail with its name (item, instruction). The result adds one field beyond LSP, context, saying where the query resolved.

    Pass triggerCharacter in the options when a character opened the query (LSP CompletionContext.triggerCharacter; Monaco and VS Code hand it to the provider). A trigger that opens nothing where the cursor is — a . or a - typed in prose — answers an empty list; [ opens a tag anywhere, and = / - / + narrow the list to the structural lines they begin. An explicit invocation (Ctrl+Space) is never narrowed. Editors that open the list on every letter (Monaco's quickSuggestions) should turn that off for bitmark: it is prose with markup in it.

    Items carry NO documentation (PLAN-202): a header's list is every bit type, and its prose would be most of the bytes of every list. resolve is LSP's completionItem/resolve — pass the same input and position you gave complete plus one item as it was listed, and get that item back with its Markdown documentation (a bit's title and description; a tag's format, count, default, JSON key, chain and description in this scope; an inline attribute's shape). Wire it to Monaco's resolveCompletionItem or an LSP server's resolve handler, so only the item about to be shown is rendered. When the list at that position no longer holds the item, the item comes back as given.

    hover returns an LSP Hover — Markdown contents plus the record in data — for the bit type, a tag AS RESOLVED IN ITS SCOPE (the same record info({ infoType: "bit", full: true }) shows for it there), a tag value, a header resource type, or an inline attribute key or value. It is null on body text, dividers and whitespace.

    import { complete, resolve, hover, CompletionItemKind } from "@gmb/bitmark-parser";

    const list = complete("[.article]\n[@", { line: 1, character: 2 });
    list.context; // { bit: "article", scope: "bit" }
    list.items.filter((i) => i.kind === CompletionItemKind.Property).map((i) => i.label);

    const id = list.items.find((i) => i.label === "@id");
    resolve("[.article]\n[@", { line: 1, character: 2 }, id).documentation.value; // Markdown for @id here

    const h = hover("[.article]\n[@id:1]", { line: 1, character: 2 });
    h.contents.value; // Markdown: format, count, default, JSON key, description
    h.data.tag; // the info record for @id in this bit

    Both need a variant built with editor (all three are) — otherwise they throw UnsupportedFeatureError. All three read bitmark SOURCE, so a JSON document is refused; an empty buffer, or one that is still only frontmatter, is source being typed and is answered normally (an empty one's complete offers the bit types). The CLI has the same three services under one command: bitmark editor diagnostics|complete|resolve|hover. The contract is .zen/specs/API-EDT-editor-services.tsp.

    convert(input, { inputFormat: "bitmark", outputFormat: "lex" }) returns the lexer's token stream as a JSON array: one { kind, span, text } per token (the LexToken type), span in bytes. Debug tooling — the token kinds are the engine's own names, not a stable vocabulary, and the parser is never run (pairing, the card-set divider rule and raw-body handling all happen after lexing), so a highlighter should use semantic-tokens instead. Bitmark input only; a JSON or markup document has no source text to lex. The former lex() export and bitmark lex command were removed in 7.0 (see the migration guide).

    convert(input, { inputFormat: "bitmark", outputFormat: "ast" }) returns the parser's tree as JSON — bits, their blocks (tag runs, body text, dividers) and inlines, every node with its byte span. Debug tooling like lex: the shape follows the parser and is not a contract. It shows what a bitmark patch path addresses (a run is one TagChain; a card is what sits between divider blocks). Not to be confused with the legacy entry point's "ast", which is bit JSON. Bitmark input only; in the same builds as lex.

    Breakscape text (escape bitmark special characters).

    • format — "bitmark++" (default) or "plainText"
    • location — "body" (default), "cardBody" or "tag"
    • subLocation — "text" (default) or "attrValue"

    subLocation REFINES location, it does not replace it. "attrValue" means the string is the value half of an inline-attr chain segment (==t==|link:VALUE|): it adds the chain delimiter | to the escaped set and drops the running-text rules (inline doubles, block markers), while location keeps deciding the tag rules — a [+trigger is escaped in body and inert in tag, a ] is inert in body and escaped in tag.

    "cardBody" is a card side or variant body. It has the body rules plus the card dividers -- and ++, which are markup only inside a card set and only at the start of a line, where they break to -^- and +^+.

    Reach for subLocation: "attrValue" when building a chain value for the patch API by hand; without it, an href containing [@ cannot be written correctly.

    breakscapeText("http://x/a[@b]", { location: "body", subLocation: "attrValue" });
    // → "http://x/a[^@b]"

    Carets themselves follow one rule: a literal ^ is written ^^ except with format: "plainText" and location: "body", where it is written ^. Three cases land on the ^^ side although they read as though they would not — a tag value in a plain-format bit (the :text suffix switches the body only), an inline code mark (ordinary bitmark text carrying a mark), and an attr value or bare URL. A |code block body is raw, but it has no public format: the parser and generator apply that rule themselves at the block boundary.

    Unbreakscape text (unescape bitmark special characters).

    • format — "bitmark++" (default) or "plainText"
    • location — "body" (default), "cardBody" or "tag"
    • subLocation — accepted and ignored

    For bitmark++ the rule is "remove one caret from each run" whatever the context, so a single inverse undoes every breakscape mode.

    The template of a bit (PLAN-225): an empty, config-filtered skeleton — the header, the tags an author usually writes (required ones, and those the config flags template) with their chains, the body, and the card structure (one card, every side, the first variant). Every value is the tag's default, so the plain rendering parses and canonicalizes cleanly.

    • bit — the bit name; required unless all is set
    • format — "text" (default; plain bitmark), "snippet" (an LSP snippet, insertTextFormat: 2: ${n:default} per value, a choice for enums and booleans with the default first, labelled body placeholders, $0 last), or "json" (the slot model — bit, attachment, tags[] with name, format, default, values, required, noValue (a tag that takes no value, written bare: a [%] chain lead), chain; body; card with its sides — plus the text and snippet renderings)
    • full — every tag in the bit's set plus the head chain of every resource type the bit may attach: a diagnostic dump, expected to validate with warnings (default false)
    • includeDeprecated — keep deprecated tags (and, with all, deprecated bits)
    • all — every bit as one JSON object { [bit]: { normal, full } }, whatever format says ("snippet" throws)
    • pretty / indent — JSON formatting
    template({ bit: "multiple-choice" });
    // [.multiple-choice]
    // [@revealSolutions:false]
    // [@additionalSolutions:]
    //
    // ====
    // [+]
    // [-]
    // ====
    template({ bit: "quote", format: "snippet" });
    // [.quote]\n[@quotedPerson:${1}]\n${2:body}\n$0
    JSON.parse(template({ bit: "image", format: "json" })).tags[0].name; // "&image"

    Which tags are "usual" is configuration: a template member on a tag entry (true, false, or { "only": { "bits": [...], "bitGroups": [...] } } / { "except": … }), resolved by bitmark-confgen into a per-bit flag the info views show as template: true. Required tags are always in the template. A resource head flagged for a bit that can only reach it through a header attachment puts the attachment in the header ([.article&image]).

    complete(input, position, { bitTemplate: true }) makes a bit-type item insert the bit's normal snippet from the name onward (article]⏎…) — the editor must then replace a ] it auto-closed after the cursor. Available in every wasm variant.

    Query information about supported bit types. "bit"/"all" default to a compact view — one line per tag with its format, effective count and default, chains and card-set tags nested. With full: true they report the complete detail: body/footer/resource rules, default provenance (configured vs format-natural), nullability (absence as a distinct state — orthogonal to defaults), JSON keys, per-context overrides, and the card set structure. "deprecated" lists deprecated bits with their deprecation version and (separately) any migration target.

    • infoType — "list" (default), "bit", "all", "deprecated", "bit-groups", "resource-groups", "languages", or "error-codes" (every issue code the parser can emit, with its severity and its message templates per scope — ErrorCodesInfo; read from the validator's own message table, so it is what the envelope emits, in every variant)
    • format — "text" (default) or "json"
    • bit — filter to a specific bit type (when infoType is "bit")
    • pretty — prettify JSON output (default: false)
    • indent — indent size for pretty JSON (default: 2)
    • includeDeprecated — include deprecated bits in "list"/"all" (default: false)
    • onlyDeprecated — restrict "list"/"all" to deprecated bits (default: false)
    • full — complete detail for "bit"/"all" (default: false = compact view)
    • language — BCP-47 tag(s) for titles and descriptions: one ("de"), several (["de", "fr"] or "de,fr"), or "all" — exported as ALL_LANGUAGES (default: English). More than one adds titles and descriptions maps. See Display names, descriptions and languages below

    "bit-groups" / "resource-groups" return the search/filter catalogs — each group's key, translated title, description, optional aliases and subgroupOf, and its members. They are available in EVERY build: the group catalog and per-bit titles are "descriptive" data, which the lean wasm variants carry too. Deprecated members are excluded by default and MARKED when included with includeDeprecated: true; a search index matching already-published content needs them, because a migrating bit is re-emitted under its target's name.

    To derive quiz categories, intersect a bit's bitGroups with the groups carrying subgroupOf: "quizzes" — subgroupOf is metadata and never implies membership, so every member of a subgroup also declares the parent.

    Only the META fields — BIT and TAG descriptions, group-inheritance provenance and raw mapping patterns (the info-meta cargo feature) — depend on the build. They are reported by the native CLI and the wasm full variant; browser-full and bitmark-json omit them to stay small (the bit descriptions alone are 14 KB gzip of the download). Those are absent structurally: the key is missing, never null or empty. Everything else info returns — including bit titles and the group catalogs — is identical in every variant.

    title — on bits, bit groups and resource groups — is a DISPLAY name, meant to be shown; description — on the same, and on the tag records of full: true output — says what it is. Pass language to get both in another language:

    info({ infoType: "bit-groups", format: "json", language: "de" });
    

    Resolution narrows one subtag at a time (de-CH → de), then falls back to the English text, then to nothing. A partially translated language is therefore safe: untranslated entries stay English rather than blank. Technical identity is never translated — name, key, tag names and member lists are the same in every language, so you can key off them freely.

    Several languages at once. Building a multilingual index should not mean walking every bit once per language, so language also takes a list, or "all" — exported as the ALL_LANGUAGES constant, so you can name it rather than retype the literal:

    import { info, ALL_LANGUAGES } from "@gmb/bitmark-parser";

    const groups = JSON.parse(
    info({ infoType: "bit-groups", format: "json", language: ALL_LANGUAGES }),
    );
    groups[0].titles; // { en: "Cloze", de: "Lückentext", … }

    // equivalent, and the idiomatic JS spelling of a list
    info({ infoType: "bit-groups", format: "json", language: ["de", "fr"] });

    // what "all" expands to
    JSON.parse(info({ infoType: "languages", format: "json" })); // ["de","en",…]

    Getting every name in one call. bit-groups carries each member's display name, so one request answers "every bit type, what it is called, and which categories it is in":

    const groups = JSON.parse(
    info({
    infoType: "bit-groups",
    format: "json",
    includeDeprecated: true,
    language: "all",
    }),
    );
    groups[0].bitTypes[0]; // { name: "assignment", title: "Assignment", titles: {…} }

    That is ~132 KB. Do not reach for infoType: "all" to get names — it carries the full per-tag detail for every bit (~5 MB) and is the wrong tool for this.

    More than one language adds a titles map and a descriptions map (on the tag records of full output too), and nothing else. It carries en when an English title exists, and is otherwise sparse: a requested language with no translation is absent rather than filled with English, so a gap stays visible. It is keyed by the tag you ASKED for — request de-CH where only de exists and you read titles["de-CH"].

    A single tag (or none) renders exactly as it always has, with no titles. The shape follows your REQUEST rather than which language you picked: "all" adds titles even on a build that resolves nothing but English.

    title is the name in your PRIMARY language — the first one you named. Name none and it is English; ask for "all" and it is English too, because with everything already in titles there is no non-arbitrary "first". So title tracks how you asked: consistent for any consumer that asks the same way every time, which in practice is all of them — a UI names one language, an index asks for "all".

    title stays optional, as its type says — it is the name in the language you asked for, and a few bits have one in some languages but not in English (the internal _comment and _error). Every other key is the same in every language.

    Which languages resolve depends on the build. The native CLI and the wasm full variant bake the table in; browser-full and bitmark-json do not, because the texts are not something a browser should download without asking — they carry English titles only, no descriptions. Those variants — and any build whose texts you want to override — take the file at runtime; once registered, a lean variant reports descriptions too, in English and in every language the file carries:

    import { init, register, info } from "@gmb/bitmark-parser";
    import translations from "@gmb/bitmark-parser/translations" with { type: "json" };

    await init({ feature: "browser-full" });
    register({ type: "translations", data: JSON.stringify(translations) });
    info({ infoType: "bit", bit: "cloze", format: "json", language: "de" });

    register REPLACES rather than merges: a second call supersedes the first entirely, and the built-in table (where there is one) stays underneath, so a key your file omits still resolves. It applies process-wide and survives an init variant swap. The language option stays per-call, because that is the axis that actually varies — one process may serve many.

    The same file backs all three routes: @gmb/bitmark-parser/translations (an opt-in subpath, so nothing pays for it unasked), the CLI's --translations <file> flag, and what the full builds bake in. It holds every text — titles and descriptions — in every language, English included (~300 KB; filter it to the languages you need before registering). Usage notes are not in it: they are docs-only, and live with the docs site since 7.16.0. Its shape is { "<text key>": { "<language>": "<text>" } }, where the key names the construct's kind and name: bit.<name>.title, bit.<name>.description, bit-group.<name>.title, resource-group.<name>.description, tag-group.<group>.tag.<tag>.description, … (the full table is in bitmark-json.tsp). A registered en text replaces the built-in English. @gmb/bitmark-parser/bitmark.json carries no text at all: join it to this file by key.

    The JSON shape is a stable contract; a change to it is a semver-major release. Language changes VALUES only — the key set is identical for every language. Parse it and assert the matching exported type:

    import { info, type DeprecatedInfo } from "@gmb/bitmark-parser";

    const deprecated = JSON.parse(
    info({ infoType: "deprecated", format: "json" }),
    ) as DeprecatedInfo;

    Result types: BitListInfo, BitInfo (CompactBitInfo | FullBitInfo), AllBitsInfo, DeprecatedInfo, BitGroupsInfo, ResourceGroupsInfo, and InfoErrorResult — returned in place of a result when infoType is "bit" and the name is unknown. info itself returns a string because it renders a document (format: "text", pretty and indent only mean anything for one).

    The text output is NOT a contract — it is for humans and may change in any release. Never parse it; every field it shows exists in the JSON form. The normative definition of the JSON is .zen/specs/API-INF-info-output.tsp.

    Return the library version string: the version the loaded wasm reports, so it is the parser actually running. Before a wasm is loaded (in the browser, before init()), the package's own version. Never throws.

    Bitmark can be parsed while it is still arriving (an LLM writing it, an author typing it). There is no streaming option: an unfinished document is parsed exactly like a finished one.

    • A tag whose ] has not arrived yet is still a tag, with the value so far: [@labelFalse:rathe gives labelFalse: "rathe". It ends at the end of its bit (the end of the input, or the next [. at a line start) or at a divider line (====, --, ++), and is reported as the info tag-unclosed.
    • A bit header with no ] ends at its line end, so every [. at the start of a line starts a new bit. Every bit before the last one is therefore final: split the text with splitBits and parse only the bit in progress.
    • Whatever is not recognised yet is text until it is complete: **bold shows its ** until the closing ** arrives.
    • In a bit type with no cards, ====, -- and ++ are ordinary text.

    TypeScript types for the parser's JSON are generated from bitmark.json (npm run schema:ts, automatic in the build): per-bit types (ArticleBit, ClozeBit, …) form the Bit discriminated union (narrow on bit.type), AnyBit is the loose merged view, BitEntry is the {bit, parser?, bitmark?} envelope, and resource/card content have named types (ImageResource, FlashcardBitCard, …).

    import {
    bitmarkToObjects,
    objectsToBitmark,
    patchEntry,
    type ArticleBit,
    } from "@gmb/bitmark-parser";

    // bitmark → typed entries
    const entries = bitmarkToObjects("[.article]\nHello **world**");
    if (entries[0].bit.type === "article") console.log(entries[0].bit.body);

    // author bits as typed literals (no builder needed) → bitmark
    const article: ArticleBit = { type: "article", body: "Hello" };
    const bitmark = objectsToBitmark([article]);

    // typed round-trip editing
    entries[0].bit.id = ["42"];
    const edited = objectsToBitmark(entries);

    // patch values typed from the literal path
    const patch = patchEntry("id", "append", "1234");

    See examples/ for runnable scripts covering the whole API (executed by npm test, so they stay correct). Generated API documentation (TypeDoc) is published at https://getmorebrain.github.io/bitmark-parser/docs/api/ with each release (the site root hosts the bitmark language docs), or build it locally with npm run docs (→ docs/api/) and preview it with npm run docs:serve (http://localhost:8080, --port to change).

    The package ships the resolved bitmark configuration its parser was compiled against — every bit type, tag group and card set with formats, defaults and mapping keys — as bitmark.json, with its TypeSpec contract beside it. It carries no human-readable text: titles and descriptions are in @gmb/bitmark-parser/translations.json, keyed by text key (see Display names, descriptions and languages):

    import config from "@gmb/bitmark-parser/bitmark.json" with { type: "json" };
    import texts from "@gmb/bitmark-parser/translations.json" with { type: "json" };
    // contract: node_modules/@gmb/bitmark-parser/config/bitmark-json.tsp
    texts["bit.cloze.description"].en;

    It is generated by the build (bitmark-confgen) from the repo's jsonc config tree, never hand-edited, and its label is a content hash of that tree. Version provenance stays in the repo and is not part of it: each construct's since, and the manifest's pendingSince, the placeholder since of a construct added since the last release (filled in when a release is prepared).

    The package also ships a JSON Schema (Draft 2020-12) describing the parser's JSON output for every bit type, generated from the same bitmark.json as the TypeScript types (npm run schema:json, automatic in the build):

    import schema from "@gmb/bitmark-parser/schema.json" with { type: "json" };
    // or (CJS): const schema = require("@gmb/bitmark-parser/schema.json");

    Use it to validate parser output or JSON input, or to enumerate the keys a bit type can carry. It accepts every top-level shape the parser does: an array of { "bit": … } entries (the parser's output), a single entry, a bare bit, or an array mixing entries and bare bits.

    Enumerating keys helps when auditing a consumer for the keys it must materialise itself under optimized output (see the migration guide). The key's type is all a consumer needs: configured (non-natural) defaults are always materialised in the output, so an absent key always means the natural default of its type (false, 0, "", [], {}).

    Note: deprecated bit types with a configured migration target (the *-collapsible family) are re-emitted under the target type with isCollapsible: true — consumers keyed on the old type strings see the base type instead. collapsible itself is unaffected.

    A tag whose configured format is number accepts what JavaScript's Number(value) accepts, minus the values that are not finite:

    • surrounding whitespace, a leading +, leading zeros (01), a trailing point (1.) and a leading point (.5);
    • an exponent in either case (1e2, 1E2);
    • hexadecimal, binary and octal literals (0x1F, 0b101, 0o701) — prefix case-insensitive, with no sign, fraction or exponent.

    NaN, Infinity, an exponent beyond ±4000, and a radix literal past 64 bits are rejected: the tag is dropped from the output and a tag-value-invalid warning names it, exactly like any other value that does not fit its format.

    Accepted values print in JavaScript's JSON.stringify form — 0.5, 1.5, 100, 1e+21, 1e-7 — so [@width: 1.50] emits 1.5 and [@width:0x10] emits 16. Values with more than 17 significant digits, including integers at or above 2^63, keep the digits you wrote rather than being rounded through a floating-point double.

    A compatibility facade for consumers migrating from @gmb/bitmark-parser-generator (bpg v5.31.x). Only the import changes:

    // before: import { BitmarkParserGenerator } from "@gmb/bitmark-parser-generator";
    import { BitmarkParserGenerator } from "@gmb/bitmark-parser/legacy";
    // or: import { legacy } from "@gmb/bitmark-parser";

    const bpg = new BitmarkParserGenerator();
    const json = bpg.convert("[.article]\nHello **world**", {
    outputFormat: "json",
    });

    Supported (bpg-compatible signatures and return shapes): convert, upgrade, info, version, breakscapeText / unbreakscapeText, convertText, textAstToPlainText, extractPlainText, convertHtmlTable, createAst, bitmarkTextParse, the Builder / ResourceBuilder classes, the Input / Output / InputFormat / BitType (config-derived) / TextMarkType / TextNodeType enums, and the option / JSON model types (vendored from bpg, exposed only via legacy). Node file input/output (Input.file, outputFile) works as in bpg; the browser build throws on file writes, as bpg does.

    Differences (all loud, never silent):

    • bitmark v3 only — bitmarkVersion: 2, cardSetVersion: 1 and jsonOptions.textAsPlainText: true throw.
    • Strict option policy — options with no equivalent here throw a LegacyUnsupportedError naming the option (explicitTextFormat, noBreakscaping, debugGenerationInline). The option types are narrowed to the supported values too (e.g. bitmarkVersion?: 3, noBreakscaping?: false), so TypeScript consumers get a build-time error rather than only the runtime throw — JS callers still get the loud runtime error. Exception: bitmarkParserType is accepted and ignored — bpg itself ignores it. jsonOptions.enableWarnings is accepted (parser warnings are always carried in the output envelope). bitmarkOptions.prettifyJson is honoured (pretty-prints the embedded JSON bodies of json-format bits in bitmark output, as bpg did).
    • Unknown properties — included in JSON output by default and excludable via jsonOptions.excludeUnknownProperties, exactly as bpg (arrays, last keys, _-prefix on collision with a real key, never converted back to bitmark). One deliberate divergence: the per-occurrence warning text names the new API's inverse flag (includeUnknownProperties), not bpg's excludeUnknownProperties.
    • The AST is bit JSON — createAst / Output.ast / Builder.buildBit produce the bit JSON ("pseudo-AST"), not bpg's internal TS node model. It round-trips through convert, but bpg's Ast walker (and the generator/parser/writer internals) throw as unsupported. buildBit's resources input follows bpg's routing: on bits whose config maps a repeatable resource group onto a collection key (e.g. images / logos), the entries land under that key; on other bits the first entry becomes the single resource (as bpg).
    • convertHtmlTable converts HTML <table> markup ↔ table bits via the core's html mapping (the <bitmark-bit> envelope is added/stripped at the boundary). The markup produced inside <table> follows this parser's html mapping and may differ from bpg's in detail. keepUnknownTags / noBreakscaping throw.
    • version() reports this package's version (the loaded wasm's), not a bpg version.

    The CLI binary is bitmark.

    # Convert bitmark to JSON (auto-detects format)
    bitmark convert input.bitmark
    bitmark convert input.bitmark -o output.json

    # Convert JSON to bitmark
    bitmark convert input.json -o output.bitmark

    # Convert with options
    bitmark convert input.bitmark --mode full --pretty --warnings

    # Include unknown properties in the bit JSON (bitmark → JSON only)
    bitmark convert input.bitmark --include-unknown-properties

    # Parse a bitmark file to JSON
    bitmark convert input.bitmark --input-format bitmark --output-format json
    bitmark convert input.bitmark --input-format bitmark -o output.json
    bitmark convert input.bitmark --input-format bitmark --output-format json --mode full

    # Generate bitmark from JSON
    bitmark convert input.json --input-format json --output-format bitmark -o output.bitmark

    # JSONL: one bit per line, and back (auto-detected)
    bitmark convert input.bitmark --output-format jsonl -o output.jsonl
    bitmark convert output.jsonl --output-format bitmark

    # Import a NISO-STS standards document (config-driven forward import).
    # Whitespace reads as a browser shows it — pretty-printing indentation and
    # line breaks become single spaces — except inside `<preformat>`, `<code>`,
    # `<inline-code>`, `<x>`, `<glyph-data>`, `<tex-math>` and
    # `xml:space="preserve"`, which keep it exactly. Namespaced names match
    # however the source declares them (`<math xmlns="…MathML">` reads as
    # `mml:math`); MathML becomes `|latex|`, a `display="block"` formula its own
    # paragraph. HTML input converts `<math>` the same way.
    bitmark convert standard.xml --input-format xml-niso-iec --output-format bitmark

    # Re-emit in the same format at optimized / full mode (canonical form)
    bitmark canonicalize input.bitmark
    bitmark canonicalize input.json --mode full

    # Write inline marks as toggle tags — (+bold)x(-bold) — instead of **x** / ==x==|bold|
    bitmark convert input.json --output-format bitmark --inline-style toggle
    bitmark canonicalize input.bitmark --inline-style toggle

    # Apply a patch document (per-bit patches by index/id, remove, insert, move;
    # a bare array of entries still patches every bit)
    bitmark transform input.json --patch patches.json # alias: patch

    # Semantic diff of two documents, rendered as bitmark
    bitmark diff old.bitmark new.bitmark # unified-style text
    bitmark diff old.bitmark new.bitmark --stat --exit-code # counts; exit 1 when they differ
    bitmark diff old.bitmark new.json --output-format json # per-bit ops, revert, hunks
    bitmark diff old.bitmark new.bitmark --output-format patch > d.patch
    bitmark transform old.bitmark --patch d.patch --output-format bitmark # reproduces new
    # --context N, --locate, --similarity F, --bbox-tolerance N, --color auto|always|never,
    # --paths json|bitmark, --no-pair-leftovers

    # Change proposals: written by the diff, applied by transform (from a file, or
    # the ones the document carries — then marked resolved, --date / --by)
    bitmark diff old.bitmark new.bitmark --output-format bitmark-patch > fixes.bitmark
    bitmark transform book.bitmark --patch fixes.bitmark --output-format bitmark
    bitmark transform book.bitmark --apply-proposals --by validator --output-format bitmark

    # Parser-derived highlighting: LSP semantic tokens (bitmark input only)
    bitmark convert input.bitmark --input-format bitmark --output-format semantic-tokens
    bitmark convert input.bitmark --output-format semantic-tokens --tokens-layout absolute --position-encoding utf-8 --pretty

    # Dump the lexer token stream as JSON (debug tooling; bitmark input only)
    bitmark convert input.bitmark --input-format bitmark --output-format lex --pretty

    # Also write where each bit is in the input and in the output,
    # {positionEncoding, spans}, to its own file (output offsets into this run's
    # output; --position-encoding applies)
    bitmark convert input.bitmark --output-format html -o out.html --bit-spans spans.json

    # Also write fixed points inside each bit, {positionEncoding, anchors}, to its
    # own file (bitmark to json / jsonl / html / xml; output offsets into this run's output)
    bitmark convert input.bitmark --output-format json --pretty --bit-anchors anchors.json

    # Breakscape / unbreakscape text
    bitmark breakscape input.txt
    bitmark breakscape input.txt --format plainText --location tag
    # a card side / variant body, where the card dividers are live
    bitmark breakscape input.txt --location cardBody
    # a value for an inline-attr chain segment (==t==|link:VALUE|)
    bitmark breakscape input.txt --location body --sub-location attrValue
    bitmark unbreakscape input.txt

    # Query bit type information
    bitmark info
    bitmark info all -f json --pretty
    bitmark info --bit article # compact: one line per tag (format, count, default)
    bitmark info --bit article --full # full detail: defaults, provenance, keys, card set
    bitmark info deprecated # deprecated bits + migration targets
    bitmark info list --include-deprecated # include deprecated bits in the listing
    bitmark info list --deprecated # only deprecated bits

    # A bit's template: the tags an author usually writes, the body and the card
    # structure (PLAN-225) — plain bitmark, an LSP snippet, or JSON
    bitmark template --bit multiple-choice
    bitmark template --bit flashcard -f snippet
    bitmark template --bit article --full # every tag + every attachable resource chain
    bitmark template --all --pretty # every bit, both modes, keyed by name (JSON)
    bitmark editor complete input.bitmark --line 0 --character 2 --bit-template # bit items insert their template

    All commands support -o, --output <file> and -a, --append flags. Commands that accept input support file paths, literal strings, or stdin (when no arguments are given).

    The package includes pre-built browser bundles with the WASM module.

    <script type="module">
    import init, {
    convert,
    } from "https://cdn.jsdelivr.net/npm/@gmb/bitmark-parser@latest/dist/browser/bitmark-parser.min.js";

    await init();

    const json = convert("[.article]\nHello **bold**", {
    inputFormat: "bitmark",
    });
    console.log(json);
    </script>

    The CDN bundle carries all three variants' JS glue and fetches only the selected variant's .wasm (from dist/browser/wasm/). A bare init() loads browser-full; init({ feature: "bitmark-json" }) fetches the smallest build, and a later init({ feature: "full" }) upgrades in place (see WASM variants).

    The script and the .wasm are one release. Loaded through a version alias such as @latest, the script fetches the .wasm from the path pinned to its own version (…/@gmb/bitmark-parser@<version>/…), so a script served from a CDN cache after a release never drives the new release's wasm. init() rejects a .wasm of another version (version-mismatch: …) rather than running it; the glue can be instantiated once per page, so a reload is the remedy.

    import init, { convert } from "@gmb/bitmark-parser/browser";

    await init();
    const json = convert("[.article]\nHello", { inputFormat: "bitmark" });

    ISC — © 2023–2026 Get More Brain Ltd.