npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

familymarkup-codemirror

v1.0.0

Published

CodeMirror 6 highlighting for FamilyMarkup, powered by the Go parser compiled to WebAssembly

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.

  • checkFamilyName promotes Name to Surname once an empty line proves the line was a family header
  • checkSurname demotes earlier surnames back to names
  • mergeWords / mergeUnknown collapse several emitted tokens into one
  • checkWordToUnknown rewrites 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.wasm
  • node_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:5173

Either 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: Root contains cycles (Relation.FamilyFamily.Relations), so json.Marshal fails on it. Tokens and family ranges are passed as flat little-endian int32 buffers instead, read as Int32Array views 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; PositionMapper converts only on lines that contain surrogate pairs.
  • TokenType/TokenSubType values are duplicated in src/tokens.ts, and assertConstantsMatch throws on the first analysis if they ever drift from the Go constants exported by the wasm module.