Skip to content

Adapters and Extension Points

Lanexio™ Parser is tooling-first: lossless flat AST, incremental reparse, multi-grammar dispatch, and never-throw are the headline strengths. The HTML DOM facade, the GraphQL validation core, and the plugin/visitor runtime are first-party in this release: ADR 0049, ADR 0050, and ADR 0051 each supersedes the ADR 0047 deferral for its own deliverable. This guide documents the extension seams that remain for the long tail: registering consumer grammars, provider adapters such as a jsdom or a graphql-js bridge, and pipeline plugins beyond the shipped set. The seams walk each grammar through the same registry, cursor, and never-throw contracts the first-party modules use, so a consumer can assemble exactly the adapter their product needs.

register() and grammarRegistry: one dispatcher, your grammars

Section titled “register() and grammarRegistry: one dispatcher, your grammars”

Every grammar pack exports a *Registration object (GrammarRegistration). register() puts it into the shared grammarRegistry, after which the unified parse() resolves it by language name, alias, file extension, MIME type, or embed context:

import { register, parse } from '@lanexio/parser';
import { jsonRegistration } from '@lanexio/parser-grammar-json';
import { myFormatRegistration } from 'my-format-parser'; // yours
register(jsonRegistration);
register(myFormatRegistration);
const tree = parse('a = 1', { language: 'myformat' });

The registry is a real object (grammarRegistry), not a hidden global, so a plugin layer can list, override, or remove registrations:

import { grammarRegistry } from '@lanexio/parser';
grammarRegistry.byLanguage('myformat'); // your registration, or undefined
grammarRegistry.byExtension('.myfmt'); // extension lookup

register() overwrites an existing registration for the same key, so later registrations take priority. That is the mechanism a plugin ecosystem would route through: a plugin registers its grammar and its language, extensions, and MIME types, and every consumer of the unified parse() picks it up.

createParser(grammar) loads a grammar pack (WASM or pure) and returns a LanexioParser handle whose parse, reparse, and createStream never throw:

import { createParser } from '@lanexio/parser';
import { myFormatGrammar } from 'my-format-parser';
const parser = await createParser(myFormatGrammar);
const tree = parser.parse('a = 1'); // string or Uint8Array
const reparsed = parser.reparse(tree, {
start: 2,
end: 3,
replacement: new TextEncoder().encode('b'),
});

createParser is the seam a tool uses when it wants one concrete parser instance instead of the registry dispatcher. WASM grammars degrade gracefully: if @lanexio/parser-wasm is not installed, pure grammars still work.

@lanexio/parser-core exposes reparse(previousTree, edit, oracle?) and the ReuseOracle interface. The oracle decides which subtrees are safe to reuse verbatim and which must be re-parsed:

import { reparse, ReuseOracle } from '@lanexio/parser-core';
import type { LexNode, LexTree } from '@lanexio/parser-core';
const oracle: ReuseOracle = {
isRestartBoundary(node: LexNode): boolean {
return node.kind === MY_KIND.Block; // a subtree that re-parses standalone
},
canReuseSubtree(node: LexNode): boolean {
return node.kind !== MY_KIND.StringLiteral; // strings are dirty after edits
},
parse(bytes: Uint8Array): LexTree {
return parseMyFormat(bytes);
},
};
const tree = reparse(previousTree, edit, oracle);

Without an oracle, reparse always falls back to a full reparse (the safe default), so a custom grammar that omits the oracle still gets the stable API. The CSS grammar ships a working reference oracle (cssReuseOracle in @lanexio/parser-grammar-css) you can mirror.

A GrammarRegistration carries an embedContexts list that declares which host contexts dispatch to this grammar (fence info strings, tag names). embedGuests in parser-core resolves embedded snippets through the same registry the top-level parse() uses, so a Markdown document that embeds code fences or an HTML document that embeds <style> can hand each embedded region to its grammar:

import { embedGuests } from '@lanexio/parser-core';
const hosted = embedGuests(markdownTree); // fences dispatch by embedContext

One registry, two consumers (top-level dispatch and embedded handoff): a new grammar registered once is reachable from both.

@lanexio/parser-grammar-kit: authoring a grammar pack

Section titled “@lanexio/parser-grammar-kit: authoring a grammar pack”

New grammar packs are built with the grammar authoring toolkit, a build-time CLI that validates grammar metadata and generates the TypeScript kind constants (a const object, never a TS enum):

Terminal window
npx grammar-kit --check --input kinds.json
npx grammar-kit --input kinds.json --output src/kinds.generated.ts --export-name MyFormatKind

A hand-written parser that emits the generated kinds and a GrammarRegistration plugs into everything above: the unified parse(), grammarRegistry, embedGuests, and reparse. This is the plugin authoring surface; the CLI is documented in the parser-grammar-kit reference.

The reference shapes and what is still a seam

Section titled “The reference shapes and what is still a seam”

The shipped modules are the reference shapes for the three most-requested general-parsing surfaces: parseDom / toDom on the HTML pack (DOM Adapter, ADR 0049), buildSchema / validate on the GraphQL pack (GraphQL validation, ADR 0050), and walk / transform / createPipeline on parser-core (Plugins, ADR 0051). Their tested implementations live in examples/ecosystem/ (run pnpm vitest run examples/ecosystem), which re-export the first-party package surface.

The seams in this guide remain for what the shipped set does not cover. The three recipes below show the pattern for a consumer grammar or a provider bridge of your own:

A DOM-shaped read layer for your grammar. Walk the lossless flat AST with LexQuery or the cursor, project element/text/attribute nodes to the shape your product needs, and use createParser or parse() as the front door. The shipped HTML facade is the reference shape: it is a read layer over the tree with a per-instance decorator override, not a copied object graph. A jsdom-style standards Document bridge would sit behind the same facade interface and is not part of the shipped module.

A validation pass for your grammar. A validation pass walks the tree (the same cursor every grammar uses) and reports rule violations as structured findings with byte ranges: a pure function from LexTree to findings. The shipped GraphQL core is the reference shape, and its graphql-js adapter shows how to bridge to a provider’s schema as an optional peer without importing the provider into the grammar.

A pipeline plugin beyond the shipped set. createPipeline accepts a Plugin with onParse and transform hooks. A consumer grammar registered through the pipeline reaches the unified parse() dispatcher; register() + grammarRegistry stay the last-wins registration surface for anything that does not need the runtime’s hook ordering. Build steps: Plugins.