familymarkup-codemirror
v1.0.0
Published
CodeMirror 6 highlighting for FamilyMarkup, powered by the Go parser compiled to WebAssembly
Maintainers
Readme
familymarkup-codemirror
CodeMirror 6 highlighting for FamilyMarkup, driven by familymarkup-parser — the same Go parser the tooling uses, compiled to WebAssembly. The editor and the tooling therefore agree on every token by construction: there is no second lexer to keep in sync.
Why decorations and not a Lezer grammar
The Go lexer is not incremental: it revises tokens it has already emitted.
checkFamilyNamepromotesNametoSurnameonce an empty line proves the line was a family headercheckSurnamedemotes earlier surnames back to namesmergeWords/mergeUnknowncollapse several emitted tokens into onecheckWordToUnknownrewrites a whole line backwards
A token's type depends on what comes after it, sometimes lines later, so
StreamLanguage (line by line, no backtracking) cannot express it and a Lezer
grammar would have to reimplement the same context tracking in a different
language. Instead the whole document is lexed on every change and the tokens are
applied as mark decorations, with classes resolved through highlightingFor so
themes keep working normally.
Whole document relexing is affordable here: the lexer is O(n) and family files
are kilobytes. The demo prints the measured analyze() time for the document in
the editor.
Install
npm install familymarkup-codemirror@codemirror/language, @codemirror/state, @codemirror/view and
@lezer/highlight are peer dependencies — any CodeMirror 6 setup already has
them.
The package ships the prebuilt module, two files your site has to serve:
node_modules/familymarkup-codemirror/wasm/familymarkup.wasmnode_modules/familymarkup-codemirror/wasm/wasm_exec.js
Copy them into your static directory, or let the bundler resolve them through the subpath exports:
import wasmUrl from "familymarkup-codemirror/familymarkup.wasm?url";
import goRuntimeUrl from "familymarkup-codemirror/wasm_exec.js?url";The published module is the TinyGo build — 294 KB, 111 KB gzipped. TinyGo brings
its own wasm_exec.js and Go's copy will not load a TinyGo module, so always
ship the one from this package.
Document size
A 13.5 KB document costs about 36 ms per pass — the lexer's own cost, not wasm overhead, since every position is tried against seventeen regular expressions. Below a few KB the whole-document pass is unnoticeable; past that, debounce the analysis or the editor will lag behind typing.
Usage
Load the module before creating the editor, so the first paint is already correct:
import { EditorView, basicSetup } from "codemirror";
import { syntaxHighlighting } from "@codemirror/language";
import {
familyMarkup,
familyMarkupHighlightStyle,
initFamilyMarkupWasm,
} from "familymarkup-codemirror";
await initFamilyMarkupWasm({ wasm: "/familymarkup.wasm", goRuntime: "/wasm_exec.js" });
new EditorView({
doc: "Шевченко\n\nТарас + Оксана\nІван\n",
parent: document.body,
extensions: [
basicSetup,
syntaxHighlighting(familyMarkupHighlightStyle),
familyMarkup(),
],
});goRuntime can be left out if the page loads wasm_exec.js with its own script
tag; wasm also accepts a Response or the raw bytes.
To let the editor appear first and load the module in the background, hand the same options to the extension instead:
familyMarkup({ wasm: "/familymarkup.wasm", goRuntime: "/wasm_exec.js" });Highlighting then appears as soon as the module is ready. Fold markers do not:
foldGutter rebuilds its markers only on document, viewport, language or fold
state changes, so with a custom setup pass the hook this package exports:
import { foldGutter } from "@codemirror/language";
import { foldingChanged } from "familymarkup-codemirror";
foldGutter({ foldingChanged });familyMarkupHighlightStyle is only a default look — any theme that styles the
standard Lezer tags covers this language already.
Token to tag mapping
| Token | Tag |
| --- | --- |
| TokenSurname | definition(typeName) |
| TokenName | variableName |
| any token with TokenAlias | propertyName |
| TokenUnknown | atom |
| TokenWord | labelName |
| TokenNum | number |
| TokenArrow | operator, definitionOperator for = |
| TokenPunctuation | operator for +, separator for , |
| TokenBracket | paren |
| TokenComment | lineComment |
| TokenInvalid | invalid |
Tokens the parser flagged through Token.ErrType additionally get the
cm-fml-error class, underlined by familyMarkupBaseTheme.
What else the analysis gives you
familyMarkup() also registers a foldService for family blocks, built from
Family.Loc. Diagnostics are a short step away too — Token.ErrType is already
in the buffer, so a linter() that walks it with tokenReader() is about
fifteen lines.
Building from source
The wasm module is compiled from familymarkup-parser, which has to be checked out in the same parent directory as this repository.
npm install
npm run build # wasm (go) + tsc
npm run build:wasm # go
npm run build:wasm:tinygo # tinygo, needs wasm-opt from binaryen on PATH
npm run build:ts # tsc only
npm run dev # demo on http://localhost:5173Either compiler produces wasm/familymarkup.wasm and copies the matching
wasm_exec.js next to it. Measured on this parser, same document, median of 30
runs in Chrome:
| | size | gzipped | 271 chars | 13.5 KB |
| --- | --- | --- | --- | --- |
| go 1.26 | 2657 KB | 776 KB | 1.5 ms | 35.6 ms |
| tinygo 0.41 -opt=z -no-debug | 294 KB | 111 KB | 0.9 ms | 36.2 ms |
Seven times less to download for the same speed, goroutines in the lexer
included. Plain go is the default because it needs no extra tooling.
npm publish runs the TinyGo build, so that is what ends up on npm.
Notes
- The parse tree is not exposed as JSON:
Rootcontains cycles (Relation.Family↔Family.Relations), sojson.Marshalfails on it. Tokens and family ranges are passed as flat little-endian int32 buffers instead, read asInt32Arrayviews with no copying or parsing on the JS side. - Go reports columns in code points, CodeMirror counts UTF-16 code units. They
agree for all BMP text;
PositionMapperconverts only on lines that contain surrogate pairs. TokenType/TokenSubTypevalues are duplicated insrc/tokens.ts, andassertConstantsMatchthrows on the first analysis if they ever drift from the Go constants exported by the wasm module.
