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 undefinedgrammarRegistry.byExtension('.myfmt'); // extension lookupregister() 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: one never-throwing handle
Section titled “createParser: one never-throwing handle”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 Uint8Arrayconst 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.
The reuse oracle: incremental reparse
Section titled “The reuse oracle: incremental reparse”@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.
embedContexts: embedded-language handoff
Section titled “embedContexts: embedded-language handoff”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 embedContextOne 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):
npx grammar-kit --check --input kinds.jsonnpx grammar-kit --input kinds.json --output src/kinds.generated.ts --export-name MyFormatKindA 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.
Related
Section titled “Related”- Who this is for
- Plugins (tested seam examples)
- LexQuery
- parser-grammar-kit reference
- parser reference - parser-core reference