Skip to content

LexQuery

LexQuery finds nodes in a LexTree by pattern. Patterns describe node kind names, fields, and error status. Queries run in preorder DFS and return an iterator.

Package: @lanexio/parser-query
Layer: 3 (Query). Depends only on @lanexio/parser-core.
Runtime: Universal.

import { LexQuery, type LexQueryKindResolver } from '@lanexio/parser-query';
import { LexQuery } from '@lanexio/parser-query';
import { HtmlKind } from '@lanexio/parser-grammar-html';
const resolver: LexQueryKindResolver = (name: string) => {
const kind = (HtmlKind as Record<string, number | undefined>)[name];
return kind;
};
const query = LexQuery.compile('Element', resolver);

LexQuery.compile returns a compiled query object. If the pattern is invalid, it throws LexQuerySyntaxError.

import { parseHtml } from '@lanexio/parser-grammar-html';
import { LexQuery } from '@lanexio/parser-query';
import { HtmlKind } from '@lanexio/parser-grammar-html';
const encoder = new TextEncoder();
const tree = parseHtml(encoder.encode('<div><p>First</p><p>Second</p></div>'));
const resolver = (name: string) => (HtmlKind as Record<string, number | undefined>)[name];
const query = LexQuery.compile('Element', resolver);
for (const node of query.matches(tree)) {
console.log(node.kind, node.range);
}

query.matches(tree) returns an IterableIterator<LexNode> in preorder DFS order.

CRITICAL: Kind names are not HTML tag names

Section titled “CRITICAL: Kind names are not HTML tag names”
// WRONG: 'p' is not a kind name. This throws LexQuerySyntaxError (UnknownKind).
const query = LexQuery.compile('p', resolver);

Correct: filter by kind, then check metadata

Section titled “Correct: filter by kind, then check metadata”
import { parseHtml, HtmlKind } from '@lanexio/parser-grammar-html';
import { LexQuery } from '@lanexio/parser-query';
const encoder = new TextEncoder();
const tree = parseHtml(encoder.encode('<div><p>First</p><h2>Second</h2></div>'));
const resolver = (name: string) => (HtmlKind as Record<string, number | undefined>)[name];
const query = LexQuery.compile('Element', resolver);
const decoder = new TextDecoder();
for (const node of query.matches(tree)) {
// Read the tag name from the source bytes to filter further.
// node.range is a tuple: readonly [start, end].
const [start, end] = node.range;
const src = decoder.decode(tree.source.subarray(start, end));
if (src.startsWith('<p')) {
console.log('found <p> at', node.range);
}
}

Multi-step combinators (":has", ":not", child combinators) are deferred to v1.1. For now, query by kind and filter the results in your loop.

PatternDescription
KindNameMatch nodes of the named kind.
*Match any node.
KindName@fieldNameRestrict matches to nodes carrying the named field role.
KindName[error]Restrict matches to nodes carrying the parse-error flag.
A, BMatch either A or B (disjunction).

Modifiers attach to a simple selector: Element@body, *[error], String@key. Field names are the grammar’s field roles (see each grammar guide’s “Navigate by field name” section), resolved through the tree’s own field metadata at match time.

// All nodes of any kind
const all = LexQuery.compile('*', resolver);
// Any node carrying the parse-error flag (malformed input)
const errors = LexQuery.compile('*[error]', resolver);
// Element or Text nodes
const elemOrText = LexQuery.compile('Element, Text', resolver);

Field roles let a query target structural positions. With the JSON grammar (key/value roles on object members):

import { parseJson, JsonKind } from '@lanexio/parser-grammar-json';
import { LexQuery } from '@lanexio/parser-query';
const encoder = new TextEncoder();
const tree = parseJson(encoder.encode('{"name": "Alice", "id": 42}'));
const resolver = (name: string) => (JsonKind as Record<string, number | undefined>)[name];
// Every member KEY in the document:
const keys = LexQuery.compile('String@key', resolver);
for (const node of keys.matches(tree)) {
console.log('key:', node.text); // "name", "id"
}

With the CSS grammar (selector/property/value roles):

import { parseCss, CssKind } from '@lanexio/parser-grammar-css';
const cssResolver = (name: string) => (CssKind as Record<string, number | undefined>)[name];
// Every declaration property name in a stylesheet:
const props = LexQuery.compile('PropertyName@property', cssResolver);

LexQueryKindResolver maps a kind name string to its numeric kind ID, or undefined if unknown.

type LexQueryKindResolver = (name: string) => number | undefined;

When the resolver returns undefined for a name in the pattern, LexQuery.compile throws LexQuerySyntaxError with code UnknownKind.

For HTML queries, cast HtmlKind as a record:

import { HtmlKind } from '@lanexio/parser-grammar-html';
const htmlResolver = (name: string): number | undefined =>
(HtmlKind as Record<string, number | undefined>)[name];

For Markdown queries:

import { MdKind } from '@lanexio/parser-grammar-markdown';
const mdResolver = (name: string): number | undefined =>
(MdKind as Record<string, number | undefined>)[name];

If LexQuery.compile receives an invalid pattern, it throws LexQuerySyntaxError.

import { LexQuery, LexQuerySyntaxError, LexQuerySyntaxErrorCode } from '@lanexio/parser-query';
try {
const query = LexQuery.compile('!!!invalid', resolver);
} catch (err) {
if (err instanceof LexQuerySyntaxError) {
console.error(err.code, 'at offset', err.offset);
// LexQuerySyntaxErrorCode values:
// EmptyPattern | UnexpectedToken | UnknownKind | UnsupportedAttribute
}
}
APITypeDescription
LexQuery.compile(pattern: string, resolver: LexQueryKindResolver) => LexQueryCompile a pattern. Throws LexQuerySyntaxError for invalid patterns.
query.matches(tree: LexTree) => IterableIterator<LexNode>Iterate matching nodes in preorder DFS.
query.testNode(node: LexNode) => booleanTest a single node.
query.sourcestringThe original pattern string.