🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

satteri

Package Overview
Dependencies
Maintainers
1
Versions
35
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

satteri - npm Package Compare versions

Comparing version
0.8.1
to
0.9.0
+31
dist/byte-writer.d.ts
/**
* Growable little-endian byte buffer shared by the op-stream and command-buffer
* encoders — the two hottest write paths in the package.
*
* Subclasses write single bytes directly through the protected `buf`/`n`
* fields after an `ensure` for the whole record (one bounds check per record,
* not per byte, and no per-byte call in unoptimized tiers — CodSpeed's
* Simulation mode runs too few iterations for V8 to inline method calls).
* Only the non-trivial logic lives here: growth, u32 writes, and UTF-8
* string encoding.
*/
export declare class ByteWriter {
#private;
protected buf: Uint8Array;
protected n: number;
constructor(initialSize: number);
/** Number of bytes written so far. */
get length(): number;
/** Reset for reuse; the grown buffer is retained so steady state is alloc-free. */
reset(): void;
/** View of the bytes written so far (no copy; valid until the next write or reset). */
take(): Uint8Array;
/** Release the backing (handed-out views stay intact); next write re-allocates. */
protected release(): void;
/** Grow (doubling) so `extra` more bytes fit; required before unchecked writes. */
ensure(extra: number): void;
/** Write a u32 (LE) at the cursor; caller must have `ensure`d 4 bytes. */
protected writeU32(v: number): void;
/** u32 byte length + UTF-8 bytes (self-ensuring). */
protected utf8WithU32Len(s: string): void;
}
/**
* Growable little-endian byte buffer shared by the op-stream and command-buffer
* encoders — the two hottest write paths in the package.
*
* Subclasses write single bytes directly through the protected `buf`/`n`
* fields after an `ensure` for the whole record (one bounds check per record,
* not per byte, and no per-byte call in unoptimized tiers — CodSpeed's
* Simulation mode runs too few iterations for V8 to inline method calls).
* Only the non-trivial logic lives here: growth, u32 writes, and UTF-8
* string encoding.
*/
const encoder = new TextEncoder();
const EMPTY = new Uint8Array(0);
/** Below this length the inline char-code copy beats `encodeInto`'s call
* overhead; above it the native bulk path wins (and skips the ASCII scan).
* Measured crossover ~16 bytes (Node 24: 8B 15 vs 41 ns, 16B 37 vs 43,
* 32B 73 vs 52). */
const INLINE_STR_MAX = 16;
export class ByteWriter {
// Backing allocates on first write: visitor passes construct buffers that
// often never receive a byte (read-only plugins).
buf = EMPTY;
n = 0;
#initialSize;
constructor(initialSize) {
this.#initialSize = initialSize;
}
/** Number of bytes written so far. */
get length() {
return this.n;
}
/** Reset for reuse; the grown buffer is retained so steady state is alloc-free. */
reset() {
this.n = 0;
}
/** View of the bytes written so far (no copy; valid until the next write or reset). */
take() {
return this.n === 0 ? EMPTY : this.buf.subarray(0, this.n);
}
/** Release the backing (handed-out views stay intact); next write re-allocates. */
release() {
this.buf = EMPTY;
this.n = 0;
}
/** Grow (doubling) so `extra` more bytes fit; required before unchecked writes. */
ensure(extra) {
if (this.n + extra <= this.buf.length)
return;
let size = Math.max(this.#initialSize, this.buf.length * 2);
while (this.n + extra > size)
size *= 2;
const grown = new Uint8Array(size);
grown.set(this.buf);
this.buf = grown;
}
/** Write a u32 (LE) at the cursor; caller must have `ensure`d 4 bytes. */
writeU32(v) {
const buf = this.buf;
let n = this.n;
buf[n++] = v & 255;
buf[n++] = (v >> 8) & 255;
buf[n++] = (v >> 16) & 255;
buf[n++] = (v >>> 24) & 255;
this.n = n;
}
/** u32 byte length + UTF-8 bytes (self-ensuring). */
utf8WithU32Len(s) {
const len = s.length;
this.ensure(4 + len * 3); // worst-case UTF-8 is 3 bytes per UTF-16 unit
// Short strings: inline char copy (cheaper than a native call), guarded by a
// quick ASCII scan. Anything longer — or non-ASCII — goes to encodeInto.
if (len <= INLINE_STR_MAX) {
let ascii = true;
for (let i = 0; i < len; i++) {
if (s.charCodeAt(i) > 127) {
ascii = false;
break;
}
}
if (ascii) {
// Inline stores, not writeU32: a per-string method call in the
// unoptimized tier is exactly what this class's header rules out.
const buf = this.buf;
let n = this.n;
buf[n++] = len & 255;
buf[n++] = (len >> 8) & 255;
buf[n++] = (len >> 16) & 255;
buf[n++] = (len >>> 24) & 255;
for (let i = 0; i < len; i++)
buf[n + i] = s.charCodeAt(i);
this.n = n + len;
return;
}
}
// Bulk path: encodeInto writes UTF-8 straight into the buffer (no alloc, no
// per-char loop); backpatch the byte length once it's known.
const lenPos = this.n;
this.n += 4;
const written = encoder.encodeInto(s, this.buf.subarray(this.n)).written;
this.buf[lenPos] = written & 255;
this.buf[lenPos + 1] = (written >> 8) & 255;
this.buf[lenPos + 2] = (written >> 16) & 255;
this.buf[lenPos + 3] = (written >>> 24) & 255;
this.n += written;
}
}
/**
* Shared machinery for walk-path child stubs (see `hast/child-stub.ts` and
* `mdast/child-stub.ts`): lightweight stand-ins that carry arena id + type
* eagerly and defer the full-arena snapshot until a plugin reads a real field.
* Passthrough children (the common replaceNode shape) thus compile to one-word
* refs without ever serializing the arena.
*/
/**
* Descriptor template for one node type's stub fields. Own enumerable getters
* so a spread copy carries correct values. `position`/`data` ride on every
* stub; they may yield `undefined` where a materialized node omits the key —
* accepted drift, invisible to `toEqual`.
*/
export declare function stubDescriptors(fields: readonly string[]): PropertyDescriptorMap;
/** Node tags are dense small ints, so the per-child stub loop indexes flat
* arrays instead of paying Map/dictionary lookups. */
export declare function flatByTag<T>(table: Readonly<Record<number, T>>): readonly (T | undefined)[];
/**
* Shared machinery for walk-path child stubs (see `hast/child-stub.ts` and
* `mdast/child-stub.ts`): lightweight stand-ins that carry arena id + type
* eagerly and defer the full-arena snapshot until a plugin reads a real field.
* Passthrough children (the common replaceNode shape) thus compile to one-word
* refs without ever serializing the arena.
*/
/** Stub → materialized arena node, filled on the first real-field read. */
const REAL_NODES = new WeakMap();
/** One shared memoizing getter per field name, so stubs install shared
* descriptor templates instead of allocating per-field closures per node. */
const FIELD_GETTERS = new Map();
function fieldGetter(key) {
let getter = FIELD_GETTERS.get(key);
if (getter === undefined) {
getter = function () {
let real = REAL_NODES.get(this);
if (real === undefined) {
real = this._resolver.materializeOne(this._id);
REAL_NODES.set(this, real);
}
const value = real[key];
Object.defineProperty(this, key, {
value,
writable: true,
enumerable: true,
configurable: true,
});
return value;
};
FIELD_GETTERS.set(key, getter);
}
return getter;
}
/**
* Descriptor template for one node type's stub fields. Own enumerable getters
* so a spread copy carries correct values. `position`/`data` ride on every
* stub; they may yield `undefined` where a materialized node omits the key —
* accepted drift, invisible to `toEqual`.
*/
export function stubDescriptors(fields) {
const map = {};
for (const key of [...fields, "position", "data"]) {
map[key] = { get: fieldGetter(key), enumerable: true, configurable: true };
}
return map;
}
/** Node tags are dense small ints, so the per-child stub loop indexes flat
* arrays instead of paying Map/dictionary lookups. */
export function flatByTag(table) {
const flat = [];
for (const tag of Object.keys(table)) {
const nodeType = Number(tag);
flat[nodeType] = table[nodeType];
}
return flat;
}
/** `b"MDAR"` magic, read as a little-endian u32. */
export declare const ARENA_MAGIC = 1380009037;
/** `Arena<K>` kind tags carried in the header's `kind` field. */
export declare const KIND_MDAST = 1;
export declare const KIND_HAST = 2;
/** `ArenaNode` `#[repr(C)]` field byte offsets (u32 fields; `node_type` is a u8). */
export declare const FIELD: {
readonly id: 0;
readonly node_type: 4;
readonly parent: 8;
readonly start_offset: 12;
readonly end_offset: 16;
readonly start_line: 20;
readonly start_column: 24;
readonly end_line: 28;
readonly end_column: 32;
readonly children_start: 36;
readonly children_count: 40;
readonly data_offset: 44;
readonly data_len: 48;
};
/** Raw-buffer header byte offsets (4-byte fields, u32 LE). */
export declare const HEADER: {
readonly magic: 0;
readonly kind: 4;
readonly node_struct_size: 8;
readonly node_count: 12;
readonly nodes_offset: 16;
readonly children_count: 20;
readonly children_offset: 24;
readonly type_data_len: 28;
readonly type_data_offset: 32;
readonly source_len: 36;
readonly source_offset: 40;
readonly node_data_count: 44;
readonly node_data_offset: 48;
};
// @generated by `cargo run -p satteri-layout-codegen`. Do not edit by hand.
// Arena raw-buffer layout, declared once in `satteri-layout-codegen/src/schema.rs`.
// The Rust side is pinned to these offsets by the compile-time asserts in
// `satteri-arena/src/generated/layout.rs`.
/** `b"MDAR"` magic, read as a little-endian u32. */
export const ARENA_MAGIC = 0x5241444d;
/** `Arena<K>` kind tags carried in the header's `kind` field. */
export const KIND_MDAST = 1;
export const KIND_HAST = 2;
/** `ArenaNode` `#[repr(C)]` field byte offsets (u32 fields; `node_type` is a u8). */
export const FIELD = {
id: 0,
node_type: 4,
parent: 8,
start_offset: 12,
end_offset: 16,
start_line: 20,
start_column: 24,
end_line: 28,
end_column: 32,
children_start: 36,
children_count: 40,
data_offset: 44,
data_len: 48,
};
/** Raw-buffer header byte offsets (4-byte fields, u32 LE). */
export const HEADER = {
magic: 0,
kind: 4,
node_struct_size: 8,
node_count: 12,
nodes_offset: 16,
children_count: 20,
children_offset: 24,
type_data_len: 28,
type_data_offset: 32,
source_len: 36,
source_offset: 40,
node_data_count: 44,
node_data_offset: 48,
};
export declare const OP_OPEN = 1;
export declare const OP_CLOSE = 2;
export declare const OP_REF = 3;
export declare const OP_KEEP_CHILDREN = 4;
export declare const OP_STR = 5;
export declare const OP_U8 = 6;
export declare const OP_U32 = 7;
export declare const OP_BOOL = 8;
export declare const OP_DATA = 9;
export declare const OP_PROP = 10;
export declare const OP_ALIGN = 11;
export declare const OF_VALUE = 0;
export declare const OF_URL = 1;
export declare const OF_TITLE = 2;
export declare const OF_ALT = 3;
export declare const OF_LANG = 4;
export declare const OF_META = 5;
export declare const OF_IDENTIFIER = 6;
export declare const OF_LABEL = 7;
export declare const OF_NAME = 8;
export declare const OF_REFERENCE_TYPE = 9;
export declare const OF_DEPTH = 10;
export declare const OF_CHECKED = 11;
export declare const OF_START = 12;
export declare const OF_ORDERED = 13;
export declare const OF_SPREAD = 14;
export declare const OF_TAGNAME = 15;
export declare const OF_EXPLICIT = 16;
export declare const CMD_REMOVE = 1;
export declare const CMD_INSERT_BEFORE = 5;
export declare const CMD_INSERT_AFTER = 6;
export declare const CMD_PREPEND_CHILD = 7;
export declare const CMD_APPEND_CHILD = 8;
export declare const CMD_WRAP = 9;
export declare const CMD_REPLACE = 11;
export declare const CMD_SET_PROPERTY = 12;
export declare const CMD_SET_CHILDREN = 13;
export declare const PAYLOAD_RAW_MARKDOWN = 16;
export declare const PAYLOAD_RAW_HTML = 17;
export declare const PAYLOAD_OPSTREAM = 20;
export declare const PROP_STRING = 0;
export declare const PROP_BOOL_TRUE = 1;
export declare const PROP_BOOL_FALSE = 2;
export declare const PROP_SPACE_SEP = 3;
export declare const PROP_COMMA_SEP = 4;
export declare const PROP_INT = 5;
export declare const PROP_NULL = 6;
export declare const MDX_ATTR_BOOLEAN_PROP = 0;
export declare const MDX_ATTR_LITERAL_PROP = 1;
export declare const MDX_ATTR_EXPRESSION_PROP = 2;
export declare const MDX_ATTR_SPREAD = 3;
// @generated by `cargo run -p satteri-layout-codegen`. Do not edit by hand.
// JS<->Rust wire-protocol byte values, declared once in
// `satteri-layout-codegen/src/schema.rs`. Rust twins live in
// `satteri-plugin-api/src/generated/wire_constants.rs` and
// `satteri-ast/src/generated/wire_constants.rs`.
// Op-stream op codes (JS `OpWriter` -> Rust `replay_opstream`).
export const OP_OPEN = 0x01; // [type: u8]
export const OP_CLOSE = 0x02;
export const OP_REF = 0x03; // [id: u32 LE] — splice an existing node
export const OP_KEEP_CHILDREN = 0x04; // splice the anchor node's original children
export const OP_STR = 0x05; // [field: u8][len: u32 LE][utf8]
export const OP_U8 = 0x06; // [field: u8][value: u8]
export const OP_U32 = 0x07; // [field: u8][value: u32 LE]
export const OP_BOOL = 0x08; // [field: u8][0|1]
export const OP_DATA = 0x09; // [len: u32 LE][json utf8]
export const OP_PROP = 0x0a; // [name str][kind: u8][value str] — HAST element property
export const OP_ALIGN = 0x0b; // [len: u32 LE][ColumnAlign bytes] — table column alignment
// Op-stream field ids (single namespace across OP_STR/OP_U8/OP_U32/OP_BOOL).
export const OF_VALUE = 0;
export const OF_URL = 1;
export const OF_TITLE = 2;
export const OF_ALT = 3;
export const OF_LANG = 4;
export const OF_META = 5;
export const OF_IDENTIFIER = 6;
export const OF_LABEL = 7;
export const OF_NAME = 8; // directive / MDX JSX element name
export const OF_REFERENCE_TYPE = 9;
export const OF_DEPTH = 10;
export const OF_CHECKED = 11;
export const OF_START = 12;
export const OF_ORDERED = 13;
export const OF_SPREAD = 14;
export const OF_TAGNAME = 15; // HAST element tag name
export const OF_EXPLICIT = 16; // MDX JSX `_mdxExplicitJsx` flag
// Command bytes (0x01–0x0F range). Each is followed by [nodeId: u32 LE];
// structural commands then carry [payloadType: u8][payload…].
export const CMD_REMOVE = 0x01;
export const CMD_INSERT_BEFORE = 0x05;
export const CMD_INSERT_AFTER = 0x06;
export const CMD_PREPEND_CHILD = 0x07;
export const CMD_APPEND_CHILD = 0x08;
export const CMD_WRAP = 0x09;
export const CMD_REPLACE = 0x0b;
export const CMD_SET_PROPERTY = 0x0c; // [valueType: u8][name str][value str], PROP_* value kinds
export const CMD_SET_CHILDREN = 0x0d; // payload is a Root-wrapped child list
// Structural-command payload types (0x10+, a range distinct from commands).
export const PAYLOAD_RAW_MARKDOWN = 0x10; // [len: u32 LE][utf8] — re-parsed as markdown
export const PAYLOAD_RAW_HTML = 0x11; // [len: u32 LE][utf8] — re-parsed as HTML/MDX
export const PAYLOAD_OPSTREAM = 0x14; // [len: u32 LE][op bytes] — replayed straight into the arena
// Property value kinds (HAST element properties and SET_PROPERTY commands).
export const PROP_STRING = 0; // UTF-8 value
export const PROP_BOOL_TRUE = 1; // no value bytes
export const PROP_BOOL_FALSE = 2; // no value bytes
export const PROP_SPACE_SEP = 3; // space-separated list (UTF-8)
export const PROP_COMMA_SEP = 4; // comma-separated list (UTF-8)
export const PROP_INT = 5; // decimal string, parsed to i64
export const PROP_NULL = 6; // no value bytes
// MDX JSX attribute kinds (MDAST and HAST MDX JSX element type_data).
export const MDX_ATTR_BOOLEAN_PROP = 0; // name only, no value
export const MDX_ATTR_LITERAL_PROP = 1; // name="literal"
export const MDX_ATTR_EXPRESSION_PROP = 2; // name={expr}
export const MDX_ATTR_SPREAD = 3; // {...expr}
/**
* Opaque handles to Rust-owned arenas. The napi-generated `index.d.ts` refers
* to `MdastHandle`/`HastHandle`/`AnyHandle` as bare, undeclared names; these
* global declarations give those signatures real types. The kind brand makes
* passing a hast handle to an mdast entry point (a runtime error in Rust) a
* compile error in TS.
*/
declare global {
interface MdastHandle {
readonly __satteriHandleKind: "mdast";
}
interface HastHandle {
readonly __satteriHandleKind: "hast";
}
/** Handle accepted by kind-agnostic entry points (drop, serialize, …). */
type AnyHandle = MdastHandle | HastHandle;
}
type MdastHandleAlias = MdastHandle;
type HastHandleAlias = HastHandle;
type AnyHandleAlias = AnyHandle;
export type { MdastHandleAlias as MdastHandle, HastHandleAlias as HastHandle, AnyHandleAlias as AnyHandle, };
export {};
import type { LazyChildResolver } from "../lazy-child-resolver.js";
import type { HastNode } from "../types.js";
import type { HastReader } from "./hast-reader.js";
type HastResolver = LazyChildResolver<HastReader, HastNode>;
/**
* Walk-path child stub: arena id + `type` eagerly, every other field a lazy
* forward to the materialized node (first read snapshots the arena via
* `materializeOne`, which enforces the handle epoch — the pass seal is checked
* where the stubs are built). Spread/identity rules are enforced by `nid()`
* in hast-visitor.ts.
*/
export declare class HastChildStub {
_resolver: HastResolver;
_id: number;
type: string;
constructor(resolver: HastResolver, id: number, nodeType: number);
}
export {};
import { flatByTag, stubDescriptors } from "../child-stub.js";
import { NAME_TO_TYPE, TYPE_NAMES } from "./generated/node-types.js";
const N = NAME_TO_TYPE;
/** Per-type stub fields, mirroring `materializeHastNode`'s switch. */
const HAST_STUB_FIELDS = {
[N.root]: ["children"],
[N.element]: ["tagName", "properties", "children"],
[N.text]: ["value"],
[N.comment]: ["value"],
[N.doctype]: [],
[N.raw]: ["value"],
[N.mdxJsxFlowElement]: ["name", "attributes", "children"],
[N.mdxJsxTextElement]: ["name", "attributes", "children"],
[N.mdxFlowExpression]: ["value"],
[N.mdxTextExpression]: ["value"],
[N.mdxjsEsm]: ["value"],
};
const TYPE_NAME_BY_TAG = flatByTag(TYPE_NAMES);
const HAST_STUB_DESCRIPTORS = [];
for (const tag of Object.keys(HAST_STUB_FIELDS)) {
const nodeType = Number(tag);
HAST_STUB_DESCRIPTORS[nodeType] = stubDescriptors(HAST_STUB_FIELDS[nodeType]);
}
/** Unknown node types still expose the prelude-backed lazy fields. */
const FALLBACK_DESCRIPTORS = stubDescriptors([]);
/**
* Walk-path child stub: arena id + `type` eagerly, every other field a lazy
* forward to the materialized node (first read snapshots the arena via
* `materializeOne`, which enforces the handle epoch — the pass seal is checked
* where the stubs are built). Spread/identity rules are enforced by `nid()`
* in hast-visitor.ts.
*/
export class HastChildStub {
_resolver;
_id;
type;
constructor(resolver, id, nodeType) {
this._resolver = resolver;
this._id = id;
this.type = TYPE_NAME_BY_TAG[nodeType] ?? `unknown(${nodeType})`;
Object.defineProperties(this, HAST_STUB_DESCRIPTORS[nodeType] ?? FALLBACK_DESCRIPTORS);
}
}
export type HastPropertyValue = string | number | boolean | string[];
export declare function decodeElementProp(kind: number, value: string): HastPropertyValue;
/** Decode a HAST element property value from its wire `(kind, value)` — shared
* by the walk decoder and the snapshot reader so the kind dispatch lives once.
* The bool kinds carry no value string (callers pass `""`). */
import { PROP_BOOL_TRUE, PROP_BOOL_FALSE, PROP_SPACE_SEP, PROP_COMMA_SEP, PROP_INT, } from "../generated/wire-constants.js";
export function decodeElementProp(kind, value) {
switch (kind) {
case PROP_BOOL_TRUE:
return true;
case PROP_BOOL_FALSE:
return false;
case PROP_SPACE_SEP:
return value.split(" ").filter((s) => s.length > 0);
case PROP_COMMA_SEP:
return value
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0);
case PROP_INT:
return Number(value);
default:
return value; // PROP_STRING
}
}
/** Node-type tag -> canonical AST name. */
export declare const TYPE_NAMES: Readonly<Record<number, string>>;
/** Canonical AST name -> node-type tag. */
export declare const NAME_TO_TYPE: Readonly<Record<string, number>>;
/** Names a plugin can subscribe to (every node except `root`). */
export declare const VISITOR_KEYS: ReadonlySet<string>;
/** Name -> tag for the types the op-stream can encode; one lookup gates AND
* resolves the emit-path tag. Excluded names (see `*_OPSTREAM_EXCLUDED` in
* schema.rs) have no encoding — the visitor throws for them. */
export declare const HAST_OPSTREAM_TYPES: Readonly<Record<string, number>>;
/** Names of the variable-length `custom` node types (hand-written or
* `Tail`-generated codec). The op-stream round-trip oracle asserts it covers
* every one, so a forgotten or drifted encode/decode arm fails loudly. */
export declare const HAST_CUSTOM_TYPES: readonly string[];
// @generated by `cargo run -p satteri-layout-codegen`. Do not edit by hand.
/** Node-type tag -> canonical AST name. */
export const TYPE_NAMES = {
0: "root",
1: "element",
2: "text",
3: "comment",
4: "doctype",
5: "raw",
10: "mdxJsxFlowElement",
11: "mdxJsxTextElement",
12: "mdxFlowExpression",
13: "mdxjsEsm",
14: "mdxTextExpression",
};
/** Canonical AST name -> node-type tag. */
export const NAME_TO_TYPE = {
root: 0,
element: 1,
text: 2,
comment: 3,
doctype: 4,
raw: 5,
mdxJsxFlowElement: 10,
mdxJsxTextElement: 11,
mdxFlowExpression: 12,
mdxjsEsm: 13,
mdxTextExpression: 14,
};
/** Names a plugin can subscribe to (every node except `root`). */
export const VISITOR_KEYS = new Set([
"element",
"text",
"comment",
"doctype",
"raw",
"mdxJsxFlowElement",
"mdxJsxTextElement",
"mdxFlowExpression",
"mdxjsEsm",
"mdxTextExpression",
]);
/** Name -> tag for the types the op-stream can encode; one lookup gates AND
* resolves the emit-path tag. Excluded names (see `*_OPSTREAM_EXCLUDED` in
* schema.rs) have no encoding — the visitor throws for them. */
export const HAST_OPSTREAM_TYPES = {
element: 1,
text: 2,
comment: 3,
raw: 5,
mdxJsxFlowElement: 10,
mdxJsxTextElement: 11,
mdxFlowExpression: 12,
mdxjsEsm: 13,
mdxTextExpression: 14,
};
/** Names of the variable-length `custom` node types (hand-written or
* `Tail`-generated codec). The op-stream round-trip oracle asserts it covers
* every one, so a forgotten or drifted encode/decode arm fails loudly. */
export const HAST_CUSTOM_TYPES = [
"element",
"mdxJsxFlowElement",
"mdxJsxTextElement",
];
import type { AnyHandle } from "./handles.js";
/** Record that `handle`'s arena was rebuilt. Resolvers created before the bump
* refuse to take a fresh snapshot afterwards (their ids are stale). */
export declare function markHandleMutated(handle: AnyHandle): void;
/**
* Lazy node materializer for the walk paths: serializes the handle's arena
* once, on the first stub materialization, then materializes nodes from that
* snapshot. Subclasses supply reader construction and per-node materialization
* so the hot path stays free of per-node closures.
*/
export declare abstract class LazyChildResolver<TReader, TNode> {
#private;
constructor(handle: AnyHandle);
protected abstract createReader(wire: Uint8Array): TReader;
protected abstract materializeNode(reader: TReader, nodeId: number): TNode;
protected abstract readParentId(reader: TReader, nodeId: number): number;
protected abstract readChildIds(reader: TReader, nodeId: number): number[];
/**
* Mark the visitor pass over. Child ids were captured at match time; once
* the pass's mutations land the arena is rebuilt and ids renumbered, so a
* later snapshot would map those stale ids onto the wrong nodes (or out of
* range). Failing loudly here beats silently wrong children.
*/
seal(): void;
/** Guards the `.children` getters: a first read after the pass is refused
* outright, even when no mutation landed — match-time ids cannot be trusted
* once the plugin no longer controls the tree. */
assertUnsealed(): void;
/** Materialize one node for a child stub's first real-field read. */
materializeOne(nodeId: number): TNode;
/** Arena id of `nodeId`'s parent in the pass snapshot, or undefined at the root. */
parentIdOf(nodeId: number): number | undefined;
/** Index of `nodeId` within its parent's children in the pass snapshot,
* or undefined at the root. */
indexInParent(nodeId: number): number | undefined;
}
import { serializeHandle } from "#binding";
/** Rebuild count per handle: bumped whenever a command buffer lands and
* renumbers the arena, invalidating ids captured before it. */
const HANDLE_EPOCHS = new WeakMap();
/** Record that `handle`'s arena was rebuilt. Resolvers created before the bump
* refuse to take a fresh snapshot afterwards (their ids are stale). */
export function markHandleMutated(handle) {
HANDLE_EPOCHS.set(handle, (HANDLE_EPOCHS.get(handle) ?? 0) + 1);
}
/** Arena sentinel in the node struct's parent field: the node has no parent. */
const NO_PARENT = 0xffffffff;
/**
* Lazy node materializer for the walk paths: serializes the handle's arena
* once, on the first stub materialization, then materializes nodes from that
* snapshot. Subclasses supply reader construction and per-node materialization
* so the hot path stays free of per-node closures.
*/
export class LazyChildResolver {
#handle;
#reader = null;
#sealed = false;
#epoch;
constructor(handle) {
this.#handle = handle;
this.#epoch = HANDLE_EPOCHS.get(handle) ?? 0;
}
/**
* Mark the visitor pass over. Child ids were captured at match time; once
* the pass's mutations land the arena is rebuilt and ids renumbered, so a
* later snapshot would map those stale ids onto the wrong nodes (or out of
* range). Failing loudly here beats silently wrong children.
*/
seal() {
this.#sealed = true;
}
/** Guards the `.children` getters: a first read after the pass is refused
* outright, even when no mutation landed — match-time ids cannot be trusted
* once the plugin no longer controls the tree. */
assertUnsealed() {
if (this.#sealed) {
throw new Error("Cannot read node content: this node was retained past its visitor pass " +
"and the tree may have changed since; read it inside the visitor.");
}
}
#ensureReader() {
if (this.#reader === null) {
// A node id proves the tree was read in-pass, so a deferred snapshot is
// still faithful as long as no command buffer rebuilt the arena since
// match time (ids renumber on rebuild). An existing reader is always
// safe: the snapshot is an immutable pre-mutation copy.
if ((HANDLE_EPOCHS.get(this.#handle) ?? 0) !== this.#epoch) {
throw new Error("Cannot read node content: this node was retained past its visitor pass " +
"and the tree has changed since; read it inside the visitor.");
}
// The serialized buffer already carries each node's `data` blob (read
// eagerly by the materializer), and the arena isn't mutated mid-visit —
// so no separate lazy NAPI fetch is needed. This also keeps walk-path
// children consistent with the fully materialized tree (no `data` key
// when a node has none).
this.#reader = this.createReader(serializeHandle(this.#handle));
}
return this.#reader;
}
/** Materialize one node for a child stub's first real-field read. */
materializeOne(nodeId) {
return this.materializeNode(this.#ensureReader(), nodeId);
}
/** Arena id of `nodeId`'s parent in the pass snapshot, or undefined at the root. */
parentIdOf(nodeId) {
this.assertUnsealed();
const parentId = this.readParentId(this.#ensureReader(), nodeId);
return parentId === NO_PARENT ? undefined : parentId;
}
/** Per-parent child-id→index maps, built lazily: null until a plugin calls
* `indexInParent` (most never do). Cache-safe because the snapshot is immutable. */
#childIndexByParent = null;
/** Index of `nodeId` within its parent's children in the pass snapshot,
* or undefined at the root. */
indexInParent(nodeId) {
this.assertUnsealed();
const reader = this.#ensureReader();
const parentId = this.readParentId(reader, nodeId);
if (parentId === NO_PARENT)
return undefined;
const byParent = (this.#childIndexByParent ??= new Map());
let indexById = byParent.get(parentId);
if (indexById === undefined) {
const map = new Map();
this.readChildIds(reader, parentId).forEach((id, i) => map.set(id, i));
byParent.set(parentId, map);
indexById = map;
}
return indexById.get(nodeId);
}
}
import type { LazyChildResolver } from "../lazy-child-resolver.js";
import type { MdastNode } from "../types.js";
import type { MdastReader } from "./mdast-reader.js";
type MdastResolver = LazyChildResolver<MdastReader, MdastNode>;
/**
* Walk-path child stub: arena id + `type` eagerly, every other field a lazy
* forward to the materialized node (first read snapshots the arena via
* `materializeOne`, which enforces the handle epoch — the pass seal is checked
* where the stubs are built). Spread/identity rules are enforced by `nid()`
* (authoritative doc in hast-visitor.ts).
*/
export declare class MdastChildStub {
_resolver: MdastResolver;
_id: number;
type: string;
constructor(resolver: MdastResolver, id: number, nodeType: number);
}
export {};
import { flatByTag, stubDescriptors } from "../child-stub.js";
import { MDAST_LAYOUT_KEYS } from "./generated/layout.js";
import { NAME_TO_TYPE, TYPE_NAMES } from "./generated/node-types.js";
import { LEAF_TYPES } from "./mdast-materializer.js";
const N = NAME_TO_TYPE;
/** Per-type stub fields for the types `addTypeProperties` hand-writes; the
* fixed-layout types come from the generated `MDAST_LAYOUT_KEYS`. */
const HAND_WRITTEN_FIELDS = {
[N.list]: ["ordered", "start", "spread"],
[N.listItem]: ["spread", "checked"],
[N.table]: ["align"],
[N.containerDirective]: ["name", "attributes"],
[N.leafDirective]: ["name", "attributes"],
[N.textDirective]: ["name", "attributes"],
[N.mdxJsxFlowElement]: ["name", "attributes"],
[N.mdxJsxTextElement]: ["name", "attributes"],
};
const TYPE_NAME_BY_TAG = flatByTag(TYPE_NAMES);
const MDAST_STUB_DESCRIPTORS = [];
for (const tag of Object.keys(TYPE_NAMES)) {
const nodeType = Number(tag);
const fields = [...(MDAST_LAYOUT_KEYS[nodeType] ?? HAND_WRITTEN_FIELDS[nodeType] ?? [])];
if (!LEAF_TYPES.has(nodeType))
fields.push("children");
MDAST_STUB_DESCRIPTORS[nodeType] = stubDescriptors(fields);
}
/** Unknown node types still expose the prelude-backed lazy fields. */
const FALLBACK_DESCRIPTORS = stubDescriptors([]);
/**
* Walk-path child stub: arena id + `type` eagerly, every other field a lazy
* forward to the materialized node (first read snapshots the arena via
* `materializeOne`, which enforces the handle epoch — the pass seal is checked
* where the stubs are built). Spread/identity rules are enforced by `nid()`
* (authoritative doc in hast-visitor.ts).
*/
export class MdastChildStub {
_resolver;
_id;
type;
constructor(resolver, id, nodeType) {
this._resolver = resolver;
this._id = id;
this.type = TYPE_NAME_BY_TAG[nodeType] ?? `unknown(${nodeType})`;
Object.defineProperties(this, MDAST_STUB_DESCRIPTORS[nodeType] ?? FALLBACK_DESCRIPTORS);
}
}
export declare function decodeColumnAlign(byte: number): string | null;
/** Decode a table column-alignment byte — shared by the generated walk decoder
* and the snapshot reader so the mapping lives in one place. */
const ALIGN_NAMES = [null, "left", "right", "center"];
export function decodeColumnAlign(byte) {
return ALIGN_NAMES[byte] ?? null;
}
import type { MdastReader } from "../mdast-reader.js";
/** Materialized property names per tag (the non-skip `js` names above),
* precomputed so `materializeMdastFields` doesn't rebuild them per node.
* Exported for the child-stub field tables. */
export declare const MDAST_LAYOUT_KEYS: Readonly<Record<number, readonly string[]>>;
/**
* Decode a node's type-specific `type_data` from the walk buffer onto `node`,
* driven by `MDAST_LAYOUTS` (fixed-field types) and `MDAST_TAILS` (counted
* attribute lists). Returns `false` for tags in neither, so the caller falls
* through to the remaining hand-written cases (list, listItem).
*/
export declare function decodeMdastTypeData(view: DataView, buf: Uint8Array, start: number, nodeType: number, node: Record<string, unknown>): boolean;
/**
* Attach a node's fixed `type_data` fields, materialized from the arena
* snapshot, driven by `MDAST_LAYOUTS`. Returns `false` for tags with no entry,
* so the materializer falls through to its hand-written cases (list, table,
* directives, MDX JSX). Fields resolve lazily on first access.
*/
export declare function materializeMdastFields(reader: MdastReader, node: object, nodeId: number, nodeType: number): boolean;
// @generated by `cargo run -p satteri-layout-codegen`. Do not edit by hand.
import { ru16, ru32, rstr } from "../../wire-read.js";
import { restorePhantomSpaces } from "../../phantom.js";
import { lazyGroup } from "../../lazy-props.js";
import { decodeMdxJsxAttr } from "../../mdx-attr.js";
import { decodeColumnAlign } from "../column-align.js";
/** Walk-wire fields per node-type tag (see Rust `write_mdast_type_data_inline`). */
const MDAST_LAYOUTS = {
2: [{ js: "depth", offset: 0, kind: "u8", default: 1 }],
7: [{ js: "value", offset: 0, kind: "str32" }],
10: [{ js: "value", offset: 0, kind: "str32" }],
13: [{ js: "value", offset: 0, kind: "str32" }],
25: [{ js: "value", offset: 0, kind: "str32" }],
26: [{ js: "value", offset: 0, kind: "str32" }],
8: [
{ js: "lang", offset: 0, kind: "str16", nullable: true },
{ js: "meta", offset: 8, kind: "str16", nullable: true },
{ js: "value", offset: 16, kind: "str32" },
],
9: [
{ js: "url", offset: 0, kind: "str16" },
{ js: "title", offset: 8, kind: "str16", nullable: true },
{ js: "identifier", offset: 16, kind: "str16" },
{ js: "label", offset: 24, kind: "str16" },
],
15: [
{ js: "url", offset: 0, kind: "str16" },
{ js: "title", offset: 8, kind: "str16", nullable: true },
],
16: [
{ js: "url", offset: 0, kind: "str16" },
{ js: "alt", offset: 8, kind: "str16" },
{ js: "title", offset: 16, kind: "str16", nullable: true },
],
17: [
{ js: "identifier", offset: 0, kind: "str16" },
{ js: "label", offset: 8, kind: "str16" },
{ js: "referenceType", offset: 16, kind: "u8", values: ["shortcut", "collapsed", "full"] },
],
18: [
{ js: "identifier", offset: 0, kind: "str16" },
{ js: "label", offset: 8, kind: "str16" },
{ js: "referenceType", offset: 16, kind: "u8", values: ["shortcut", "collapsed", "full"] },
{ js: "alt", offset: 20, kind: "str16" },
],
19: [
{ js: "identifier", offset: 0, kind: "str16" },
{ js: "label", offset: 8, kind: "str16" },
],
20: [
{ js: "identifier", offset: 0, kind: "str16" },
{ js: "label", offset: 8, kind: "str16" },
{ js: "", offset: 16, kind: "u8", skip: true },
],
27: [
{ js: "meta", offset: 0, kind: "str16", nullable: true },
{ js: "value", offset: 8, kind: "str32" },
],
28: [{ js: "value", offset: 8, kind: "str32" }],
102: [{ js: "value", offset: 0, kind: "str32", phantom: true }],
103: [{ js: "value", offset: 0, kind: "str32", phantom: true }],
104: [{ js: "value", offset: 0, kind: "str32", phantom: true }],
};
/** Materialized property names per tag (the non-skip `js` names above),
* precomputed so `materializeMdastFields` doesn't rebuild them per node.
* Exported for the child-stub field tables. */
export const MDAST_LAYOUT_KEYS = {
2: ["depth"],
7: ["value"],
10: ["value"],
13: ["value"],
25: ["value"],
26: ["value"],
8: ["lang", "meta", "value"],
9: ["url", "title", "identifier", "label"],
15: ["url", "title"],
16: ["url", "alt", "title"],
17: ["identifier", "label", "referenceType"],
18: ["identifier", "label", "referenceType", "alt"],
19: ["identifier", "label"],
20: ["identifier", "label"],
27: ["meta", "value"],
28: ["value"],
102: ["value"],
103: ["value"],
104: ["value"],
};
/** Walk-wire tail descriptors (a name head, then a counted item list) for
* the variable-length attribute types the generic decoder below handles
* without a hand-written case: `map` is a string key/value object
* (directives), `jsx` a typed MDX-JSX attribute array. */
const MDAST_TAILS = {
21: { head: [], item: [{ js: "align", kind: "u8" }], bytes: { attrsKey: "align" } },
30: {
head: [{ js: "name", kind: "str16" }],
item: [
{ js: "key", kind: "str16" },
{ js: "value", kind: "str16" },
],
map: { attrsKey: "attributes", key: "key", value: "value" },
},
31: {
head: [{ js: "name", kind: "str16" }],
item: [
{ js: "key", kind: "str16" },
{ js: "value", kind: "str16" },
],
map: { attrsKey: "attributes", key: "key", value: "value" },
},
32: {
head: [{ js: "name", kind: "str16" }],
item: [
{ js: "key", kind: "str16" },
{ js: "value", kind: "str16" },
],
map: { attrsKey: "attributes", key: "key", value: "value" },
},
100: {
head: [{ js: "name", kind: "str16" }],
item: [
{ js: "kind", kind: "u8" },
{ js: "name", kind: "str16" },
{ js: "value", kind: "str32" },
],
jsx: { attrsKey: "attributes" },
},
101: {
head: [{ js: "name", kind: "str16" }],
item: [
{ js: "kind", kind: "u8" },
{ js: "name", kind: "str16" },
{ js: "value", kind: "str32" },
],
jsx: { attrsKey: "attributes" },
},
};
/**
* Decode a node's type-specific `type_data` from the walk buffer onto `node`,
* driven by `MDAST_LAYOUTS` (fixed-field types) and `MDAST_TAILS` (counted
* attribute lists). Returns `false` for tags in neither, so the caller falls
* through to the remaining hand-written cases (list, listItem).
*/
export function decodeMdastTypeData(view, buf, start, nodeType, node) {
const fields = MDAST_LAYOUTS[nodeType];
if (fields !== undefined) {
let pos = start;
for (const f of fields) {
if (f.kind === "u8") {
const b = buf[pos];
pos += 1;
if (f.skip)
continue;
node[f.js] = f.values ? (f.values[b] ?? f.values[0]) : b;
}
else {
const len = f.kind === "str32" ? ru32(view, pos) : ru16(view, pos);
pos += f.kind === "str32" ? 4 : 2;
const raw = rstr(buf, pos, len);
pos += len;
if (f.skip)
continue;
node[f.js] = f.nullable && len === 0 ? null : f.phantom ? restorePhantomSpaces(raw) : raw;
}
}
return true;
}
const tail = MDAST_TAILS[nodeType];
if (tail !== undefined) {
let pos = start;
// Reads are inlined and advance `pos` in place (no per-field tuple): this
// runs per attribute item in the matched-node decode path, and the
// unoptimized tiers CodSpeed measures won't elide the allocation.
for (const f of tail.head) {
let value;
if (f.kind === "u8") {
value = buf[pos];
pos += 1;
}
else {
const len = f.kind === "str32" ? ru32(view, pos) : ru16(view, pos);
pos += f.kind === "str32" ? 4 : 2;
const raw = rstr(buf, pos, len);
pos += len;
value = f.phantom ? restorePhantomSpaces(raw) : raw;
}
// MDX JSX elements carry a nullable name (empty → null); map heads keep it.
node[f.js] = tail.jsx !== undefined && value === "" ? null : value;
}
const count = ru16(view, pos);
pos += 2;
const readItem = () => {
const item = {};
for (const f of tail.item) {
if (f.kind === "u8") {
item[f.js] = buf[pos];
pos += 1;
}
else {
const len = f.kind === "str32" ? ru32(view, pos) : ru16(view, pos);
pos += f.kind === "str32" ? 4 : 2;
const raw = rstr(buf, pos, len);
pos += len;
item[f.js] = f.phantom ? restorePhantomSpaces(raw) : raw;
}
}
return item;
};
if (tail.map !== undefined) {
const attrs = {};
for (let i = 0; i < count; i++) {
const item = readItem();
attrs[item[tail.map.key]] = item[tail.map.value];
}
node[tail.map.attrsKey] = attrs;
}
else if (tail.jsx !== undefined) {
const attrs = [];
for (let i = 0; i < count; i++) {
const item = readItem();
attrs.push(decodeMdxJsxAttr(item.kind, item.name, item.value));
}
node[tail.jsx.attrsKey] = attrs;
}
else {
const align = [];
for (let i = 0; i < count; i++) {
const item = readItem();
align.push(decodeColumnAlign(item.align));
}
node[tail.bytes.attrsKey] = align;
}
return true;
}
return false;
}
/**
* Attach a node's fixed `type_data` fields, materialized from the arena
* snapshot, driven by `MDAST_LAYOUTS`. Returns `false` for tags with no entry,
* so the materializer falls through to its hand-written cases (list, table,
* directives, MDX JSX). Fields resolve lazily on first access.
*/
export function materializeMdastFields(reader, node, nodeId, nodeType) {
const fields = MDAST_LAYOUTS[nodeType];
if (fields === undefined)
return false;
lazyGroup(node, MDAST_LAYOUT_KEYS[nodeType], () => {
const data = reader.getTypeData(nodeId);
const out = {};
for (const f of fields) {
if (f.kind === "u8") {
// Short type_data falls back to the layout's declared default,
// matching the Rust walk serializer.
const b = data[f.offset] ?? f.default ?? 0;
if (!f.skip)
out[f.js] = f.values ? (f.values[b] ?? f.values[0]) : b;
}
else {
// Short type_data (e.g. a 20-byte ReferenceData-only imageReference)
// yields an empty ref, mirroring the Rust decoders' bounds checks.
const ref = f.offset + 8 <= data.length
? reader.readStringRef(data, f.offset)
: { offset: 0, len: 0 };
if (f.skip)
continue;
const s = f.nullable && ref.len === 0 ? null : reader.getString(ref.offset, ref.len);
out[f.js] = f.phantom && typeof s === "string" ? restorePhantomSpaces(s) : s;
}
}
return out;
});
return true;
}
/** Node-type tag -> canonical AST name. */
export declare const TYPE_NAMES: Readonly<Record<number, string>>;
/** Canonical AST name -> node-type tag. */
export declare const NAME_TO_TYPE: Readonly<Record<string, number>>;
/** Node-type tag by Rust enum variant name. */
export declare const NodeType: Readonly<{
readonly Root: 0;
readonly Paragraph: 1;
readonly Heading: 2;
readonly ThematicBreak: 3;
readonly Blockquote: 4;
readonly List: 5;
readonly ListItem: 6;
readonly Html: 7;
readonly Code: 8;
readonly Definition: 9;
readonly Text: 10;
readonly Emphasis: 11;
readonly Strong: 12;
readonly InlineCode: 13;
readonly Break: 14;
readonly Link: 15;
readonly Image: 16;
readonly LinkReference: 17;
readonly ImageReference: 18;
readonly FootnoteDefinition: 19;
readonly FootnoteReference: 20;
readonly Table: 21;
readonly TableRow: 22;
readonly TableCell: 23;
readonly Delete: 24;
readonly Yaml: 25;
readonly Toml: 26;
readonly Math: 27;
readonly InlineMath: 28;
readonly ContainerDirective: 30;
readonly LeafDirective: 31;
readonly TextDirective: 32;
readonly Superscript: 33;
readonly Subscript: 34;
readonly MdxJsxFlowElement: 100;
readonly MdxJsxTextElement: 101;
readonly MdxFlowExpression: 102;
readonly MdxTextExpression: 103;
readonly MdxjsEsm: 104;
}>;
/** Node-type tag -> Rust variant name (`typeName` diagnostics). */
export declare const NodeTypeName: Readonly<Record<number, string>>;
/** Names a plugin can subscribe to (every node except `root`). */
export declare const VISITOR_KEYS: ReadonlySet<string>;
/** Name -> tag for the types the op-stream can encode; one lookup gates AND
* resolves the emit-path tag. Excluded names (see `*_OPSTREAM_EXCLUDED` in
* schema.rs) have no encoding — the visitor throws for them. */
export declare const MDAST_OPSTREAM_TYPES: Readonly<Record<string, number>>;
/** Names of the variable-length `custom` node types (hand-written or
* `Tail`-generated codec). The op-stream round-trip oracle asserts it covers
* every one, so a forgotten or drifted encode/decode arm fails loudly. */
export declare const MDAST_CUSTOM_TYPES: readonly string[];
// @generated by `cargo run -p satteri-layout-codegen`. Do not edit by hand.
/** Node-type tag -> canonical AST name. */
export const TYPE_NAMES = {
0: "root",
1: "paragraph",
2: "heading",
3: "thematicBreak",
4: "blockquote",
5: "list",
6: "listItem",
7: "html",
8: "code",
9: "definition",
10: "text",
11: "emphasis",
12: "strong",
13: "inlineCode",
14: "break",
15: "link",
16: "image",
17: "linkReference",
18: "imageReference",
19: "footnoteDefinition",
20: "footnoteReference",
21: "table",
22: "tableRow",
23: "tableCell",
24: "delete",
25: "yaml",
26: "toml",
27: "math",
28: "inlineMath",
30: "containerDirective",
31: "leafDirective",
32: "textDirective",
33: "superscript",
34: "subscript",
100: "mdxJsxFlowElement",
101: "mdxJsxTextElement",
102: "mdxFlowExpression",
103: "mdxTextExpression",
104: "mdxjsEsm",
};
/** Canonical AST name -> node-type tag. */
export const NAME_TO_TYPE = {
root: 0,
paragraph: 1,
heading: 2,
thematicBreak: 3,
blockquote: 4,
list: 5,
listItem: 6,
html: 7,
code: 8,
definition: 9,
text: 10,
emphasis: 11,
strong: 12,
inlineCode: 13,
break: 14,
link: 15,
image: 16,
linkReference: 17,
imageReference: 18,
footnoteDefinition: 19,
footnoteReference: 20,
table: 21,
tableRow: 22,
tableCell: 23,
delete: 24,
yaml: 25,
toml: 26,
math: 27,
inlineMath: 28,
containerDirective: 30,
leafDirective: 31,
textDirective: 32,
superscript: 33,
subscript: 34,
mdxJsxFlowElement: 100,
mdxJsxTextElement: 101,
mdxFlowExpression: 102,
mdxTextExpression: 103,
mdxjsEsm: 104,
};
/** Node-type tag by Rust enum variant name. */
export const NodeType = Object.freeze({
Root: 0,
Paragraph: 1,
Heading: 2,
ThematicBreak: 3,
Blockquote: 4,
List: 5,
ListItem: 6,
Html: 7,
Code: 8,
Definition: 9,
Text: 10,
Emphasis: 11,
Strong: 12,
InlineCode: 13,
Break: 14,
Link: 15,
Image: 16,
LinkReference: 17,
ImageReference: 18,
FootnoteDefinition: 19,
FootnoteReference: 20,
Table: 21,
TableRow: 22,
TableCell: 23,
Delete: 24,
Yaml: 25,
Toml: 26,
Math: 27,
InlineMath: 28,
ContainerDirective: 30,
LeafDirective: 31,
TextDirective: 32,
Superscript: 33,
Subscript: 34,
MdxJsxFlowElement: 100,
MdxJsxTextElement: 101,
MdxFlowExpression: 102,
MdxTextExpression: 103,
MdxjsEsm: 104,
});
/** Node-type tag -> Rust variant name (`typeName` diagnostics). */
export const NodeTypeName = {
0: "Root",
1: "Paragraph",
2: "Heading",
3: "ThematicBreak",
4: "Blockquote",
5: "List",
6: "ListItem",
7: "Html",
8: "Code",
9: "Definition",
10: "Text",
11: "Emphasis",
12: "Strong",
13: "InlineCode",
14: "Break",
15: "Link",
16: "Image",
17: "LinkReference",
18: "ImageReference",
19: "FootnoteDefinition",
20: "FootnoteReference",
21: "Table",
22: "TableRow",
23: "TableCell",
24: "Delete",
25: "Yaml",
26: "Toml",
27: "Math",
28: "InlineMath",
30: "ContainerDirective",
31: "LeafDirective",
32: "TextDirective",
33: "Superscript",
34: "Subscript",
100: "MdxJsxFlowElement",
101: "MdxJsxTextElement",
102: "MdxFlowExpression",
103: "MdxTextExpression",
104: "MdxjsEsm",
};
/** Names a plugin can subscribe to (every node except `root`). */
export const VISITOR_KEYS = new Set([
"paragraph",
"heading",
"thematicBreak",
"blockquote",
"list",
"listItem",
"html",
"code",
"definition",
"text",
"emphasis",
"strong",
"inlineCode",
"break",
"link",
"image",
"linkReference",
"imageReference",
"footnoteDefinition",
"footnoteReference",
"table",
"tableRow",
"tableCell",
"delete",
"yaml",
"toml",
"math",
"inlineMath",
"containerDirective",
"leafDirective",
"textDirective",
"superscript",
"subscript",
"mdxJsxFlowElement",
"mdxJsxTextElement",
"mdxFlowExpression",
"mdxTextExpression",
"mdxjsEsm",
]);
/** Name -> tag for the types the op-stream can encode; one lookup gates AND
* resolves the emit-path tag. Excluded names (see `*_OPSTREAM_EXCLUDED` in
* schema.rs) have no encoding — the visitor throws for them. */
export const MDAST_OPSTREAM_TYPES = {
paragraph: 1,
heading: 2,
thematicBreak: 3,
blockquote: 4,
list: 5,
listItem: 6,
html: 7,
code: 8,
definition: 9,
text: 10,
emphasis: 11,
strong: 12,
inlineCode: 13,
break: 14,
link: 15,
image: 16,
linkReference: 17,
imageReference: 18,
footnoteDefinition: 19,
footnoteReference: 20,
table: 21,
tableRow: 22,
tableCell: 23,
delete: 24,
yaml: 25,
toml: 26,
math: 27,
inlineMath: 28,
containerDirective: 30,
leafDirective: 31,
textDirective: 32,
superscript: 33,
subscript: 34,
mdxJsxFlowElement: 100,
mdxJsxTextElement: 101,
mdxFlowExpression: 102,
mdxTextExpression: 103,
mdxjsEsm: 104,
};
/** Names of the variable-length `custom` node types (hand-written or
* `Tail`-generated codec). The op-stream round-trip oracle asserts it covers
* every one, so a forgotten or drifted encode/decode arm fails loudly. */
export const MDAST_CUSTOM_TYPES = [
"list",
"listItem",
"table",
"containerDirective",
"leafDirective",
"textDirective",
"mdxJsxFlowElement",
"mdxJsxTextElement",
];
import type { MdxJsxAttributeUnion } from "./types.js";
export declare function decodeMdxJsxAttr(kind: number, name: string, value: string): MdxJsxAttributeUnion;
/** Decode an MDX JSX attribute from its wire `(kind, name, value)` — shared by
* the generated mdast tail decoder and the hast path so the kind dispatch lives
* once. Expression/spread values carry phantom-space sentinels, restored here. */
import { restorePhantomSpaces } from "./phantom.js";
export function decodeMdxJsxAttr(kind, name, value) {
switch (kind) {
case 0:
return { type: "mdxJsxAttribute", name, value: null };
case 1:
return { type: "mdxJsxAttribute", name, value };
case 2:
return {
type: "mdxJsxAttribute",
name,
value: { type: "mdxJsxAttributeValueExpression", value: restorePhantomSpaces(value) },
};
default:
return { type: "mdxJsxExpressionAttribute", value: restorePhantomSpaces(value) };
}
}
/**
* Low-level op-stream writer shared by the MDAST/HAST declarative compilers.
*
* Emits the compact OPEN/CLOSE/field/REF/KEEP_CHILDREN/PROP stream that Rust
* replays straight into the arena (`replay_opstream` in js_commands.rs; byte
* values in generated/wire-constants.ts) — the only structural encoding for
* plugin-built content. Strings ride ByteWriter's zero-alloc path (inline
* char codes when short ASCII, `encodeInto` otherwise).
*/
import { ByteWriter } from "./byte-writer.js";
export { OF_VALUE, OF_URL, OF_TITLE, OF_ALT, OF_LANG, OF_META, OF_IDENTIFIER, OF_LABEL, OF_NAME, OF_REFERENCE_TYPE, OF_DEPTH, OF_CHECKED, OF_START, OF_ORDERED, OF_SPREAD, OF_TAGNAME, OF_EXPLICIT, PROP_STRING, PROP_BOOL_TRUE, PROP_BOOL_FALSE, PROP_SPACE_SEP, PROP_COMMA_SEP, PROP_INT, PROP_NULL, MDX_ATTR_BOOLEAN_PROP, MDX_ATTR_LITERAL_PROP, MDX_ATTR_EXPRESSION_PROP, MDX_ATTR_SPREAD, } from "./generated/wire-constants.js";
export declare class OpWriter extends ByteWriter {
#private;
constructor();
/**
* Start a compile on this (shared, module-level) writer. Encoding evaluates
* plugin-supplied getters and `toJSON`; if one of those calls a context
* mutation, the nested compile would reset the stream under the outer one
* and corrupt it silently — throw instead. Pair with `end` in a `finally`.
*/
begin(): void;
end(): void;
open(type: number): void;
close(): void;
str(field: number, s: string): void;
u8(field: number, v: number): void;
u32(field: number, v: number): void;
bool(field: number, v: boolean): void;
data(value: unknown): void;
prop(name: string, kind: number, value: string): void;
ref(id: number): void;
/** Table column-alignment codes (0=none, 1=left, 2=right, 3=center). */
align(codes: readonly number[]): void;
keepChildren(): void;
}
/** Emit one MDX JSX attribute as an OP_PROP, mirroring `encode_js_jsx_attrs`:
* null→boolean, string→literal, `{ value }` object→expression, else→boolean. */
export declare function emitMdxAttr(w: OpWriter, a: Record<string, unknown>): void;
/**
* Low-level op-stream writer shared by the MDAST/HAST declarative compilers.
*
* Emits the compact OPEN/CLOSE/field/REF/KEEP_CHILDREN/PROP stream that Rust
* replays straight into the arena (`replay_opstream` in js_commands.rs; byte
* values in generated/wire-constants.ts) — the only structural encoding for
* plugin-built content. Strings ride ByteWriter's zero-alloc path (inline
* char codes when short ASCII, `encodeInto` otherwise).
*/
import { ByteWriter } from "./byte-writer.js";
import { OP_OPEN, OP_CLOSE, OP_REF, OP_KEEP_CHILDREN, OP_STR, OP_U8, OP_U32, OP_BOOL, OP_DATA, OP_PROP, OP_ALIGN, MDX_ATTR_BOOLEAN_PROP, MDX_ATTR_LITERAL_PROP, MDX_ATTR_EXPRESSION_PROP, MDX_ATTR_SPREAD, } from "./generated/wire-constants.js";
// Re-exported so visitors/readers keep importing wire constants from here.
export { OF_VALUE, OF_URL, OF_TITLE, OF_ALT, OF_LANG, OF_META, OF_IDENTIFIER, OF_LABEL, OF_NAME, OF_REFERENCE_TYPE, OF_DEPTH, OF_CHECKED, OF_START, OF_ORDERED, OF_SPREAD, OF_TAGNAME, OF_EXPLICIT, PROP_STRING, PROP_BOOL_TRUE, PROP_BOOL_FALSE, PROP_SPACE_SEP, PROP_COMMA_SEP, PROP_INT, PROP_NULL, MDX_ATTR_BOOLEAN_PROP, MDX_ATTR_LITERAL_PROP, MDX_ATTR_EXPRESSION_PROP, MDX_ATTR_SPREAD, } from "./generated/wire-constants.js";
export class OpWriter extends ByteWriter {
#encoding = false;
constructor() {
super(512);
}
/**
* Start a compile on this (shared, module-level) writer. Encoding evaluates
* plugin-supplied getters and `toJSON`; if one of those calls a context
* mutation, the nested compile would reset the stream under the outer one
* and corrupt it silently — throw instead. Pair with `end` in a `finally`.
*/
begin() {
if (this.#encoding) {
throw new Error("reentrant op-stream compile: a node getter or toJSON invoked a context mutation while its node was being encoded");
}
this.#encoding = true;
this.reset();
}
end() {
this.#encoding = false;
}
open(type) {
this.ensure(2);
this.buf[this.n++] = OP_OPEN;
this.buf[this.n++] = type;
}
close() {
this.ensure(1);
this.buf[this.n++] = OP_CLOSE;
}
str(field, s) {
this.ensure(2);
this.buf[this.n++] = OP_STR;
this.buf[this.n++] = field;
this.utf8WithU32Len(s);
}
u8(field, v) {
this.ensure(3);
this.buf[this.n++] = OP_U8;
this.buf[this.n++] = field;
this.buf[this.n++] = v & 255;
}
u32(field, v) {
this.ensure(6);
this.buf[this.n++] = OP_U32;
this.buf[this.n++] = field;
this.writeU32(v);
}
bool(field, v) {
this.ensure(3);
this.buf[this.n++] = OP_BOOL;
this.buf[this.n++] = field;
this.buf[this.n++] = v ? 1 : 0;
}
data(value) {
const json = JSON.stringify(value);
// `JSON.stringify` yields undefined for a function or a `toJSON`
// returning undefined; a clear error beats a TypeError deep in the
// string encoder.
if (typeof json !== "string") {
throw new Error("node `data` is not JSON-serializable");
}
this.ensure(1);
this.buf[this.n++] = OP_DATA;
this.utf8WithU32Len(json);
}
prop(name, kind, value) {
this.ensure(1);
this.buf[this.n++] = OP_PROP;
this.utf8WithU32Len(name);
this.ensure(1);
this.buf[this.n++] = kind;
this.utf8WithU32Len(value);
}
ref(id) {
this.ensure(5);
this.buf[this.n++] = OP_REF;
this.writeU32(id);
}
/** Table column-alignment codes (0=none, 1=left, 2=right, 3=center). */
align(codes) {
this.ensure(5 + codes.length);
this.buf[this.n++] = OP_ALIGN;
this.writeU32(codes.length);
for (let i = 0; i < codes.length; i++)
this.buf[this.n++] = codes[i] & 255;
}
keepChildren() {
this.ensure(1);
this.buf[this.n++] = OP_KEEP_CHILDREN;
}
}
/** Emit one MDX JSX attribute as an OP_PROP, mirroring `encode_js_jsx_attrs`:
* null→boolean, string→literal, `{ value }` object→expression, else→boolean. */
export function emitMdxAttr(w, a) {
if (a.type === "mdxJsxExpressionAttribute") {
w.prop("", MDX_ATTR_SPREAD, typeof a.value === "string" ? a.value : "");
return;
}
const name = typeof a.name === "string" ? a.name : "";
const val = a.value;
if (typeof val === "string") {
w.prop(name, MDX_ATTR_LITERAL_PROP, val);
}
else if (val !== null && typeof val === "object" && !Array.isArray(val)) {
const expr = val.value;
w.prop(name, MDX_ATTR_EXPRESSION_PROP, typeof expr === "string" ? expr : "");
}
else {
w.prop(name, MDX_ATTR_BOOLEAN_PROP, "");
}
}
/** Restore phantom-space sentinels to real spaces, allocating only when present. */
export declare function restorePhantomSpaces(value: string): string;
/**
* Sentinel the MDX parser substitutes for spaces inside expression and JSX
* attribute-expression values (see `PHANTOM_SPACE` in
* satteri-pulldown-cmark/src/mdx.rs). mdast and hast keep the sentinel in the
* stored value; readers and visitors restore the real space on the way out.
* Both plugin-facing paths (direct walk match and materialized child) must
* restore it so a node's `value` reads the same regardless of how it's reached.
*/
const PHANTOM_SPACE = "\uF002";
/** Restore phantom-space sentinels to real spaces, allocating only when present. */
export function restorePhantomSpaces(value) {
return value.includes(PHANTOM_SPACE) ? value.replaceAll(PHANTOM_SPACE, " ") : value;
}
/**
* Cold/shape-stable helpers shared by the mdast and hast visitors. The
* per-node hot decoders stay duplicated in each visitor on purpose: sharing
* them would feed differently-shaped arguments through one call site and turn
* it polymorphic.
*/
import type { CommandBuffer } from "./command-buffer.js";
export declare function asArray<T>(value: T | T[]): T[];
/** Thrown when declarative replacement content can't be compiled to the
* structural op-stream — an unsupported node type (e.g. a bare `root`/`doctype`
* handed in as content) or an out-of-range numeric field. The op-stream is the
* only structural encoding, so this is a hard error rather than a fallback. */
export declare function unencodableContentError(content: unknown): Error;
/**
* Arena id for a node passed to a context method, via a per-kind `nid` lookup
* (closure keeps each kind's call site monomorphic). Plugin-built nodes have
* no id; without this check the id would coerce to 0 in the command buffer
* and the mutation would silently target the document root.
*/
export declare function makeRequireNid<TNode>(nid: (node: TNode) => number | undefined): (node: TNode, method: string) => number;
/** Concatenate the return-value and context command buffers for one pass,
* resetting both for reuse. */
export declare function mergeAndReset(returnBuffer: CommandBuffer, ctx: {
getCommandBuffer(): CommandBuffer;
}): {
merged: Uint8Array;
hasMutations: boolean;
};
/**
* Cold/shape-stable helpers shared by the mdast and hast visitors. The
* per-node hot decoders stay duplicated in each visitor on purpose: sharing
* them would feed differently-shaped arguments through one call site and turn
* it polymorphic.
*/
const EMPTY_BYTES = new Uint8Array(0);
export function asArray(value) {
return Array.isArray(value) ? value : [value];
}
/** Thrown when declarative replacement content can't be compiled to the
* structural op-stream — an unsupported node type (e.g. a bare `root`/`doctype`
* handed in as content) or an out-of-range numeric field. The op-stream is the
* only structural encoding, so this is a hard error rather than a fallback. */
export function unencodableContentError(content) {
const type = content?.type;
return new Error(`satteri: cannot encode replacement content${typeof type === "string" ? ` of type "${type}"` : ""} ` +
"into the structural op-stream — unsupported node type or out-of-range numeric field.");
}
/**
* Arena id for a node passed to a context method, via a per-kind `nid` lookup
* (closure keeps each kind's call site monomorphic). Plugin-built nodes have
* no id; without this check the id would coerce to 0 in the command buffer
* and the mutation would silently target the document root.
*/
export function makeRequireNid(nid) {
return (node, method) => {
const id = nid(node);
if (id === undefined) {
throw new Error(`${method}: node has no arena id — it was built in JS, not read from this tree. ` +
`Pass plugin-built nodes as new content (e.g. the second argument of insertAfter).`);
}
return id;
};
}
/** Concatenate the return-value and context command buffers for one pass,
* resetting both for reuse. */
export function mergeAndReset(returnBuffer, ctx) {
const ctxCmdBuf = ctx.getCommandBuffer();
// The common case — no mutations this pass — allocates nothing.
if (returnBuffer.length === 0 && ctxCmdBuf.length === 0) {
return { merged: EMPTY_BYTES, hasMutations: false };
}
const ctxBuf = ctxCmdBuf.getBuffer();
const retBuf = returnBuffer.getBuffer();
const merged = new Uint8Array(retBuf.length + ctxBuf.length);
merged.set(retBuf, 0);
merged.set(ctxBuf, retBuf.length);
returnBuffer.reset();
ctxCmdBuf.reset();
return { merged, hasMutations: true };
}
/**
* Little-endian primitives for reading the walk/snapshot wire buffers. Shared by
* the hand-written decoders and the generated layout decoder so both interpret
* the bytes identically.
*/
import type { Position } from "unist";
/** Read a u16 (LE) at `off`. */
export declare function ru16(view: DataView, off: number): number;
/** Read a u32 (LE) at `off`. */
export declare function ru32(view: DataView, off: number): number;
/** Read a UTF-8 string of `len` bytes at `off`. Very short ASCII runs decode
* with a charCode loop — TextDecoder's per-call overhead dominates there. The
* threshold is lower than the encode-side `INLINE_STR_MAX`: decoding builds a
* JS string by concatenation, which costs more per char than storing bytes.
* Measured crossover ~8-12 bytes (Node 24: 8B 30 vs 57 ns, 16B 64 vs 57). */
export declare function rstr(buf: Uint8Array, off: number, len: number): string;
/**
* Decode the 24-byte position block ([startOffset, endOffset, startLine,
* startColumn, endLine, endColumn], u32 LE each) shared by the walk prefixes
* and the snapshot node structs. A zero start line is the sentinel for
* synthesized nodes with no source range (e.g. GFM autolink-literal nodes, or a
* plugin-built replacement spliced by the rebuild): unist lines are 1-based, so
* line 0 always means "no source position" — even when the rebuild has rebased
* the (otherwise zero) offset by the spliced subtree's source base. Surfaced as
* `undefined` so a node reads the same however it's reached.
*/
export declare function readPosition(view: DataView, off: number): Position | undefined;
/**
* Little-endian primitives for reading the walk/snapshot wire buffers. Shared by
* the hand-written decoders and the generated layout decoder so both interpret
* the bytes identically.
*/
const textDecoder = new TextDecoder("utf-8");
/** Read a u16 (LE) at `off`. */
export function ru16(view, off) {
return view.getUint16(off, true);
}
/** Read a u32 (LE) at `off`. */
export function ru32(view, off) {
return view.getUint32(off, true);
}
/** Read a UTF-8 string of `len` bytes at `off`. Very short ASCII runs decode
* with a charCode loop — TextDecoder's per-call overhead dominates there. The
* threshold is lower than the encode-side `INLINE_STR_MAX`: decoding builds a
* JS string by concatenation, which costs more per char than storing bytes.
* Measured crossover ~8-12 bytes (Node 24: 8B 30 vs 57 ns, 16B 64 vs 57). */
export function rstr(buf, off, len) {
if (len === 0)
return "";
if (len <= 8) {
let ascii = true;
for (let i = 0; i < len; i++) {
if (buf[off + i] > 127) {
ascii = false;
break;
}
}
if (ascii) {
let s = "";
for (let i = 0; i < len; i++)
s += String.fromCharCode(buf[off + i]);
return s;
}
}
return textDecoder.decode(buf.subarray(off, off + len));
}
/**
* Decode the 24-byte position block ([startOffset, endOffset, startLine,
* startColumn, endLine, endColumn], u32 LE each) shared by the walk prefixes
* and the snapshot node structs. A zero start line is the sentinel for
* synthesized nodes with no source range (e.g. GFM autolink-literal nodes, or a
* plugin-built replacement spliced by the rebuild): unist lines are 1-based, so
* line 0 always means "no source position" — even when the rebuild has rebased
* the (otherwise zero) offset by the spliced subtree's source base. Surfaced as
* `undefined` so a node reads the same however it's reached.
*/
export function readPosition(view, off) {
const startLine = ru32(view, off + 8);
if (startLine === 0)
return undefined;
const startOffset = ru32(view, off);
return {
start: { offset: startOffset, line: startLine, column: ru32(view, off + 12) },
end: { offset: ru32(view, off + 4), line: ru32(view, off + 16), column: ru32(view, off + 20) },
};
}
+23
-26

@@ -5,4 +5,6 @@ /**

* Simple mutations (remove, setProperty) are encoded as compact binary commands.
* Structural mutations (insert, replace) carry payloads that can be raw strings
* (for Rust to re-parse) or JSON-serialized node trees.
* Structural mutations (insert, replace, …) carry one of two payload kinds:
* compiled op-streams (`PAYLOAD_OPSTREAM`, replayed straight into the arena —
* see op-stream.ts) for declarative content, or raw markdown/HTML strings
* (re-parsed by Rust) for the `{raw}`/`{rawHtml}` escape hatches.
*

@@ -12,10 +14,10 @@ * All multi-byte integers are little-endian to match native x86/ARM layout and

*/
import { ByteWriter } from "./byte-writer.js";
import type { MdastNode } from "./types.js";
type ReturnClass = "no_change" | "raw_markdown" | "raw_html" | "structured_node";
export declare function classifyReturn(value: unknown): ReturnClass;
export declare class CommandBuffer {
private buffer;
private view;
private bytes;
private offset;
/** Structural commands that carry a subtree payload. Each has an `${op}Opstream`
* twin (declarative content) and a raw twin (`{raw}`/`{rawHtml}` escape hatch). */
export type StructuralOp = "replace" | "insertBefore" | "insertAfter" | "prependChild" | "appendChild" | "wrapNode";
export declare class CommandBuffer extends ByteWriter {
constructor();

@@ -55,26 +57,21 @@ removeNode(nodeId: number): void;

}): void;
/** Write a REPLACE command with a pre-serialized JSON payload. */
replaceRawJson(nodeId: number, json: string): void;
/** Replace a node's child list (Root-wrapped `json`) while keeping the node. */
setChildren(nodeId: number, json: string): void;
/** Write any structural command with a pre-serialized JSON payload. */
insertBeforeRawJson(nodeId: number, json: string): void;
insertAfterRawJson(nodeId: number, json: string): void;
prependChildRawJson(nodeId: number, json: string): void;
appendChildRawJson(nodeId: number, json: string): void;
wrapNodeRawJson(nodeId: number, json: string): void;
private writeRawJsonCommand;
/** Header (cmd + nodeId + payloadType) followed by a length-prefixed string payload. */
private writePayloadCommand;
replaceOpstream(nodeId: number, ops: Uint8Array): void;
/** Replace a node's child list (root-wrapped `ops`) while keeping the node. */
setChildrenOpstream(nodeId: number, ops: Uint8Array): void;
insertBeforeOpstream(nodeId: number, ops: Uint8Array): void;
insertAfterOpstream(nodeId: number, ops: Uint8Array): void;
prependChildOpstream(nodeId: number, ops: Uint8Array): void;
appendChildOpstream(nodeId: number, ops: Uint8Array): void;
wrapNodeOpstream(nodeId: number, ops: Uint8Array): void;
private writeOpstreamCommand;
/** Return a Uint8Array view of the written bytes (no copy). */
getBuffer(): Uint8Array;
/** Number of bytes written so far. */
get length(): number;
/** Reset the buffer for reuse, releasing the old ArrayBuffer. */
/** Reset for reuse, releasing the old buffer (handed-out views stay intact). */
reset(): void;
/** Raw structural content (`{raw}`/`{rawHtml}` escape hatches). Declarative
* nodes go through the `*Opstream` methods, not here. */
private writeStructuralCommand;
private ensureCapacity;
private grow;
private writeU8;
private writeU32;
private writeBytes;
}
export {};

@@ -5,4 +5,6 @@ /**

* Simple mutations (remove, setProperty) are encoded as compact binary commands.
* Structural mutations (insert, replace) carry payloads that can be raw strings
* (for Rust to re-parse) or JSON-serialized node trees.
* Structural mutations (insert, replace, …) carry one of two payload kinds:
* compiled op-streams (`PAYLOAD_OPSTREAM`, replayed straight into the arena —
* see op-stream.ts) for declarative content, or raw markdown/HTML strings
* (re-parsed by Rust) for the `{raw}`/`{rawHtml}` escape hatches.
*

@@ -12,23 +14,5 @@ * All multi-byte integers are little-endian to match native x86/ARM layout and

*/
// Command bytes (0x01–0x0F)
const CMD_REMOVE = 0x01;
const CMD_INSERT_BEFORE = 0x05;
const CMD_INSERT_AFTER = 0x06;
const CMD_PREPEND_CHILD = 0x07;
const CMD_APPEND_CHILD = 0x08;
const CMD_WRAP = 0x09;
const CMD_REPLACE = 0x0b;
const CMD_SET_PROPERTY = 0x0c;
const CMD_SET_CHILDREN = 0x0d;
// Payload types (0x10+, distinct range from commands)
const PAYLOAD_RAW_MARKDOWN = 0x10;
const PAYLOAD_RAW_HTML = 0x11;
const PAYLOAD_SERDE_JSON = 0x12;
// Value types for CMD_SET_PROPERTY (must match commands.rs PROP_* constants)
const PROP_STRING = 0;
const PROP_BOOL_TRUE = 1;
const PROP_BOOL_FALSE = 2;
const PROP_SPACE_SEP = 3;
const PROP_INT = 5;
const PROP_NULL = 6;
import { ByteWriter } from "./byte-writer.js";
import { PROP_STRING, PROP_BOOL_TRUE, PROP_BOOL_FALSE, PROP_SPACE_SEP, PROP_INT, PROP_NULL, } from "./op-stream.js";
import { CMD_REMOVE, CMD_INSERT_BEFORE, CMD_INSERT_AFTER, CMD_PREPEND_CHILD, CMD_APPEND_CHILD, CMD_WRAP, CMD_REPLACE, CMD_SET_PROPERTY, CMD_SET_CHILDREN, PAYLOAD_RAW_MARKDOWN, PAYLOAD_RAW_HTML, PAYLOAD_OPSTREAM, } from "./generated/wire-constants.js";
export function classifyReturn(value) {

@@ -47,17 +31,9 @@ if (value === undefined || value === null)

const INITIAL_SIZE = 4096;
const encoder = new TextEncoder();
const EMPTY_U8 = new Uint8Array(0);
export class CommandBuffer {
buffer;
view;
bytes;
offset = 0;
export class CommandBuffer extends ByteWriter {
constructor() {
this.buffer = new ArrayBuffer(INITIAL_SIZE);
this.view = new DataView(this.buffer);
this.bytes = new Uint8Array(this.buffer);
super(INITIAL_SIZE);
}
removeNode(nodeId) {
this.ensureCapacity(5);
this.writeU8(CMD_REMOVE);
this.ensure(5);
this.buf[this.n++] = CMD_REMOVE;
this.writeU32(nodeId);

@@ -67,38 +43,35 @@ }

setProperty(nodeId, key, value) {
const encodedName = encoder.encode(key);
let valueType;
let encodedValue;
let str;
if (value === null || value === undefined) {
valueType = PROP_NULL;
encodedValue = EMPTY_U8;
str = "";
}
else if (value === true) {
valueType = PROP_BOOL_TRUE;
encodedValue = EMPTY_U8;
str = "";
}
else if (value === false) {
valueType = PROP_BOOL_FALSE;
encodedValue = EMPTY_U8;
str = "";
}
else if (typeof value === "number") {
valueType = PROP_INT;
encodedValue = encoder.encode(String(value));
str = String(value);
}
else if (Array.isArray(value)) {
valueType = PROP_SPACE_SEP;
encodedValue = encoder.encode(value.join(" "));
str = value.join(" ");
}
else {
valueType = PROP_STRING;
encodedValue = encoder.encode(String(value));
str = String(value);
}
// 1(cmd) + 4(nodeId) + 1(valueType) + 4(nameLen) + name + 4(valueLen) + value
this.ensureCapacity(14 + encodedName.length + encodedValue.length);
this.writeU8(CMD_SET_PROPERTY);
// 1(cmd) + 4(nodeId) + 1(valueType); name and value are length-prefixed strings
this.ensure(6);
this.buf[this.n++] = CMD_SET_PROPERTY;
this.writeU32(nodeId);
this.writeU8(valueType);
this.writeU32(encodedName.length);
this.writeBytes(encodedName);
this.writeU32(encodedValue.length);
this.writeBytes(encodedValue);
this.buf[this.n++] = valueType;
this.utf8WithU32Len(key);
this.utf8WithU32Len(str);
}

@@ -123,106 +96,65 @@ insertBefore(nodeId, newNode) {

}
/** Write a REPLACE command with a pre-serialized JSON payload. */
replaceRawJson(nodeId, json) {
this.writeRawJsonCommand(CMD_REPLACE, nodeId, json);
/** Header (cmd + nodeId + payloadType) followed by a length-prefixed string payload. */
writePayloadCommand(cmd, nodeId, payloadType, s) {
this.ensure(6);
this.buf[this.n++] = cmd;
this.writeU32(nodeId);
this.buf[this.n++] = payloadType;
this.utf8WithU32Len(s);
}
/** Replace a node's child list (Root-wrapped `json`) while keeping the node. */
setChildren(nodeId, json) {
this.writeRawJsonCommand(CMD_SET_CHILDREN, nodeId, json);
replaceOpstream(nodeId, ops) {
this.writeOpstreamCommand(CMD_REPLACE, nodeId, ops);
}
/** Write any structural command with a pre-serialized JSON payload. */
insertBeforeRawJson(nodeId, json) {
this.writeRawJsonCommand(CMD_INSERT_BEFORE, nodeId, json);
/** Replace a node's child list (root-wrapped `ops`) while keeping the node. */
setChildrenOpstream(nodeId, ops) {
this.writeOpstreamCommand(CMD_SET_CHILDREN, nodeId, ops);
}
insertAfterRawJson(nodeId, json) {
this.writeRawJsonCommand(CMD_INSERT_AFTER, nodeId, json);
insertBeforeOpstream(nodeId, ops) {
this.writeOpstreamCommand(CMD_INSERT_BEFORE, nodeId, ops);
}
prependChildRawJson(nodeId, json) {
this.writeRawJsonCommand(CMD_PREPEND_CHILD, nodeId, json);
insertAfterOpstream(nodeId, ops) {
this.writeOpstreamCommand(CMD_INSERT_AFTER, nodeId, ops);
}
appendChildRawJson(nodeId, json) {
this.writeRawJsonCommand(CMD_APPEND_CHILD, nodeId, json);
prependChildOpstream(nodeId, ops) {
this.writeOpstreamCommand(CMD_PREPEND_CHILD, nodeId, ops);
}
wrapNodeRawJson(nodeId, json) {
this.writeRawJsonCommand(CMD_WRAP, nodeId, json);
appendChildOpstream(nodeId, ops) {
this.writeOpstreamCommand(CMD_APPEND_CHILD, nodeId, ops);
}
writeRawJsonCommand(cmd, nodeId, json) {
const encoded = encoder.encode(json);
this.ensureCapacity(10 + encoded.length);
this.writeU8(cmd);
wrapNodeOpstream(nodeId, ops) {
this.writeOpstreamCommand(CMD_WRAP, nodeId, ops);
}
writeOpstreamCommand(cmd, nodeId, ops) {
this.ensure(10 + ops.length);
this.buf[this.n++] = cmd;
this.writeU32(nodeId);
this.writeU8(PAYLOAD_SERDE_JSON);
this.writeU32(encoded.length);
this.writeBytes(encoded);
this.buf[this.n++] = PAYLOAD_OPSTREAM;
this.writeU32(ops.length);
this.buf.set(ops, this.n);
this.n += ops.length;
}
/** Return a Uint8Array view of the written bytes (no copy). */
getBuffer() {
return new Uint8Array(this.buffer, 0, this.offset);
return this.take();
}
/** Number of bytes written so far. */
get length() {
return this.offset;
}
/** Reset the buffer for reuse, releasing the old ArrayBuffer. */
/** Reset for reuse, releasing the old buffer (handed-out views stay intact). */
reset() {
this.buffer = new ArrayBuffer(INITIAL_SIZE);
this.view = new DataView(this.buffer);
this.bytes = new Uint8Array(this.buffer);
this.offset = 0;
if (this.n === 0)
return;
this.release();
}
/** Raw structural content (`{raw}`/`{rawHtml}` escape hatches). Declarative
* nodes go through the `*Opstream` methods, not here. */
writeStructuralCommand(cmd, nodeId, node) {
const v = node;
if (typeof v.raw === "string") {
const encoded = encoder.encode(v.raw);
this.ensureCapacity(10 + encoded.length); // 1(cmd) + 4(nodeId) + 1(payloadType) + 4(len) + payload
this.writeU8(cmd);
this.writeU32(nodeId);
this.writeU8(PAYLOAD_RAW_MARKDOWN);
this.writeU32(encoded.length);
this.writeBytes(encoded);
this.writePayloadCommand(cmd, nodeId, PAYLOAD_RAW_MARKDOWN, v.raw);
}
else if (typeof v.rawHtml === "string") {
const encoded = encoder.encode(v.rawHtml);
this.ensureCapacity(10 + encoded.length);
this.writeU8(cmd);
this.writeU32(nodeId);
this.writeU8(PAYLOAD_RAW_HTML);
this.writeU32(encoded.length);
this.writeBytes(encoded);
this.writePayloadCommand(cmd, nodeId, PAYLOAD_RAW_HTML, v.rawHtml);
}
else {
// Structured node, serialize as JSON
const json = JSON.stringify(node);
const encoded = encoder.encode(json);
this.ensureCapacity(10 + encoded.length);
this.writeU8(cmd);
this.writeU32(nodeId);
this.writeU8(PAYLOAD_SERDE_JSON);
this.writeU32(encoded.length);
this.writeBytes(encoded);
throw new Error("CommandBuffer: structural content must be {raw} or {rawHtml}");
}
}
ensureCapacity(needed) {
while (this.offset + needed > this.buffer.byteLength) {
this.grow();
}
}
grow() {
const newBuffer = new ArrayBuffer(this.buffer.byteLength * 2);
new Uint8Array(newBuffer).set(this.bytes);
this.buffer = newBuffer;
this.view = new DataView(this.buffer);
this.bytes = new Uint8Array(this.buffer);
}
writeU8(val) {
this.view.setUint8(this.offset, val);
this.offset += 1;
}
writeU32(val) {
this.view.setUint32(this.offset, val, true);
this.offset += 4;
}
writeBytes(data) {
this.bytes.set(data, this.offset);
this.offset += data.length;
}
}
import type { MdastPluginInput, HastPluginInput } from "./plugin.js";
import type { MdastNode, HastNode } from "./types.js";
import type { MdastNode, HastNode, Data } from "./types.js";
/** Configuration for static subtree collapsing during MDX compilation. */

@@ -203,2 +203,4 @@ export interface OptimizeStaticConfig {

frontmatter: Frontmatter | null;
/** Document-level data bag populated by plugins via `ctx.data`. Empty `{}` if untouched. */
data: Data;
}

@@ -211,2 +213,4 @@ /** Result of {@link mdxToJs}. */

frontmatter: Frontmatter | null;
/** Document-level data bag populated by plugins via `ctx.data`. Empty `{}` if untouched. */
data: Data;
}

@@ -213,0 +217,0 @@ type AnyFn = (...args: any[]) => unknown;

@@ -6,2 +6,3 @@ import { visitHastHandle, resolveSubscriptions } from "./hast/hast-visitor.js";

import { materializeMdastTree } from "./mdast/mdast-materializer.js";
import { markHandleMutated } from "./lazy-child-resolver.js";
import { HastReader } from "./hast/hast-reader.js";

@@ -78,2 +79,9 @@ import { materializeHastTree } from "./hast/hast-materializer.js";

}
/** Drop a handle after bumping its epoch: the arena dies with the handle, so a
* child stub retained past the pipeline must hit the designed retention error,
* not a cryptic RangeError from snapshotting the freed arena. */
function disposeHandle(handle) {
markHandleMutated(handle);
dropHandle(handle);
}
function warnDroppedTransforms(plugin, dropped, kind) {

@@ -85,3 +93,3 @@ const name = plugin.name ?? "<anonymous>";

}
function runMdastPluginsOnHandle(handle, plugins, fileURL) {
function runMdastPluginsOnHandle(handle, plugins, fileURL, data) {
// Each plugin runs once over the tree. A transform that passes a child

@@ -94,6 +102,7 @@ // through (returning it inside the replacement) keeps that child's identity,

const subs = resolveMdastSubscriptions(plugin);
const result = visitMdastHandle(handle, plugin, subs, () => getHandleSource(handle), fileURL);
const result = visitMdastHandle(handle, plugin, subs, () => getHandleSource(handle), fileURL, data);
const apply = (r) => {
if (!r.hasMutations)
return;
markHandleMutated(handle);
const dropped = applyCommandsToMdastHandle(handle, r.commandBuffer);

@@ -118,3 +127,3 @@ if (dropped)

}
function runHastPluginsOnHandle(handle, plugins, source, fileURL) {
function runHastPluginsOnHandle(handle, plugins, source, fileURL, data) {
if (plugins.length === 0)

@@ -129,3 +138,3 @@ return;

const subs = resolveSubscriptions(plugin);
const result = visitHastHandle(handle, plugin, subs, source, fileURL);
const result = visitHastHandle(handle, plugin, subs, source, fileURL, data);
const warnIfDropped = (dropped) => {

@@ -192,10 +201,11 @@ if (dropped)

const { features: nativeFeatures, convertOptions: nativeConvertOptions } = featuresToNative(features);
const result = createHastHandleFromMdast(source, mdastPlugins, false, fileURL, nativeFeatures, nativeConvertOptions);
const data = {};
const result = createHastHandleFromMdast(source, mdastPlugins, false, fileURL, nativeFeatures, nativeConvertOptions, data);
const renderAndDrop = (h, frontmatter) => {
try {
const html = renderHandle(h);
return { html, frontmatter };
return { html, frontmatter, data };
}
finally {
dropHandle(h);
disposeHandle(h);
}

@@ -206,6 +216,6 @@ };

try {
hastResult = runHastPluginsOnHandle(r.hastHandle, hastPlugins, source, fileURL);
hastResult = runHastPluginsOnHandle(r.hastHandle, hastPlugins, source, fileURL, data);
}
catch (err) {
dropHandle(r.hastHandle);
disposeHandle(r.hastHandle);
throw err;

@@ -215,3 +225,3 @@ }

return hastResult.then(() => renderAndDrop(r.hastHandle, r.frontmatter), (err) => {
dropHandle(r.hastHandle);
disposeHandle(r.hastHandle);
throw err;

@@ -230,10 +240,11 @@ });

const { features: nativeFeatures, convertOptions: nativeConvertOptions } = featuresToNative(features);
const result = createHastHandleFromMdast(source, mdastPlugins, true, fileURL, nativeFeatures, nativeConvertOptions);
const data = {};
const result = createHastHandleFromMdast(source, mdastPlugins, true, fileURL, nativeFeatures, nativeConvertOptions, data);
const compileAndDrop = (h, frontmatter) => {
try {
const code = compileHandle(h, mdxOptions);
return { code, frontmatter };
return { code, frontmatter, data };
}
finally {
dropHandle(h);
disposeHandle(h);
}

@@ -244,6 +255,6 @@ };

try {
hastResult = runHastPluginsOnHandle(r.hastHandle, hastPlugins, source, fileURL);
hastResult = runHastPluginsOnHandle(r.hastHandle, hastPlugins, source, fileURL, data);
}
catch (err) {
dropHandle(r.hastHandle);
disposeHandle(r.hastHandle);
throw err;

@@ -253,3 +264,3 @@ }

return hastResult.then(() => compileAndDrop(r.hastHandle, r.frontmatter), (err) => {
dropHandle(r.hastHandle);
disposeHandle(r.hastHandle);
throw err;

@@ -295,7 +306,7 @@ });

// eslint-disable-next-line @typescript-eslint/no-explicit-any
nativeConvertOptions) {
nativeConvertOptions, data) {
const mdastHandle = mdx
? createMdxMdastHandle(source, nativeFeatures)
: createMdastHandle(source, nativeFeatures);
// finally{drop} is intentional: convertMdastToHastHandle empties the arena
// finally{dispose} is intentional: convertMdastToHastHandle empties the arena
// on success, but if any step here throws the handle would otherwise leak.

@@ -305,2 +316,5 @@ const finalize = (r) => {

const frontmatter = readFrontmatter(r.handle);
// The conversion empties the mdast arena; bump the epoch so a stub
// retained past the mdast pass fails with the retention error.
markHandleMutated(r.handle);
const hastHandle = convertMdastToHastHandle(r.handle, nativeConvertOptions);

@@ -310,3 +324,3 @@ return { hastHandle, frontmatter };

finally {
dropHandle(r.handle);
disposeHandle(r.handle);
}

@@ -318,6 +332,6 @@ };

}
const mdastResult = runMdastPluginsOnHandle(mdastHandle, mdastPlugins, fileURL);
const mdastResult = runMdastPluginsOnHandle(mdastHandle, mdastPlugins, fileURL, data);
if (mdastResult instanceof Promise) {
return mdastResult.then(finalize, (err) => {
dropHandle(mdastHandle);
disposeHandle(mdastHandle);
throw err;

@@ -329,3 +343,3 @@ });

catch (err) {
dropHandle(mdastHandle);
disposeHandle(mdastHandle);
throw err;

@@ -342,3 +356,3 @@ }

finally {
dropHandle(handle);
disposeHandle(handle);
}

@@ -353,3 +367,3 @@ }

finally {
dropHandle(handle);
disposeHandle(handle);
}

@@ -365,3 +379,3 @@ }

finally {
dropHandle(handle);
disposeHandle(handle);
}

@@ -377,4 +391,4 @@ }

finally {
dropHandle(handle);
disposeHandle(handle);
}
}
import type { BlockContent, Data as MdastData, DefinitionContent, Parent as MdastParent, PhrasingContent } from "mdast";
export type DirectiveAttributes = Record<string, string>;
export type DirectiveAttributes = Record<string, string | null | undefined>;
export interface ContainerDirective extends MdastParent {

@@ -11,3 +11,2 @@ type: "containerDirective";

export interface ContainerDirectiveData extends MdastData {
directiveLabel?: boolean | undefined;
}

@@ -37,2 +36,5 @@ export interface LeafDirective extends MdastParent {

}
interface ParagraphData {
directiveLabel?: boolean | null | undefined;
}
interface PhrasingContentMap {

@@ -39,0 +41,0 @@ textDirective: TextDirective;

import { HastReader } from "./hast-reader.js";
import type { Root } from "hast";
import type { HastNode } from "../types.js";

@@ -11,2 +12,2 @@ export type { HastNode };

*/
export declare function materializeHastTree(reader: HastReader): HastNode;
export declare function materializeHastTree(reader: HastReader): Root;
import { HastReader, HAST_ROOT, HAST_ELEMENT, HAST_TEXT, HAST_COMMENT, HAST_DOCTYPE, HAST_RAW, HAST_MDX_JSX_ELEMENT, HAST_MDX_JSX_TEXT_ELEMENT, HAST_MDX_FLOW_EXPRESSION, HAST_MDX_TEXT_EXPRESSION, HAST_MDX_ESM, } from "./hast-reader.js";
import { TYPE_NAMES } from "./generated/node-types.js";
import { lazyProp, lazyGroup } from "../lazy-props.js";
import { restorePhantomSpaces } from "../phantom.js";
function propsToRecord(props) {

@@ -15,43 +17,9 @@ const result = {};

const nodeType = reader.getNodeType(nodeId);
let typeName;
switch (nodeType) {
case HAST_ROOT:
typeName = "root";
break;
case HAST_ELEMENT:
typeName = "element";
break;
case HAST_TEXT:
typeName = "text";
break;
case HAST_COMMENT:
typeName = "comment";
break;
case HAST_DOCTYPE:
typeName = "doctype";
break;
case HAST_RAW:
typeName = "raw";
break;
case HAST_MDX_JSX_ELEMENT:
typeName = "mdxJsxFlowElement";
break;
case HAST_MDX_JSX_TEXT_ELEMENT:
typeName = "mdxJsxTextElement";
break;
case HAST_MDX_FLOW_EXPRESSION:
typeName = "mdxFlowExpression";
break;
case HAST_MDX_TEXT_EXPRESSION:
typeName = "mdxTextExpression";
break;
case HAST_MDX_ESM:
typeName = "mdxjsEsm";
break;
default:
typeName = `unknown(${nodeType})`;
break;
const typeName = TYPE_NAMES[nodeType] ?? `unknown(${nodeType})`;
const node = { type: typeName };
// Position decodes to three objects; defer it — passthrough children (e.g.
// replaceNode keeping `node.children`) never read it.
if (reader.hasPosition(nodeId)) {
Object.defineProperty(node, "position", lazyProp("position", () => reader.getPosition(nodeId)));
}
const position = reader.getPosition(nodeId);
const node = (position ? { type: typeName, position } : { type: typeName });
// _nodeId: non-enumerable internal reference

@@ -139,7 +107,3 @@ Object.defineProperty(node, "_nodeId", {

Object.defineProperties(node, {
// Substitute U+F002 phantom-space sentinels (see PHANTOM_SPACE in
// satteri-pulldown-cmark/src/mdx.rs) back to regular spaces — mdast
// and hast keep phantoms in the value field; only the compile path
// drops them.
value: lazyProp("value", () => reader.getTextValue(nodeId).replaceAll("", " ")),
value: lazyProp("value", () => restorePhantomSpaces(reader.getTextValue(nodeId))),
});

@@ -146,0 +110,0 @@ break;

import type { BufferHeader } from "../types.js";
import type { MdxJsxAttribute, MdxJsxExpressionAttribute } from "../mdx-types.js";
import type { Position } from "unist";
export type { MdxJsxAttribute, MdxJsxExpressionAttribute };
export declare const HAST_ROOT = 0;
export declare const HAST_ELEMENT = 1;
export declare const HAST_TEXT = 2;
export declare const HAST_COMMENT = 3;
export declare const HAST_DOCTYPE = 4;
export declare const HAST_RAW = 5;
export declare const HAST_MDX_JSX_ELEMENT = 10;
export declare const HAST_MDX_JSX_TEXT_ELEMENT = 11;
export declare const HAST_MDX_FLOW_EXPRESSION = 12;
export declare const HAST_MDX_ESM = 13;
export declare const HAST_MDX_TEXT_EXPRESSION = 14;
export declare const HAST_ROOT: number;
export declare const HAST_ELEMENT: number;
export declare const HAST_TEXT: number;
export declare const HAST_COMMENT: number;
export declare const HAST_DOCTYPE: number;
export declare const HAST_RAW: number;
export declare const HAST_MDX_JSX_ELEMENT: number;
export declare const HAST_MDX_JSX_TEXT_ELEMENT: number;
export declare const HAST_MDX_FLOW_EXPRESSION: number;
export declare const HAST_MDX_ESM: number;
export declare const HAST_MDX_TEXT_EXPRESSION: number;
export interface HastProperty {

@@ -36,16 +37,10 @@ name: string;

/** Get position data for a node. */
getPosition(nodeId: number): {
start: {
offset: number;
line: number;
column: number;
};
end: {
offset: number;
line: number;
column: number;
};
} | undefined;
getPosition(nodeId: number): Position | undefined;
/** Whether the node carries a source position (line 0 + offset 0 is the
* synthesized-node sentinel), without decoding the three position objects. */
hasPosition(nodeId: number): boolean;
/** Get the node_type byte for a given node ID. */
getNodeType(nodeId: number): number;
/** Get the parent id for a given node (0xffffffff at the root). */
getParentId(nodeId: number): number;
/** Get child node IDs for a given node. */

@@ -52,0 +47,0 @@ getChildIds(nodeId: number): number[];

@@ -1,51 +0,18 @@

// HAST node type constants (must match node_types.rs)
export const HAST_ROOT = 0;
export const HAST_ELEMENT = 1;
export const HAST_TEXT = 2;
export const HAST_COMMENT = 3;
export const HAST_DOCTYPE = 4;
export const HAST_RAW = 5;
// MDX-specific HAST node types
export const HAST_MDX_JSX_ELEMENT = 10;
export const HAST_MDX_JSX_TEXT_ELEMENT = 11;
export const HAST_MDX_FLOW_EXPRESSION = 12;
export const HAST_MDX_ESM = 13;
export const HAST_MDX_TEXT_EXPRESSION = 14;
const PROP_STRING = 0;
const PROP_BOOL_TRUE = 1;
const PROP_BOOL_FALSE = 2;
const PROP_SPACE_SEP = 3;
const PROP_COMMA_SEP = 4;
// MDX JSX attribute kinds (must match node_types.rs)
const MDX_ATTR_BOOLEAN_PROP = 0;
const MDX_ATTR_LITERAL_PROP = 1;
const MDX_ATTR_EXPRESSION_PROP = 2;
const MDX_ATTR_SPREAD = 3;
// HastNode field offsets (same layout as MDAST, shared binary format)
// id: u32 @ 0
// node_type: u8 @ 4
// _pad: [u8; 3] @ 5
// parent: u32 @ 8
// ...
// children_start: u32 @ 36
// children_count: u32 @ 40
// data_offset: u32 @ 44
// data_len: u32 @ 48
// Total: 52 bytes
const FIELD = {
node_type: 4,
start_offset: 12,
end_offset: 16,
start_line: 20,
start_column: 24,
end_line: 28,
end_column: 32,
children_start: 36,
children_count: 40,
data_offset: 44,
data_len: 48,
};
// "MDAR" bytes: 4d 44 41 52; read as LE u32 = 0x5241444d
const MAGIC = 0x5241444d;
const KIND_HAST = 2;
import { restorePhantomSpaces } from "../phantom.js";
import { readPosition } from "../wire-read.js";
import { MDX_ATTR_BOOLEAN_PROP, MDX_ATTR_LITERAL_PROP, MDX_ATTR_EXPRESSION_PROP, MDX_ATTR_SPREAD, } from "../op-stream.js";
import { decodeElementProp } from "./element-props.js";
import { NAME_TO_TYPE } from "./generated/node-types.js";
import { ARENA_MAGIC, KIND_HAST, FIELD, HEADER } from "../generated/arena-layout.js";
export const HAST_ROOT = NAME_TO_TYPE.root;
export const HAST_ELEMENT = NAME_TO_TYPE.element;
export const HAST_TEXT = NAME_TO_TYPE.text;
export const HAST_COMMENT = NAME_TO_TYPE.comment;
export const HAST_DOCTYPE = NAME_TO_TYPE.doctype;
export const HAST_RAW = NAME_TO_TYPE.raw;
export const HAST_MDX_JSX_ELEMENT = NAME_TO_TYPE.mdxJsxFlowElement;
export const HAST_MDX_JSX_TEXT_ELEMENT = NAME_TO_TYPE.mdxJsxTextElement;
export const HAST_MDX_FLOW_EXPRESSION = NAME_TO_TYPE.mdxFlowExpression;
export const HAST_MDX_ESM = NAME_TO_TYPE.mdxjsEsm;
export const HAST_MDX_TEXT_EXPRESSION = NAME_TO_TYPE.mdxTextExpression;
export class HastReader {

@@ -68,7 +35,7 @@ #view;

const v = this.#view;
const magic = v.getUint32(0, true);
if (magic !== MAGIC) {
const magic = v.getUint32(HEADER.magic, true);
if (magic !== ARENA_MAGIC) {
throw new Error(`Invalid HAST buffer: bad magic 0x${magic.toString(16)}`);
}
const kind = v.getUint32(4, true);
const kind = v.getUint32(HEADER.kind, true);
if (kind !== KIND_HAST) {

@@ -79,13 +46,13 @@ throw new Error(`HastReader was handed a buffer of kind ${kind} (expected ${KIND_HAST}). ` +

return {
nodeStructSize: v.getUint32(8, true),
nodeCount: v.getUint32(12, true),
nodesOffset: v.getUint32(16, true),
childrenCount: v.getUint32(20, true),
childrenOffset: v.getUint32(24, true),
typeDataLen: v.getUint32(28, true),
typeDataOffset: v.getUint32(32, true),
sourceLen: v.getUint32(36, true),
sourceOffset: v.getUint32(40, true),
nodeDataCount: v.getUint32(44, true),
nodeDataOffset: v.getUint32(48, true),
nodeStructSize: v.getUint32(HEADER.node_struct_size, true),
nodeCount: v.getUint32(HEADER.node_count, true),
nodesOffset: v.getUint32(HEADER.nodes_offset, true),
childrenCount: v.getUint32(HEADER.children_count, true),
childrenOffset: v.getUint32(HEADER.children_offset, true),
typeDataLen: v.getUint32(HEADER.type_data_len, true),
typeDataOffset: v.getUint32(HEADER.type_data_offset, true),
sourceLen: v.getUint32(HEADER.source_len, true),
sourceOffset: v.getUint32(HEADER.source_offset, true),
nodeDataCount: v.getUint32(HEADER.node_data_count, true),
nodeDataOffset: v.getUint32(HEADER.node_data_offset, true),
};

@@ -145,19 +112,11 @@ }

const base = this.#header.nodesOffset + nodeId * this.#header.nodeStructSize;
return readPosition(this.#view, base + FIELD.start_offset);
}
/** Whether the node carries a source position (line 0 + offset 0 is the
* synthesized-node sentinel), without decoding the three position objects. */
hasPosition(nodeId) {
const base = this.#header.nodesOffset + nodeId * this.#header.nodeStructSize;
const v = this.#view;
const startLine = v.getUint32(base + FIELD.start_line, true);
const startOffset = v.getUint32(base + FIELD.start_offset, true);
if (startLine === 0 && startOffset === 0)
return undefined;
return {
start: {
offset: startOffset,
line: startLine,
column: v.getUint32(base + FIELD.start_column, true),
},
end: {
offset: v.getUint32(base + FIELD.end_offset, true),
line: v.getUint32(base + FIELD.end_line, true),
column: v.getUint32(base + FIELD.end_column, true),
},
};
return (v.getUint32(base + FIELD.start_line, true) !== 0 ||
v.getUint32(base + FIELD.start_offset, true) !== 0);
}

@@ -169,2 +128,7 @@ /** Get the node_type byte for a given node ID. */

}
/** Get the parent id for a given node (0xffffffff at the root). */
getParentId(nodeId) {
const { nodesOffset, nodeStructSize } = this.#header;
return this.#view.getUint32(nodesOffset + nodeId * nodeStructSize + FIELD.parent, true);
}
/** Get child node IDs for a given node. */

@@ -240,35 +204,4 @@ getChildIds(nodeId) {

const valueRef = this.#readStringRef(data, base + 12);
switch (valueType) {
case PROP_BOOL_TRUE:
properties.push({ name, value: true });
break;
case PROP_BOOL_FALSE:
properties.push({ name, value: false });
break;
case PROP_STRING:
properties.push({ name, value: this.getString(valueRef.offset, valueRef.len) });
break;
case PROP_SPACE_SEP: {
const raw = this.getString(valueRef.offset, valueRef.len);
properties.push({ name, value: raw.split(" ").filter((s) => s.length > 0) });
break;
}
case PROP_COMMA_SEP: {
const raw = this.getString(valueRef.offset, valueRef.len);
properties.push({
name,
value: raw
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0),
});
break;
}
case 5: {
// PROP_INT
const raw = this.getString(valueRef.offset, valueRef.len);
properties.push({ name, value: Number(raw) });
break;
}
}
const valueStr = this.getString(valueRef.offset, valueRef.len);
properties.push({ name, value: decodeElementProp(valueType, valueStr) });
}

@@ -321,3 +254,3 @@ return { tagName, properties };

type: "mdxJsxAttributeValueExpression",
value: this.getString(attrValueRef.offset, attrValueRef.len).replaceAll("", " "),
value: restorePhantomSpaces(this.getString(attrValueRef.offset, attrValueRef.len)),
},

@@ -329,3 +262,3 @@ });

type: "mdxJsxExpressionAttribute",
value: this.getString(attrValueRef.offset, attrValueRef.len).replaceAll("", " "),
value: restorePhantomSpaces(this.getString(attrValueRef.offset, attrValueRef.len)),
});

@@ -332,0 +265,0 @@ break;

import { type HastNode } from "./hast-materializer.js";
import type { HastRaw } from "../types.js";
import type { Element, Text, Comment, Doctype } from "hast";
import type { HastRaw, Data } from "../types.js";
import type { Element, Text, Comment, Doctype, Parents as HastParents, Root as HastRoot } from "hast";
import type { Program } from "estree-jsx";

@@ -8,3 +8,4 @@ import type { MdxJsxFlowElementHast, MdxJsxTextElementHast } from "../mdx-types.js";

import type { MdxjsEsmHast } from "../mdx-types.js";
export type HastHandle = any;
import type { HastHandle } from "../handles.js";
export type { HastHandle };
/** ESTree-compatible Program node returned by `parseExpression()`. */

@@ -25,6 +26,14 @@ export type EstreeProgram = Program;

readonly fileURL: URL | undefined;
/**
* Document-level data bag, shared across every plugin in the compile and
* across the mdast→hast phase boundary. Mutate keys directly
* (`ctx.data.foo = x`); the bag itself isn't reassignable. Values are kept
* on the JS side, so any value is allowed, including functions and class
* instances. Returned to the caller as `result.data`.
*/
readonly data: Data;
removeNode(node: Readonly<HastNode>): void;
replaceNode(node: Readonly<HastNode>, newNode: HastNode): void;
insertBefore(node: Readonly<HastNode>, newNode: HastNode | HastNode[]): void;
insertAfter(node: Readonly<HastNode>, newNode: HastNode | HastNode[]): void;
replaceNode(node: Readonly<HastNode>, newNode: HastContent): void;
insertBefore(node: Readonly<HastNode>, newNode: HastContent | HastContent[]): void;
insertAfter(node: Readonly<HastNode>, newNode: HastContent | HastContent[]): void;
/**

@@ -35,7 +44,7 @@ * Wrap `node` in `parentNode`, making it `parentNode`'s first child. Any

*/
wrapNode(node: Readonly<HastNode>, parentNode: HastNode): void;
prependChild(node: Readonly<HastNode>, childNode: HastNode | HastNode[]): void;
appendChild(node: Readonly<HastNode>, childNode: HastNode | HastNode[]): void;
wrapNode(node: Readonly<HastNode>, parentNode: HastContent): void;
prependChild(node: Readonly<HastNode>, childNode: HastContent | HastContent[]): void;
appendChild(node: Readonly<HastNode>, childNode: HastContent | HastContent[]): void;
/** Insert one node or an array at `index`; clamps (`0` or less prepends, past the end appends). */
insertChildAt(node: Readonly<HastNode>, index: number, childNode: HastNode | HastNode[]): void;
insertChildAt(node: Readonly<HastNode>, index: number, childNode: HastContent | HastContent[]): void;
/** Remove the `index`-th child of `node`; a no-op when there is no such child. */

@@ -46,2 +55,14 @@ removeChildAt(node: Readonly<HastNode>, index: number): void;

textContent(node: Readonly<HastNode>): string;
/**
* The parent of a node, or `undefined` at the root. Within a pass the same
* parent is always the same object, so visitors on sibling nodes can dedupe
* by identity.
*/
parent<N extends Exclude<HastNode, HastRoot>>(node: Readonly<N>): Readonly<HastParents>;
parent(node: Readonly<HastNode>): Readonly<HastParents> | undefined;
/**
* Index of `node` within its parent's children, or `undefined` at the root.
* Use this rather than `parent.children.indexOf(node)`, which won't find it.
*/
indexOf(node: Readonly<HastNode>): number | undefined;
report(opts: {

@@ -54,2 +75,5 @@ message: string;

}
/** New content for a HAST structural mutation. Unlike [`MdastContent`], HAST has
* a `raw` node type, so it needs no raw/rawHtml escape hatch. */
export type HastContent = HastNode;
/** A filtered visitor: Rust filters by tag/component name, only matched nodes cross the boundary. */

@@ -84,3 +108,2 @@ export interface HastFilteredVisitor<N extends HastNode = HastNode> {

}
/** Resolve subscriptions from a plugin instance. */
export declare function resolveSubscriptions(plugin: HastVisitorInstance): ResolvedSubscription[];

@@ -95,3 +118,2 @@ /**

*/
export declare function visitHastHandle(handle: HastHandle, plugin: HastVisitorInstance, subs: ResolvedSubscription[], source: string | (() => string), fileURL: URL | undefined): number | Promise<number>;
export {};
export declare function visitHastHandle(handle: HastHandle, plugin: HastVisitorInstance, subs: ResolvedSubscription[], source: string | (() => string), fileURL: URL | undefined, data?: Data): number | Promise<number>;
import { materializeHastNode } from "./hast-materializer.js";
import { HastReader, HAST_ELEMENT, HAST_TEXT, HAST_COMMENT, HAST_RAW, HAST_MDX_JSX_ELEMENT, HAST_MDX_JSX_TEXT_ELEMENT, HAST_MDX_FLOW_EXPRESSION, HAST_MDX_TEXT_EXPRESSION, HAST_MDX_ESM, } from "./hast-reader.js";
import { TYPE_NAMES, NAME_TO_TYPE, VISITOR_KEYS, HAST_OPSTREAM_TYPES, } from "./generated/node-types.js";
import { CommandBuffer } from "../command-buffer.js";
import { walkHandle, applyCommandsToHandle, serializeHandle, textContentHandle, getNodeData as napiGetNodeData, parseExpression as napiParseExpression, parseEsm as napiParseEsm, } from "#binding";
import { OpWriter, OF_VALUE, OF_TAGNAME, OF_NAME, OF_EXPLICIT, PROP_STRING, PROP_BOOL_TRUE, PROP_BOOL_FALSE, PROP_SPACE_SEP, PROP_INT, emitMdxAttr, } from "../op-stream.js";
import { restorePhantomSpaces } from "../phantom.js";
import { decodeMdxJsxAttr } from "../mdx-attr.js";
import { decodeElementProp } from "./element-props.js";
import { readPosition, rstr } from "../wire-read.js";
import { walkHandle, applyCommandsToHandle, textContentHandle, parseExpression as napiParseExpression, parseEsm as napiParseEsm, } from "#binding";
import { asArray, makeRequireNid, mergeAndReset, unencodableContentError, } from "../visitor-shared.js";
import { LazyChildResolver, markHandleMutated } from "../lazy-child-resolver.js";
import { HastChildStub } from "./child-stub.js";
/** Maps HastNode objects to their arena node IDs without Object.defineProperty overhead. */

@@ -25,41 +34,158 @@ const nodeIdMap = new WeakMap();

/**
* Serialize a HastNode for the command buffer. Marks each node `_hast: true`,
* and — except at the root, which is the new replacement content — emits a
* reused (materialized) node as a `{ _ref: id }` placeholder so the rebuild
* splices the original in place (preserving its id, applying any pending patch
* on it) instead of rebuilding it fresh.
* Arena identity of a node, rejecting impostors — the one place the
* spread/identity invariant is enforced. A spread copy of a matched node or
* stub must read as NEW content: trusting a copied id would splice the
* original in as a ref and drop the copy's edits. Walk elements carry their
* id in a private field behind `instanceof` (spread copies fail the check);
* other walk-built nodes are keyed in the WeakMap (invisible to spread);
* `HastChildStub`s (enumerable `_id`, but that key is ignored on plain
* objects) are recognized by `instanceof`. Plain objects are trusted only via
* the WeakMap or a NON-enumerable `_nodeId` (the materializers' convention,
* which spread cannot copy).
*/
function markHast(node) {
return markHastNode(node, true);
function nid(node) {
if (node instanceof WalkElement)
return node._nid;
if (node instanceof HastChildStub)
return node._id;
const id = nodeIdMap.get(node);
if (id !== undefined)
return id;
const d = Object.getOwnPropertyDescriptor(node, "_nodeId");
return d !== undefined && !d.enumerable ? d.value : undefined;
}
function markHastNode(node, isRoot) {
const requireNid = makeRequireNid(nid);
function hastReusedId(node) {
if (node === null || typeof node !== "object")
return undefined;
const id = nid(node);
return typeof id === "number" ? id : undefined;
}
// Reused across replacements in a pass — see the note on `mdastWriter`.
const hastWriter = new OpWriter();
/** Compile a set-children payload: a root-wrapped child list, the shape
* `Patch::SetChildren` splices in. Reused children become refs. */
function compileHastChildrenToOpstream(children) {
if (!Array.isArray(children))
return null;
hastWriter.begin();
try {
hastWriter.open(NAME_TO_TYPE.root);
for (const c of children) {
if (!emitHastOp(hastWriter, c, false))
return null;
}
hastWriter.close();
return hastWriter.take();
}
finally {
hastWriter.end();
}
}
/** Encode `node` as the `op` structural command. HAST content is always a
* declarative node (no raw escape hatch), so it compiles to the op-stream or
* it's a hard error — the op-stream is the only structural encoding. The
* switch stays inline so the buffer calls are monomorphic (computed method
* names defeat inline caches on this warm path). */
function emitHastTree(buffer, op, id, node) {
const ops = compileHastToOpstream(node);
if (ops === null)
throw unencodableContentError(node);
switch (op) {
case "replace":
return buffer.replaceOpstream(id, ops);
case "insertBefore":
return buffer.insertBeforeOpstream(id, ops);
case "insertAfter":
return buffer.insertAfterOpstream(id, ops);
case "prependChild":
return buffer.prependChildOpstream(id, ops);
case "appendChild":
return buffer.appendChildOpstream(id, ops);
case "wrapNode":
return buffer.wrapNodeOpstream(id, ops);
}
}
/**
* Compile a declarative HAST replacement tree to the op-stream — the only
* structural encoding. Reused nodes become `ref`s (transparent passthrough).
* Returns null when the tree holds a type the replay can't reproduce
* identically; the caller throws.
*/
function compileHastToOpstream(root) {
hastWriter.begin();
try {
if (!emitHastOp(hastWriter, root, true))
return null;
return hastWriter.take();
}
finally {
hastWriter.end();
}
}
function emitHastOp(w, node, isRoot) {
if (node === null || typeof node !== "object")
return false;
if (!isRoot) {
const id = nodeIdMap.get(node) ?? node._nodeId;
if (typeof id === "number")
return { _ref: id };
const id = hastReusedId(node);
if (id !== undefined) {
w.ref(id);
return true;
}
}
const obj = { _hast: true, type: node.type };
if ("tagName" in node)
obj.tagName = node.tagName;
if ("properties" in node)
obj.properties = node.properties;
if ("value" in node)
obj.value = node.value;
if ("name" in node)
obj.name = node.name;
if ("attributes" in node)
obj.attributes = node.attributes;
if ("data" in node && node.data != null)
obj.data = node.data;
if ("children" in node) {
obj.children = node.children.map((c) => markHastNode(c, false));
const n = node;
const type = HAST_OPSTREAM_TYPES[n.type];
if (type === undefined)
return false;
w.open(type);
if (type === HAST_ELEMENT) {
w.str(OF_TAGNAME, typeof n.tagName === "string" ? n.tagName : "div");
const props = n.properties;
if (props !== null && typeof props === "object") {
for (const key in props) {
emitHastProp(w, key, props[key]);
}
}
}
return obj;
else if (type === HAST_MDX_JSX_ELEMENT || type === HAST_MDX_JSX_TEXT_ELEMENT) {
// Name falls back to tagName, matching `encode_hast_js_node_data`.
const name = typeof n.name === "string" ? n.name : typeof n.tagName === "string" ? n.tagName : "";
if (name !== "")
w.str(OF_NAME, name);
if (Array.isArray(n.attributes)) {
for (const a of n.attributes)
emitMdxAttr(w, a);
}
if (n.data?._mdxExplicitJsx === true) {
w.bool(OF_EXPLICIT, true);
}
}
else {
w.str(OF_VALUE, typeof n.value === "string" ? n.value : "");
}
if (n.data != null)
w.data(n.data);
const children = n.children;
if (Array.isArray(children)) {
for (const c of children)
if (!emitHastOp(w, c, false))
return false;
}
w.close();
return true;
}
function nid(node) {
return nodeIdMap.get(node) ?? node._nodeId;
/** Emit one element property, mirroring `encode_hast_js_node_data` exactly:
* bool/string/number/array → kind; null/object → skip. */
function emitHastProp(w, name, value) {
if (value === true)
w.prop(name, PROP_BOOL_TRUE, "");
else if (value === false)
w.prop(name, PROP_BOOL_FALSE, "");
else if (typeof value === "string")
w.prop(name, PROP_STRING, value);
else if (typeof value === "number")
w.prop(name, PROP_INT, String(value));
else if (Array.isArray(value))
w.prop(name, PROP_SPACE_SEP, value.filter((v) => typeof v === "string").join(" "));
}
function asArray(value) {
return Array.isArray(value) ? value : [value];
}
class HastVisitorContextImpl {

@@ -72,7 +198,14 @@ #commandBuffer = new CommandBuffer();

#getSource;
#resolver;
/** One canonical object per parent id, so visitors can dedupe by identity.
* Null until the first `parent()` call; most passes never make one. */
#parentsById = null;
fileURL;
constructor(handle, getSource, fileURL) {
data;
constructor(handle, getSource, fileURL, resolver, data) {
this.#handle = handle;
this.#getSource = getSource;
this.fileURL = fileURL;
this.#resolver = resolver;
this.data = data;
}

@@ -85,35 +218,33 @@ get source() {

removeNode(node) {
this.#commandBuffer.removeNode(nid(node));
this.#commandBuffer.removeNode(requireNid(node, "removeNode"));
}
replaceNode(node, newNode) {
const id = nid(node);
this.#commandBuffer.replaceRawJson(id, JSON.stringify(markHast(newNode)));
const id = requireNid(node, "replaceNode");
emitHastTree(this.#commandBuffer, "replace", id, newNode);
// Track the replacement so a later mdxJsx setProperty can fold into it.
this.#pendingNodes.set(id, newNode);
}
insertBefore(node, newNode) {
const id = nid(node);
for (const n of asArray(newNode)) {
this.#commandBuffer.insertBeforeRawJson(id, JSON.stringify(markHast(n)));
}
const id = requireNid(node, "insertBefore");
for (const n of asArray(newNode))
emitHastTree(this.#commandBuffer, "insertBefore", id, n);
}
insertAfter(node, newNode) {
const id = nid(node);
for (const n of asArray(newNode)) {
this.#commandBuffer.insertAfterRawJson(id, JSON.stringify(markHast(n)));
}
const id = requireNid(node, "insertAfter");
for (const n of asArray(newNode))
emitHastTree(this.#commandBuffer, "insertAfter", id, n);
}
wrapNode(node, parentNode) {
this.#commandBuffer.wrapNodeRawJson(nid(node), JSON.stringify(markHast(parentNode)));
const id = requireNid(node, "wrapNode");
emitHastTree(this.#commandBuffer, "wrapNode", id, parentNode);
}
prependChild(node, childNode) {
const id = nid(node);
for (const n of asArray(childNode)) {
this.#commandBuffer.prependChildRawJson(id, JSON.stringify(markHast(n)));
}
const id = requireNid(node, "prependChild");
for (const n of asArray(childNode))
emitHastTree(this.#commandBuffer, "prependChild", id, n);
}
appendChild(node, childNode) {
const id = nid(node);
for (const n of asArray(childNode)) {
this.#commandBuffer.appendChildRawJson(id, JSON.stringify(markHast(n)));
}
const id = requireNid(node, "appendChild");
for (const n of asArray(childNode))
emitHastTree(this.#commandBuffer, "appendChild", id, n);
}

@@ -138,12 +269,10 @@ insertChildAt(node, index, childNode) {

setProperty(node, key, value) {
const id = nid(node);
const id = requireNid(node, "setProperty");
if (key === "children") {
// children is structural: set-children keeps the node and swaps only its
// child list (reused children keep their id).
const wrapper = {
_hast: true,
type: "root",
children: value.map((child) => markHastNode(child, false)),
};
this.#commandBuffer.setChildren(id, JSON.stringify(wrapper));
const ops = compileHastChildrenToOpstream(value);
if (!ops)
throw unencodableContentError(value);
this.#commandBuffer.setChildrenOpstream(id, ops);
return;

@@ -156,3 +285,2 @@ }

if (node.type === "element") {
// Fast binary path, no materialization, no JSON serialization
this.#commandBuffer.setProperty(id, key, value);

@@ -162,27 +290,56 @@ return;

if (node.type === "mdxJsxFlowElement" || node.type === "mdxJsxTextElement") {
// MDX JSX nodes use `attributes`, not `properties`, keep replaceNode path
// MDX JSX nodes carry `attributes`, not `properties`. If a replacement is
// already queued for this node, fold the attribute into it so the change
// survives the rebuild. This spreads the queued replacement object, not
// the matched node, so it never forces the matched node's children to
// materialize.
const pending = this.#pendingNodes.get(id);
const current = (pending ?? node);
const updated = { ...current };
const attrs = [...(updated.attributes ?? [])];
const idx = attrs.findIndex((a) => a.type === "mdxJsxAttribute" && a.name === key);
if (idx !== -1)
attrs.splice(idx, 1);
const attrValue = value === true || value === null || value === undefined
? null
: typeof value === "string"
? value
: String(value);
attrs.push({ type: "mdxJsxAttribute", name: key, value: attrValue });
updated.attributes = attrs;
this.replaceNode(node, updated);
if (pending !== undefined) {
const updated = { ...pending };
const attrs = [...(updated.attributes ?? [])];
const idx = attrs.findIndex((a) => a.type === "mdxJsxAttribute" && a.name === key);
if (idx !== -1)
attrs.splice(idx, 1);
// Arrays space-join, matching the binary path's PROP_SPACE_SEP encoding
// (hast convention for list-valued properties like className).
const attrValue = value === true || value === null || value === undefined
? null
: typeof value === "string"
? value
: Array.isArray(value)
? value.join(" ")
: String(value);
attrs.push({ type: "mdxJsxAttribute", name: key, value: attrValue });
updated.attributes = attrs;
this.replaceNode(node, updated);
return;
}
// Binary attribute upsert in the arena's type_data — no child
// materialization. Rust maps the value-type to a boolean (true/null) or
// literal (string/number/false) attribute, mirroring the fold path above.
this.#commandBuffer.setProperty(id, key, value);
return;
}
// Text-like nodes (text, comment, raw, expressions, esm), fast binary path.
// Rust handles "value" setProperty directly on these types.
// Text-like nodes (text, comment, raw, expressions, esm): Rust handles
// `value` directly on these types.
this.#commandBuffer.setProperty(id, key, value);
}
textContent(node) {
return textContentHandle(this.#handle, nid(node));
return textContentHandle(this.#handle, requireNid(node, "textContent"));
}
parent(node) {
const parentId = this.#resolver.parentIdOf(requireNid(node, "parent"));
if (parentId === undefined)
return undefined;
const byId = (this.#parentsById ??= new Map());
let parent = byId.get(parentId);
if (parent === undefined) {
parent = this.#resolver.materializeOne(parentId);
byId.set(parentId, parent);
}
return parent;
}
indexOf(node) {
return this.#resolver.indexInParent(requireNid(node, "indexOf"));
}
report({ message, node, severity = "error", }) {

@@ -198,8 +355,4 @@ this.#diagnostics.push({ message, nodeId: node ? nid(node) : undefined, severity });

}
function isFilteredVisitor(v) {
return typeof v === "object" && v !== null && "filter" in v && "visit" in v;
}
/** Node types that use filtered visitors (have tag/component names). */
const FILTERED_METHODS = new Set(["element", "mdxJsxFlowElement", "mdxJsxTextElement"]);
/** Resolve subscriptions from a plugin instance. */
export function resolveSubscriptions(plugin) {

@@ -228,20 +381,4 @@ const subs = [];

}
/** Reverse map: method name → node type number */
const METHOD_TO_TYPE = {
element: HAST_ELEMENT,
text: HAST_TEXT,
comment: HAST_COMMENT,
raw: HAST_RAW,
doctype: 4, // HAST_DOCTYPE
mdxJsxFlowElement: HAST_MDX_JSX_ELEMENT,
mdxJsxTextElement: HAST_MDX_JSX_TEXT_ELEMENT,
mdxFlowExpression: HAST_MDX_FLOW_EXPRESSION,
mdxTextExpression: HAST_MDX_TEXT_EXPRESSION,
mdxjsEsm: HAST_MDX_ESM,
};
/**
* Selective walk path: Rust walks the tree, only sends matched nodes to JS.
*/
const textDecoder = new TextDecoder("utf-8");
/** Decode properties from the walk buffer at the given position. */
/** Visitor method name → node-type tag (method names are the subscribable AST names). */
const METHOD_TO_TYPE = Object.fromEntries([...VISITOR_KEYS].map((name) => [name, NAME_TO_TYPE[name]]));
function decodeProperties(view, buf, pos) {

@@ -254,3 +391,3 @@ const propCount = view.getUint16(pos, true);

pos += 2;
const name = textDecoder.decode(buf.subarray(pos, pos + nameLen));
const name = rstr(buf, pos, nameLen);
pos += nameLen;

@@ -261,72 +398,100 @@ const kind = buf[pos];

pos += 2;
const valStr = textDecoder.decode(buf.subarray(pos, pos + valLen));
const valStr = rstr(buf, pos, valLen);
pos += valLen;
switch (kind) {
case 0: // PROP_STRING
properties[name] = valStr;
break;
case 1: // PROP_BOOL_TRUE
properties[name] = true;
break;
case 2: // PROP_BOOL_FALSE
break;
case 3: // PROP_SPACE_SEP
properties[name] = valStr.split(" ").filter((s) => s.length > 0);
break;
case 5: // PROP_INT
properties[name] = Number(valStr);
break;
}
properties[name] = decodeElementProp(kind, valStr);
}
return properties;
}
/** Decode the 24-byte position block written by `serialize_hast_node_inline`. */
function readPositionPrefix(view, offset) {
const startOffset = view.getUint32(offset, true);
const endOffset = view.getUint32(offset + 4, true);
const startLine = view.getUint32(offset + 8, true);
const startColumn = view.getUint32(offset + 12, true);
const endLine = view.getUint32(offset + 16, true);
const endColumn = view.getUint32(offset + 20, true);
if (startLine === 0 && startOffset === 0)
return undefined;
return {
start: { offset: startOffset, line: startLine, column: startColumn },
end: { offset: endOffset, line: endLine, column: endColumn },
};
/** Build the child-stub list for a matched node from the wire's `[child_ids]
* [child_types]` blocks — no arena snapshot. The seal check still applies:
* post-pass ids are stale, and a stub built from them could later splice the
* wrong node as a ref. */
function readChildStubs(view, buf, idsPos, typesPos, count, resolver) {
resolver.assertUnsealed();
const stubs = new Array(count);
for (let i = 0; i < count; i++) {
stubs[i] = new HastChildStub(resolver, view.getUint32(idsPos + i * 4, true), buf[typesPos + i]);
}
return stubs;
}
// Shared own-getter descriptors for WalkElement's lazy fields, populated in
// its static block so the getters can read the private wire fields.
let WALK_PROPS_DESC;
let WALK_CHILDREN_DESC;
/**
* Walk-path element node: uses prototype getters instead of per-instance
* Object.defineProperty. V8 optimises shared hidden classes far better,
* this is ~16x faster for construction than the defineProperty approach.
*
* The buffer reference data is stored on private instance fields so the
* prototype getter can decode lazily on first access.
* Walk-path element. Spread-correctness requires `properties`/`children` as
* own enumerable keys (`{ ...node }` copies nothing else), but construction
* runs per matched element, so everything stays off the expensive paths:
* wire state in private fields (plain stores, invisible to spread — a WeakMap
* entry per element caused major-GC ephemeron stalls at this volume), shared
* getter functions instead of per-node closures, at most one define per lazy
* field, and `instanceof` gating identity so copies read as new content.
*/
class WalkElement {
type = "element";
/** @internal */ _view;
/** @internal */ _buf;
/** @internal */ _propsPos;
/** @internal */ _childIds;
/** @internal */ _resolver;
get properties() {
const val = decodeProperties(this._view, this._buf, this._propsPos);
Object.defineProperty(this, "properties", {
value: val,
writable: true,
tagName;
#nodeId;
#view;
#buf;
#propsPos;
#childIdsPos;
#childTypesPos;
#childCount;
#resolver;
constructor(tagName, nodeId, view, buf, propsPos, propCount, childIdsPos, childTypesPos, childCount, resolver) {
this.tagName = tagName;
this.#nodeId = nodeId;
this.#view = view;
this.#buf = buf;
this.#propsPos = propsPos;
this.#childIdsPos = childIdsPos;
this.#childTypesPos = childTypesPos;
this.#childCount = childCount;
this.#resolver = resolver;
if (propCount === 0) {
this.properties = {};
}
else {
Object.defineProperty(this, "properties", WALK_PROPS_DESC);
}
if (childCount === 0) {
this.children = [];
}
else {
Object.defineProperty(this, "children", WALK_CHILDREN_DESC);
}
}
/** @internal */
get _nid() {
return this.#nodeId;
}
static {
WALK_PROPS_DESC = {
enumerable: true,
configurable: true,
});
return val;
}
get children() {
const val = this._resolver.materializeChildren(this._childIds);
Object.defineProperty(this, "children", {
value: val,
writable: true,
get() {
const val = decodeProperties(this.#view, this.#buf, this.#propsPos);
Object.defineProperty(this, "properties", {
value: val,
writable: true,
enumerable: true,
configurable: true,
});
return val;
},
};
WALK_CHILDREN_DESC = {
enumerable: true,
configurable: true,
});
return val;
get() {
const val = readChildStubs(this.#view, this.#buf, this.#childIdsPos, this.#childTypesPos, this.#childCount, this.#resolver);
Object.defineProperty(this, "children", {
value: val,
writable: true,
enumerable: true,
configurable: true,
});
return val;
},
};
}

@@ -336,3 +501,3 @@ }

* Common prelude (data/position/children) is already consumed by `readMatchedNode`. */
function readElementFromBinary(view, buf, offset, nodeId, resolver, position, childIds, data) {
function readElementFromBinary(view, buf, offset, nodeId, resolver, position, childIdsPos, childTypesPos, childCount, data) {
let pos = offset;

@@ -342,8 +507,6 @@ // Eager: tagName (almost always accessed by visitors)

pos += 2;
const tagName = textDecoder.decode(buf.subarray(pos, pos + tagLen));
const tagName = rstr(buf, pos, tagLen);
pos += tagLen;
const propsPos = pos;
// Build node using class (prototype getters, no per-instance defineProperty)
const node = new WalkElement();
node.tagName = tagName;
const propCount = view.getUint16(pos, true);
const node = new WalkElement(tagName, nodeId, view, buf, pos, propCount, childIdsPos, childTypesPos, childCount, resolver);
if (position !== undefined)

@@ -353,22 +516,14 @@ node.position = position;

node.data = data;
node._view = view;
node._buf = buf;
node._propsPos = propsPos;
node._childIds = childIds;
node._resolver = resolver;
nodeIdMap.set(node, nodeId);
return node;
}
/** Read a text/comment/raw/expression node from the binary data section. */
const TEXT_NODE_TYPES = {
2: "text",
3: "comment",
5: "raw",
[HAST_MDX_FLOW_EXPRESSION]: "mdxFlowExpression",
[HAST_MDX_TEXT_EXPRESSION]: "mdxTextExpression",
[HAST_MDX_ESM]: "mdxjsEsm",
};
/** Value-carrying types read by `readTextFromBinary` (tag → AST name). */
const TEXT_NODE_TYPES = Object.fromEntries(["text", "comment", "raw", "mdxFlowExpression", "mdxTextExpression", "mdxjsEsm"].map((name) => [NAME_TO_TYPE[name], name]));
function readTextFromBinary(view, buf, offset, nodeId, nodeType, position, data) {
const valLen = view.getUint32(offset, true);
const value = textDecoder.decode(buf.subarray(offset + 4, offset + 4 + valLen));
const rawValue = rstr(buf, offset + 4, valLen);
// MDX flow/text expressions store phantom-space sentinels; restore them so
// the value matches the reader path. ESM and plain text keep their value.
const value = nodeType === HAST_MDX_FLOW_EXPRESSION || nodeType === HAST_MDX_TEXT_EXPRESSION
? restorePhantomSpaces(rawValue)
: rawValue;
const base = { type: TEXT_NODE_TYPES[nodeType], value };

@@ -389,9 +544,7 @@ if (position !== undefined)

}
/** Read an MDX JSX element from the binary data section. */
function readMdxJsxFromBinary(view, buf, offset, nodeId, nodeType, resolver, position, childIds, data) {
function readMdxJsxFromBinary(view, buf, offset, nodeId, nodeType, resolver, position, childIdsPos, childTypesPos, childCount, data) {
let pos = offset;
// Name
const nameLen = view.getUint16(pos, true);
pos += 2;
const name = nameLen > 0 ? textDecoder.decode(buf.subarray(pos, pos + nameLen)) : null;
const name = nameLen > 0 ? rstr(buf, pos, nameLen) : null;
pos += nameLen;

@@ -407,26 +560,9 @@ // Attributes: [kind: u8][nameLen: u16][name][valLen: u32][val]

pos += 2;
const attrName = textDecoder.decode(buf.subarray(pos, pos + attrNameLen));
const attrName = rstr(buf, pos, attrNameLen);
pos += attrNameLen;
const attrValLen = view.getUint32(pos, true);
pos += 4;
const attrVal = textDecoder.decode(buf.subarray(pos, pos + attrValLen));
const attrVal = rstr(buf, pos, attrValLen);
pos += attrValLen;
switch (kind) {
case 0: // BooleanProp
attributes.push({ type: "mdxJsxAttribute", name: attrName, value: null });
break;
case 1: // LiteralProp
attributes.push({ type: "mdxJsxAttribute", name: attrName, value: attrVal });
break;
case 2: // ExpressionProp
attributes.push({
type: "mdxJsxAttribute",
name: attrName,
value: { type: "mdxJsxAttributeValueExpression", value: attrVal },
});
break;
case 3: // Spread
attributes.push({ type: "mdxJsxExpressionAttribute", value: attrVal });
break;
}
attributes.push(decodeMdxJsxAttr(kind, attrName, attrVal));
}

@@ -440,3 +576,3 @@ const typeName = nodeType === HAST_MDX_JSX_ELEMENT ? "mdxJsxFlowElement" : "mdxJsxTextElement";

nodeIdMap.set(base, nodeId);
makeLazyChildren(base, childIds, resolver);
makeLazyChildren(base, view, buf, childIdsPos, childTypesPos, childCount, resolver);
return base;

@@ -447,4 +583,3 @@ }

// Shared prelude (matches serialize_hast_node_inline / serialize_mdast_node_inline):
// [data_len: u32][data_bytes][position: 24B][child_count: u16][child_ids: N×u32]
// Data (JSON), eagerly parsed
// [data_len: u32][data_bytes][position: 24B][child_count: u32][child_ids: N×u32][child_types: N×u8]
const dataLen = view.getUint32(pos, true);

@@ -454,23 +589,25 @@ pos += 4;

if (dataLen > 0) {
const jsonStr = textDecoder.decode(buf.subarray(pos, pos + dataLen));
const jsonStr = rstr(buf, pos, dataLen);
try {
data = JSON.parse(jsonStr);
}
catch {
/* ignore malformed JSON */
catch (err) {
if (process.env.NODE_ENV !== "production") {
console.warn(`readMatchedNode: malformed node_data for nodeId=${nodeId}`, err);
}
}
pos += dataLen;
}
const position = readPositionPrefix(view, pos);
const position = readPosition(view, pos);
pos += 24;
const childCount = view.getUint16(pos, true);
pos += 2;
const childIds = [];
for (let i = 0; i < childCount; i++) {
childIds.push(view.getUint32(pos, true));
pos += 4;
}
const childCount = view.getUint32(pos, true);
pos += 4;
// Ids/types decode lazily with `.children` — most matched nodes never read them.
const childIdsPos = pos;
pos += childCount * 4;
const childTypesPos = pos;
pos += childCount;
// Dispatch to type-specific tail (pos now sits at the type-specific section)
if (nodeType === HAST_ELEMENT) {
return readElementFromBinary(view, buf, pos, nodeId, resolver, position, childIds, data);
return readElementFromBinary(view, buf, pos, nodeId, resolver, position, childIdsPos, childTypesPos, childCount, data);
}

@@ -486,6 +623,6 @@ else if (nodeType === HAST_TEXT ||

else if (nodeType === HAST_MDX_JSX_ELEMENT || nodeType === HAST_MDX_JSX_TEXT_ELEMENT) {
return readMdxJsxFromBinary(view, buf, pos, nodeId, nodeType, resolver, position, childIds, data);
return readMdxJsxFromBinary(view, buf, pos, nodeId, nodeType, resolver, position, childIdsPos, childTypesPos, childCount, data);
}
// Fallback: minimal node carrying whatever prelude data we found
const base = { type: `unknown(${nodeType})` };
// Fallback (e.g. doctype): minimal node carrying whatever prelude data we found
const base = { type: TYPE_NAMES[nodeType] ?? `unknown(${nodeType})` };
if (position !== undefined)

@@ -499,54 +636,26 @@ base.position = position;

}
// Shared helpers
/**
* Lazy child materializer, serializes the handle's buffer once when first
* child is accessed, then materializes children from it via HastReader.
*/
class LazyChildResolver {
#handle;
#reader = null;
constructor(handle) {
this.#handle = handle;
class HastLazyChildResolver extends LazyChildResolver {
createReader(wire) {
return new HastReader(wire);
}
#ensure() {
if (!this.#reader) {
this.#reader = new HastReader(serializeHandle(this.#handle));
}
return this.#reader;
materializeNode(reader, nodeId) {
return materializeHastNode(reader, nodeId);
}
materializeChildren(childIds) {
const reader = this.#ensure();
return childIds.map((id) => {
const node = materializeHastNode(reader, id);
this.attachLazyData(node, id);
return node;
});
readParentId(reader, nodeId) {
return reader.getParentId(nodeId);
}
/** Attach a lazy `data` getter backed by the Rust arena's node_data. */
attachLazyData(node, nodeId) {
const handle = this.#handle;
Object.defineProperty(node, "data", {
get() {
const json = napiGetNodeData(handle, nodeId);
const val = json ? JSON.parse(json) : null;
Object.defineProperty(this, "data", {
value: val,
writable: true,
enumerable: true,
configurable: true,
});
return val;
},
configurable: true,
enumerable: true,
});
readChildIds(reader, nodeId) {
return reader.getChildIds(nodeId);
}
}
/** Create a lazy `children` property backed by the handle. */
function makeLazyChildren(node, childIds, resolver) {
/** Install `children` as an own enumerable getter (spread must carry it),
* self-replacing with the one stable stub array on first read. One closure
* and one define per node — installing the wire locals as hidden slots
* instead measurably regressed every matching pipeline. */
function makeLazyChildren(node, view, buf, childIdsPos, childTypesPos, childCount, resolver) {
Object.defineProperty(node, "children", {
get() {
const children = resolver.materializeChildren(childIds);
const val = readChildStubs(view, buf, childIdsPos, childTypesPos, childCount, resolver);
Object.defineProperty(this, "children", {
value: children,
value: val,
writable: true,

@@ -556,11 +665,10 @@ enumerable: true,

});
return children;
return val;
},
enumerable: true,
configurable: true,
enumerable: true,
});
}
/** Handle a visitor result (sync).
* If the result is the same object as the input node, treat it as a no-op
* so that context mutations (e.g. setProperty) are not clobbered. */
/** A result that is the same object as the input node is a no-op, so context
* mutations (e.g. setProperty) are not clobbered. */
function handleVisitResult(result, nodeId, returnBuffer, deferred, originalNode) {

@@ -576,3 +684,3 @@ if (result == null)

}
returnBuffer.replaceRawJson(nodeId, JSON.stringify(markHast(result)));
emitHastTree(returnBuffer, "replace", nodeId, result);
return deferred;

@@ -589,3 +697,3 @@ }

for (let i = 0; i < matchCount; i++) {
const indexBase = 4 + i * 12;
const indexBase = 4 + i * 10;
const nodeId = matchView.getUint32(indexBase, true);

@@ -601,22 +709,2 @@ const subIndex = matchBuf[indexBase + 4];

}
/** Merge return-value + context command buffers and release internals. */
function mergeAndReset(returnBuffer, ctx) {
const ctxCmdBuf = ctx.getCommandBuffer();
const ctxBuf = ctxCmdBuf.getBuffer();
const retBuf = returnBuffer.getBuffer();
const totalLen = retBuf.length + ctxBuf.length;
let merged;
if (totalLen === 0) {
merged = new Uint8Array(0);
}
else {
merged = new Uint8Array(totalLen);
merged.set(retBuf, 0);
merged.set(ctxBuf, retBuf.length);
}
returnBuffer.reset();
ctxCmdBuf.reset();
return { merged, hasMutations: totalLen > 0 };
}
// Handle-based visitor
/**

@@ -630,7 +718,7 @@ * Walk a handle's arena in Rust, dispatch matched nodes to JS visitor functions,

*/
export function visitHastHandle(handle, plugin, subs, source, fileURL) {
export function visitHastHandle(handle, plugin, subs, source, fileURL, data = {}) {
const getSource = typeof source === "function" ? source : () => source;
const ctx = new HastVisitorContextImpl(handle, getSource, fileURL);
const resolver = new HastLazyChildResolver(handle);
const ctx = new HastVisitorContextImpl(handle, getSource, fileURL, resolver, data);
const returnBuffer = new CommandBuffer();
const resolver = new LazyChildResolver(handle);
const rustSubs = subs.map((s) => ({ nodeType: s.nodeType, tagFilter: s.tagFilter }));

@@ -642,8 +730,12 @@ const deferred = dispatchMatches(walkHandle(handle, rustSubs), subs, ctx, returnBuffer, resolver);

if (result != null && result !== originalNode) {
returnBuffer.replaceRawJson(nodeId, JSON.stringify(markHast(result)));
emitHastTree(returnBuffer, "replace", nodeId, result);
}
}
// Mutations land next, renumbering the arena: snapshots taken after
// this point would resolve match-time child ids against wrong nodes.
resolver.seal();
return applyMutations(handle, returnBuffer, ctx);
});
}
resolver.seal();
return applyMutations(handle, returnBuffer, ctx);

@@ -655,2 +747,3 @@ }

if (hasMutations) {
markHandleMutated(handle);
return applyCommandsToHandle(handle, merged);

@@ -657,0 +750,0 @@ }

@@ -5,6 +5,6 @@ export { markdownToHtml, mdxToJs, evaluate, markdownToMdast, mdxToMdast, markdownToHast, mdxToHast, } from "./compile.js";

export type { MdastPluginDefinition, HastPluginDefinition, MdastPluginInput, HastPluginInput, } from "./plugin.js";
export type { HastVisitorInstance, HastVisitorContext, HastFilteredVisitor, EstreeProgram, } from "./hast/hast-visitor.js";
export type { MdastNode, HastNode, Position, Point, MdxJsxAttributeNode, MdxJsxExpressionAttributeNode, MdxJsxAttributeValueExpressionNode, MdxJsxAttributeUnion, MdxJsxFlowElement, MdxJsxTextElement, MdxFlowExpression, MdxTextExpression, MdxjsEsm, MdxJsxFlowElementHast, MdxJsxTextElementHast, MdxFlowExpressionHast, MdxTextExpressionHast, MdxjsEsmHast, } from "./types.js";
export type { HastVisitorInstance, HastVisitorContext, HastFilteredVisitor, HastContent, EstreeProgram, } from "./hast/hast-visitor.js";
export type { MdastNode, HastNode, DataMap, Data, Position, Point, MdxJsxAttributeNode, MdxJsxExpressionAttributeNode, MdxJsxAttributeValueExpressionNode, MdxJsxAttributeUnion, MdxJsxFlowElement, MdxJsxTextElement, MdxFlowExpression, MdxTextExpression, MdxjsEsm, MdxJsxFlowElementHast, MdxJsxTextElementHast, MdxFlowExpressionHast, MdxTextExpressionHast, MdxjsEsmHast, } from "./types.js";
export { visitMdastHandle, resolveMdastSubscriptions } from "./mdast/mdast-visitor.js";
export type { MdastPluginInstance } from "./mdast/mdast-visitor.js";
export type { MdastPluginInstance, MdastContent } from "./mdast/mdast-visitor.js";
export { visitHastHandle, resolveSubscriptions as resolveHastSubscriptions, } from "./hast/hast-visitor.js";

@@ -15,2 +15,8 @@ export { MdastReader } from "./mdast/mdast-reader.js";

export { materializeHastTree } from "./hast/hast-materializer.js";
export { createMdastHandle, createMdxMdastHandle, createHastHandle, createMdxHastHandle, convertMdastToHastHandle, serializeHandle, renderHandle, compileHandle, dropHandle, applyCommandsToMdastHandle, applyCommandsAndConvertToHastHandle, getHandleSource, } from "#binding";
export { createMdastHandle, createMdxMdastHandle, createHastHandle, createMdxHastHandle, serializeHandle, renderHandle, compileHandle, getHandleSource, } from "#binding";
import { applyCommandsAndConvertToHastHandle as napiApplyCommandsAndConvertToHastHandle, convertMdastToHastHandle as napiConvertMdastToHastHandle } from "#binding";
import type { AnyHandle } from "./handles.js";
export declare function applyCommandsToMdastHandle(handle: MdastHandle, commandBuf: Uint8Array): number;
export declare function convertMdastToHastHandle(handle: MdastHandle, convertOptions?: Parameters<typeof napiConvertMdastToHastHandle>[1]): HastHandle;
export declare function dropHandle(handle: AnyHandle): void;
export declare function applyCommandsAndConvertToHastHandle(handle: MdastHandle, commandBuf: Uint8Array, convertOptions?: Parameters<typeof napiApplyCommandsAndConvertToHastHandle>[2]): HastHandle;

@@ -13,2 +13,24 @@ // Public API: compile functions

export { materializeHastTree } from "./hast/hast-materializer.js";
export { createMdastHandle, createMdxMdastHandle, createHastHandle, createMdxHastHandle, convertMdastToHastHandle, serializeHandle, renderHandle, compileHandle, dropHandle, applyCommandsToMdastHandle, applyCommandsAndConvertToHastHandle, getHandleSource, } from "#binding";
export { createMdastHandle, createMdxMdastHandle, createHastHandle, createMdxHastHandle, serializeHandle, renderHandle, compileHandle, getHandleSource, } from "#binding";
import { applyCommandsToMdastHandle as napiApplyCommandsToMdastHandle, applyCommandsAndConvertToHastHandle as napiApplyCommandsAndConvertToHastHandle, convertMdastToHastHandle as napiConvertMdastToHastHandle, dropHandle as napiDropHandle, } from "#binding";
import { markHandleMutated } from "./lazy-child-resolver.js";
// The raw NAPI mutators renumber or empty the arena; without the epoch bump a
// child stub retained past a manual-pipeline pass would silently snapshot the
// changed arena (or die with an opaque RangeError) instead of hitting the
// retention error.
export function applyCommandsToMdastHandle(handle, commandBuf) {
markHandleMutated(handle);
return napiApplyCommandsToMdastHandle(handle, commandBuf);
}
export function convertMdastToHastHandle(handle, convertOptions) {
markHandleMutated(handle);
return napiConvertMdastToHastHandle(handle, convertOptions);
}
export function dropHandle(handle) {
markHandleMutated(handle);
napiDropHandle(handle);
}
export function applyCommandsAndConvertToHastHandle(handle, commandBuf, convertOptions) {
markHandleMutated(handle);
return napiApplyCommandsAndConvertToHastHandle(handle, commandBuf, convertOptions);
}

@@ -10,2 +10,2 @@ /**

*/
export declare function lazyGroup(node: object, keys: string[], resolve: () => Record<string, unknown>): void;
export declare function lazyGroup(node: object, keys: readonly string[], resolve: () => Record<string, unknown>): void;

@@ -0,4 +1,5 @@

import type { Root } from "mdast";
import type { MdastNode } from "../types.js";
import type { MdastReader } from "./mdast-reader.js";
export declare const TYPE_NAMES: Record<number, string>;
export declare const LEAF_TYPES: ReadonlySet<number>;
/**

@@ -9,2 +10,2 @@ * Materialize a single MDAST node from a binary buffer as a lazy JS object.

/** Materialize the full tree from root (nodeId=0). */
export declare function materializeMdastTree(reader: MdastReader): MdastNode;
export declare function materializeMdastTree(reader: MdastReader): Root;
import { lazyProp, lazyGroup } from "../lazy-props.js";
export const TYPE_NAMES = {
0: "root",
1: "paragraph",
2: "heading",
3: "thematicBreak",
4: "blockquote",
5: "list",
6: "listItem",
7: "html",
8: "code",
9: "definition",
10: "text",
11: "emphasis",
12: "strong",
13: "inlineCode",
14: "break",
15: "link",
16: "image",
17: "linkReference",
18: "imageReference",
19: "footnoteDefinition",
20: "footnoteReference",
21: "table",
22: "tableRow",
23: "tableCell",
24: "delete",
25: "yaml",
26: "toml",
27: "math",
28: "inlineMath",
30: "containerDirective",
31: "leafDirective",
32: "textDirective",
100: "mdxJsxFlowElement",
101: "mdxJsxTextElement",
102: "mdxFlowExpression",
103: "mdxTextExpression",
104: "mdxjsEsm",
};
import { TYPE_NAMES } from "./generated/node-types.js";
import { materializeMdastFields } from "./generated/layout.js";
// Leaf node types that do NOT have children.
// Type 9 = `definition`; type 18 = `imageReference` — leaves per mdast spec
// (imageReference carries `alt` as a string, not children).
const LEAF_TYPES = new Set([9, 10, 13, 7, 8, 14, 3, 16, 18, 20, 25, 26, 27, 28, 102, 103, 104]);
export const LEAF_TYPES = new Set([
9, 10, 13, 7, 8, 14, 3, 16, 18, 20, 25, 26, 27, 28, 102, 103, 104,
]);
/**

@@ -49,37 +14,7 @@ * Add type-specific lazy properties to a node object.

function addTypeProperties(node, reader, nodeId, nodeType) {
// Fixed-field types materialize from the generated layout table; the rest
// (variable-length / cross-field) stay in the hand-written switch.
if (materializeMdastFields(reader, node, nodeId, nodeType))
return;
switch (nodeType) {
case 2: // heading
Object.defineProperties(node, {
depth: lazyProp("depth", () => reader.getHeadingDepth(nodeId)),
});
break;
case 10: // text
case 13: // inlineCode
case 7: // html
case 25: // yaml
case 26: // toml
Object.defineProperties(node, {
value: lazyProp("value", () => reader.getTextValue(nodeId)),
});
break;
case 8: // code
lazyGroup(node, ["lang", "meta", "value"], () => reader.getCodeData(nodeId));
break;
case 27: // math
lazyGroup(node, ["meta", "value"], () => reader.getMathData(nodeId));
break;
case 28: // inlineMath
Object.defineProperties(node, {
value: lazyProp("value", () => reader.getMathData(nodeId).value),
});
break;
case 15: // link
lazyGroup(node, ["url", "title"], () => reader.getLinkData(nodeId));
break;
case 9: // definition
lazyGroup(node, ["url", "title", "identifier", "label"], () => reader.getDefinitionData(nodeId));
break;
case 16: // image
lazyGroup(node, ["url", "alt", "title"], () => reader.getImageData(nodeId));
break;
case 5: {

@@ -97,14 +32,2 @@ // list

break;
case 17: // linkReference
lazyGroup(node, ["identifier", "label", "referenceType"], () => reader.getReferenceData(nodeId));
break;
case 20: // footnoteReference — no `referenceType` per mdast spec
lazyGroup(node, ["identifier", "label"], () => reader.getReferenceData(nodeId));
break;
case 18: // imageReference — leaf with alt (remark treats it as a void node)
lazyGroup(node, ["identifier", "label", "referenceType", "alt"], () => reader.getImageReferenceData(nodeId));
break;
case 19: // footnoteDefinition
lazyGroup(node, ["identifier", "label"], () => reader.getFootnoteDefinitionData(nodeId));
break;
case 21: // table

@@ -124,9 +47,2 @@ Object.defineProperties(node, {

break;
case 102: // mdxFlowExpression
case 103: // mdxTextExpression
case 104: // mdxjsEsm
Object.defineProperties(node, {
value: lazyProp("value", () => reader.getExpressionValue(nodeId)),
});
break;
// Nodes with no type-specific props:

@@ -133,0 +49,0 @@ // root(0), paragraph(1), thematicBreak(3), blockquote(4),

import type { MdastNodeRaw, BufferHeader, StringRefRaw, MdxJsxAttributeUnion } from "../types.js";
export declare const NodeType: Readonly<{
readonly Root: 0;
readonly Paragraph: 1;
readonly Heading: 2;
readonly ThematicBreak: 3;
readonly Blockquote: 4;
readonly List: 5;
readonly ListItem: 6;
readonly Html: 7;
readonly Code: 8;
readonly Definition: 9;
readonly Text: 10;
readonly Emphasis: 11;
readonly Strong: 12;
readonly InlineCode: 13;
readonly Break: 14;
readonly Link: 15;
readonly Image: 16;
readonly LinkReference: 17;
readonly ImageReference: 18;
readonly FootnoteDefinition: 19;
readonly FootnoteReference: 20;
readonly Table: 21;
readonly TableRow: 22;
readonly TableCell: 23;
readonly Delete: 24;
readonly Yaml: 25;
readonly Toml: 26;
readonly Math: 27;
readonly InlineMath: 28;
readonly ContainerDirective: 30;
readonly LeafDirective: 31;
readonly TextDirective: 32;
readonly MdxJsxFlowElement: 100;
readonly MdxJsxTextElement: 101;
readonly MdxFlowExpression: 102;
readonly MdxTextExpression: 103;
readonly MdxjsEsm: 104;
}>;
export declare const NodeTypeName: Record<number, string>;
export { NodeType, NodeTypeName } from "./generated/node-types.js";
export declare class MdastReader {

@@ -56,2 +17,4 @@ #private;

getNodeType(nodeId: number): number;
/** Fast path: read only the parent id for a node (0xffffffff at the root). */
getParentId(nodeId: number): number;
getChildIds(nodeId: number): number[];

@@ -63,6 +26,4 @@ /** Push child node IDs directly onto a stack array (reverse order for depth-first). */

readStringRef(typeData: Uint8Array, byteOffset?: number): StringRefRaw;
/** HeadingData: depth u8 @ 0. */
getHeadingDepth(nodeId: number): number;
/**
* StringRef value. Valid for Text, InlineCode, Html, Yaml, Toml, InlineMath nodes.
* StringRef value. Valid for Text, InlineCode, Html, Yaml, Toml nodes.
* These store a single StringRef as their type data.

@@ -72,46 +33,2 @@ */

/**
* LinkData: url(0..8), title(8..16).
* Valid for Link nodes.
*/
getLinkData(nodeId: number): {
url: string;
title: string | null;
};
/**
* ImageData: url(0..8), alt(8..16), title(16..24).
* Valid for Image nodes.
*/
getImageData(nodeId: number): {
url: string;
alt: string;
title: string | null;
};
/**
* CodeData #[repr(C)]: lang(0..8), meta(8..16), value(16..24), fence_char(24), _pad(25..28).
* Valid for Code nodes.
*/
getCodeData(nodeId: number): {
lang: string | null;
meta: string | null;
value: string;
};
/**
* MathData #[repr(C)]: meta(0..8), value(8..16).
* Valid for Math nodes.
*/
getMathData(nodeId: number): {
meta: string | null;
value: string;
};
/**
* DefinitionData #[repr(C)]: url(0..8), title(8..16), identifier(16..24), label(24..32).
* Valid for Definition nodes.
*/
getDefinitionData(nodeId: number): {
url: string;
title: string | null;
identifier: string;
label: string;
};
/**
* ListData #[repr(C)]: start(0..4), ordered(4), spread(5), _pad(6..8).

@@ -134,30 +51,2 @@ * Valid for List nodes.

/**
* ReferenceData #[repr(C)]: identifier(0..8), label(8..16), reference_kind(16), _pad(17..20).
* referenceKind: 0=shortcut, 1=collapsed, 2=full.
* Valid for LinkReference, ImageReference, FootnoteReference nodes.
*/
getReferenceData(nodeId: number): {
identifier: string;
label: string;
referenceType: string;
};
/**
* ImageReference layout: 20-byte ReferenceData header + 8-byte alt StringRef.
* Parser-emitted image references carry alt inline; plugin-created ones
* may lack this suffix, in which case `alt` is empty.
*/
getImageReferenceData(nodeId: number): {
identifier: string;
label: string;
referenceType: string;
alt: string;
};
/**
* FootnoteDefinitionData #[repr(C)]: identifier(0..8), label(8..16).
*/
getFootnoteDefinitionData(nodeId: number): {
identifier: string;
label: string;
};
/**
* TableData #[repr(C)]: align_count(0..4), then align_count bytes.

@@ -196,7 +85,2 @@ * Alignment bytes: 0=none, 1=left, 2=right, 3=center.

/**
* ExpressionData #[repr(C)]: value StringRef (0..8).
* Valid for MdxFlowExpression, MdxTextExpression, MdxjsEsm.
*/
getExpressionValue(nodeId: number): string;
/**
* Walk the tree depth-first. Return false from visitor to skip children.

@@ -203,0 +87,0 @@ */

@@ -1,92 +0,7 @@

// Node type discriminant values (must match NodeType enum in node.rs)
export const NodeType = Object.freeze({
Root: 0,
Paragraph: 1,
Heading: 2,
ThematicBreak: 3,
Blockquote: 4,
List: 5,
ListItem: 6,
Html: 7,
Code: 8,
Definition: 9,
Text: 10,
Emphasis: 11,
Strong: 12,
InlineCode: 13,
Break: 14,
Link: 15,
Image: 16,
LinkReference: 17,
ImageReference: 18,
FootnoteDefinition: 19,
FootnoteReference: 20,
Table: 21,
TableRow: 22,
TableCell: 23,
Delete: 24,
Yaml: 25,
Toml: 26,
Math: 27,
InlineMath: 28,
// Directives
ContainerDirective: 30,
LeafDirective: 31,
TextDirective: 32,
// MDX
MdxJsxFlowElement: 100,
MdxJsxTextElement: 101,
MdxFlowExpression: 102,
MdxTextExpression: 103,
MdxjsEsm: 104,
});
// Reverse map: number → string name
export const NodeTypeName = Object.fromEntries(Object.entries(NodeType).map(([k, v]) => [v, k]));
// MdastNode field offsets within NODE_STRUCT_SIZE bytes.
// Must match MdastNode #[repr(C)] layout in node.rs:
// id: u32 @ 0
// node_type: u8 @ 4
// _pad: [u8; 3] @ 5
// parent: u32 @ 8
// start_offset: u32 @ 12
// end_offset: u32 @ 16
// start_line: u32 @ 20
// start_column: u32 @ 24
// end_line: u32 @ 28
// end_column: u32 @ 32
// children_start: u32 @ 36
// children_count: u32 @ 40
// data_offset: u32 @ 44
// data_len: u32 @ 48
// Total: 52 bytes
const FIELD = {
id: 0,
node_type: 4, // u8
parent: 8,
start_offset: 12,
end_offset: 16,
start_line: 20,
start_column: 24,
end_line: 28,
end_column: 32,
children_start: 36,
children_count: 40,
data_offset: 44,
data_len: 48,
};
// BufferHeader field offsets (all u32, little-endian):
// magic: [u8; 4] @ 0
// kind: u32 @ 4 (1 = mdast, 2 = hast)
// node_struct_size: u32 @ 8
// node_count: u32 @ 12
// nodes_offset: u32 @ 16
// children_count: u32 @ 20
// children_offset: u32 @ 24
// type_data_len: u32 @ 28
// type_data_offset: u32 @ 32
// source_len: u32 @ 36
// source_offset: u32 @ 40
// Total: 44 bytes
const MAGIC = 0x5241444d; // "MDAR" bytes [0x4d,0x44,0x41,0x52] read as little-endian u32
const KIND_MDAST = 1;
import { restorePhantomSpaces } from "../phantom.js";
import { readPosition } from "../wire-read.js";
import { decodeColumnAlign } from "./column-align.js";
import { NodeTypeName } from "./generated/node-types.js";
import { ARENA_MAGIC, KIND_MDAST, FIELD, HEADER } from "../generated/arena-layout.js";
export { NodeType, NodeTypeName } from "./generated/node-types.js";
export class MdastReader {

@@ -109,7 +24,7 @@ #view;

const v = this.#view;
const magic = v.getUint32(0, true);
if (magic !== MAGIC) {
throw new Error(`Invalid buffer: bad magic 0x${magic.toString(16)}, expected 0x${MAGIC.toString(16)}`);
const magic = v.getUint32(HEADER.magic, true);
if (magic !== ARENA_MAGIC) {
throw new Error(`Invalid buffer: bad magic 0x${magic.toString(16)}, expected 0x${ARENA_MAGIC.toString(16)}`);
}
const kind = v.getUint32(4, true);
const kind = v.getUint32(HEADER.kind, true);
if (kind !== KIND_MDAST) {

@@ -120,13 +35,13 @@ throw new Error(`MdastReader was handed a buffer of kind ${kind} (expected ${KIND_MDAST}). ` +

return {
nodeStructSize: v.getUint32(8, true),
nodeCount: v.getUint32(12, true),
nodesOffset: v.getUint32(16, true),
childrenCount: v.getUint32(20, true),
childrenOffset: v.getUint32(24, true),
typeDataLen: v.getUint32(28, true),
typeDataOffset: v.getUint32(32, true),
sourceLen: v.getUint32(36, true),
sourceOffset: v.getUint32(40, true),
nodeDataCount: v.getUint32(44, true),
nodeDataOffset: v.getUint32(48, true),
nodeStructSize: v.getUint32(HEADER.node_struct_size, true),
nodeCount: v.getUint32(HEADER.node_count, true),
nodesOffset: v.getUint32(HEADER.nodes_offset, true),
childrenCount: v.getUint32(HEADER.children_count, true),
childrenOffset: v.getUint32(HEADER.children_offset, true),
typeDataLen: v.getUint32(HEADER.type_data_len, true),
typeDataOffset: v.getUint32(HEADER.type_data_offset, true),
sourceLen: v.getUint32(HEADER.source_len, true),
sourceOffset: v.getUint32(HEADER.source_offset, true),
nodeDataCount: v.getUint32(HEADER.node_data_count, true),
nodeDataOffset: v.getUint32(HEADER.node_data_offset, true),
};

@@ -186,21 +101,3 @@ }

const type = v.getUint8(base + FIELD.node_type);
const startLine = v.getUint32(base + FIELD.start_line, true);
const startOffset = v.getUint32(base + FIELD.start_offset, true);
// Sentinel: nodes with both line=0 and offset=0 are synthesized
// without source position (e.g. GFM autolink-literal nodes that
// remark-gfm produces via mdast-util-find-and-replace).
const position = startLine === 0 && startOffset === 0
? undefined
: {
start: {
offset: startOffset,
line: startLine,
column: v.getUint32(base + FIELD.start_column, true),
},
end: {
offset: v.getUint32(base + FIELD.end_offset, true),
line: v.getUint32(base + FIELD.end_line, true),
column: v.getUint32(base + FIELD.end_column, true),
},
};
const position = readPosition(v, base + FIELD.start_offset);
return {

@@ -223,11 +120,18 @@ id: v.getUint32(base + FIELD.id, true),

}
/** Fast path: read only the parent id for a node (0xffffffff at the root). */
getParentId(nodeId) {
const { nodesOffset, nodeStructSize } = this.#header;
return this.#view.getUint32(nodesOffset + nodeId * nodeStructSize + FIELD.parent, true);
}
getChildIds(nodeId) {
const node = this.getNode(nodeId);
if (node.childrenCount === 0)
const { nodesOffset, nodeStructSize, childrenOffset } = this.#header;
const base = nodesOffset + nodeId * nodeStructSize;
const v = this.#view;
const childrenStart = v.getUint32(base + FIELD.children_start, true);
const childrenCount = v.getUint32(base + FIELD.children_count, true);
if (childrenCount === 0)
return [];
const { childrenOffset } = this.#header;
const ids = [];
for (let i = 0; i < node.childrenCount; i++) {
const off = childrenOffset + (node.childrenStart + i) * 4;
ids.push(this.#view.getUint32(off, true));
for (let i = 0; i < childrenCount; i++) {
ids.push(v.getUint32(childrenOffset + (childrenStart + i) * 4, true));
}

@@ -250,7 +154,9 @@ return ids;

getTypeData(nodeId) {
const node = this.getNode(nodeId);
if (node.dataLen === 0)
const base = this.#header.nodesOffset + nodeId * this.#header.nodeStructSize;
const v = this.#view;
const dataOffset = v.getUint32(base + FIELD.data_offset, true);
const dataLen = v.getUint32(base + FIELD.data_len, true);
if (dataLen === 0)
return new Uint8Array(0);
const { typeDataOffset } = this.#header;
return new Uint8Array(this.#view.buffer, this.#view.byteOffset + typeDataOffset + node.dataOffset, node.dataLen);
return new Uint8Array(v.buffer, v.byteOffset + this.#header.typeDataOffset + dataOffset, dataLen);
}

@@ -265,8 +171,4 @@ /** Read a StringRef (offset: u32 LE, len: u32 LE) from type data. */

}
/** HeadingData: depth u8 @ 0. */
getHeadingDepth(nodeId) {
return this.getTypeData(nodeId)[0];
}
/**
* StringRef value. Valid for Text, InlineCode, Html, Yaml, Toml, InlineMath nodes.
* StringRef value. Valid for Text, InlineCode, Html, Yaml, Toml nodes.
* These store a single StringRef as their type data.

@@ -280,75 +182,2 @@ */

/**
* LinkData: url(0..8), title(8..16).
* Valid for Link nodes.
*/
getLinkData(nodeId) {
const data = this.getTypeData(nodeId);
const urlRef = this.readStringRef(data, 0);
const titleRef = this.readStringRef(data, 8);
return {
url: this.getString(urlRef.offset, urlRef.len),
title: titleRef.len > 0 ? this.getString(titleRef.offset, titleRef.len) : null,
};
}
/**
* ImageData: url(0..8), alt(8..16), title(16..24).
* Valid for Image nodes.
*/
getImageData(nodeId) {
const data = this.getTypeData(nodeId);
const urlRef = this.readStringRef(data, 0);
const altRef = this.readStringRef(data, 8);
const titleRef = this.readStringRef(data, 16);
return {
url: this.getString(urlRef.offset, urlRef.len),
alt: this.getString(altRef.offset, altRef.len),
title: titleRef.len > 0 ? this.getString(titleRef.offset, titleRef.len) : null,
};
}
/**
* CodeData #[repr(C)]: lang(0..8), meta(8..16), value(16..24), fence_char(24), _pad(25..28).
* Valid for Code nodes.
*/
getCodeData(nodeId) {
const data = this.getTypeData(nodeId);
const langRef = this.readStringRef(data, 0);
const metaRef = this.readStringRef(data, 8);
const valueRef = this.readStringRef(data, 16);
return {
lang: langRef.len > 0 ? this.getString(langRef.offset, langRef.len) : null,
meta: metaRef.len > 0 ? this.getString(metaRef.offset, metaRef.len) : null,
value: this.getString(valueRef.offset, valueRef.len),
};
}
/**
* MathData #[repr(C)]: meta(0..8), value(8..16).
* Valid for Math nodes.
*/
getMathData(nodeId) {
const data = this.getTypeData(nodeId);
const metaRef = this.readStringRef(data, 0);
const valueRef = this.readStringRef(data, 8);
return {
meta: metaRef.len > 0 ? this.getString(metaRef.offset, metaRef.len) : null,
value: this.getString(valueRef.offset, valueRef.len),
};
}
/**
* DefinitionData #[repr(C)]: url(0..8), title(8..16), identifier(16..24), label(24..32).
* Valid for Definition nodes.
*/
getDefinitionData(nodeId) {
const data = this.getTypeData(nodeId);
const urlRef = this.readStringRef(data, 0);
const titleRef = this.readStringRef(data, 8);
const identifierRef = this.readStringRef(data, 16);
const labelRef = this.readStringRef(data, 24);
return {
url: this.getString(urlRef.offset, urlRef.len),
title: titleRef.len > 0 ? this.getString(titleRef.offset, titleRef.len) : null,
identifier: this.getString(identifierRef.offset, identifierRef.len),
label: this.getString(labelRef.offset, labelRef.len),
};
}
/**
* ListData #[repr(C)]: start(0..4), ordered(4), spread(5), _pad(6..8).

@@ -379,48 +208,2 @@ * Valid for List nodes.

/**
* ReferenceData #[repr(C)]: identifier(0..8), label(8..16), reference_kind(16), _pad(17..20).
* referenceKind: 0=shortcut, 1=collapsed, 2=full.
* Valid for LinkReference, ImageReference, FootnoteReference nodes.
*/
getReferenceData(nodeId) {
const data = this.getTypeData(nodeId);
const identifierRef = this.readStringRef(data, 0);
const labelRef = this.readStringRef(data, 8);
const kindByte = data[16];
const referenceTypes = ["shortcut", "collapsed", "full"];
return {
identifier: this.getString(identifierRef.offset, identifierRef.len),
label: this.getString(labelRef.offset, labelRef.len),
referenceType: referenceTypes[kindByte] ?? "shortcut",
};
}
/**
* ImageReference layout: 20-byte ReferenceData header + 8-byte alt StringRef.
* Parser-emitted image references carry alt inline; plugin-created ones
* may lack this suffix, in which case `alt` is empty.
*/
getImageReferenceData(nodeId) {
const base = this.getReferenceData(nodeId);
const data = this.getTypeData(nodeId);
if (data.length >= 28) {
const altRef = this.readStringRef(data, 20);
return {
...base,
alt: this.getString(altRef.offset, altRef.len),
};
}
return { ...base, alt: "" };
}
/**
* FootnoteDefinitionData #[repr(C)]: identifier(0..8), label(8..16).
*/
getFootnoteDefinitionData(nodeId) {
const data = this.getTypeData(nodeId);
const identifierRef = this.readStringRef(data, 0);
const labelRef = this.readStringRef(data, 8);
return {
identifier: this.getString(identifierRef.offset, identifierRef.len),
label: this.getString(labelRef.offset, labelRef.len),
};
}
/**
* TableData #[repr(C)]: align_count(0..4), then align_count bytes.

@@ -435,6 +218,5 @@ * Alignment bytes: 0=none, 1=left, 2=right, 3=center.

const count = view.getUint32(0, true);
const alignNames = [null, "left", "right", "center"];
const result = [];
for (let i = 0; i < count; i++) {
result.push(alignNames[data[4 + i]] ?? null);
result.push(decodeColumnAlign(data[4 + i]));
}

@@ -497,3 +279,3 @@ return result;

type: "mdxJsxAttributeValueExpression",
value: this.getString(attrValueRef.offset, attrValueRef.len).replaceAll("", " "),
value: restorePhantomSpaces(this.getString(attrValueRef.offset, attrValueRef.len)),
},

@@ -505,3 +287,3 @@ });

type: "mdxJsxExpressionAttribute",
value: this.getString(attrValueRef.offset, attrValueRef.len).replaceAll("", " "),
value: restorePhantomSpaces(this.getString(attrValueRef.offset, attrValueRef.len)),
});

@@ -540,11 +322,2 @@ break;

/**
* ExpressionData #[repr(C)]: value StringRef (0..8).
* Valid for MdxFlowExpression, MdxTextExpression, MdxjsEsm.
*/
getExpressionValue(nodeId) {
const data = this.getTypeData(nodeId);
const valueRef = this.readStringRef(data, 0);
return this.getString(valueRef.offset, valueRef.len).replaceAll("", " ");
}
/**
* Walk the tree depth-first. Return false from visitor to skip children.

@@ -551,0 +324,0 @@ */

@@ -0,4 +1,7 @@

import { MdastReader } from "./mdast-reader.js";
import { CommandBuffer } from "../command-buffer.js";
import type { MdastNode, Toml, MathNode, InlineMath } from "../types.js";
import type { Blockquote, Break, Code, Definition, Delete, Emphasis, FootnoteDefinition, FootnoteReference, Heading, Html, Image, ImageReference, InlineCode, Link, LinkReference, List, ListItem, Paragraph, Strong, Table, TableRow, TableCell, Text, ThematicBreak, Yaml } from "mdast";
import type { MdastNode, Toml, MathNode, InlineMath, Superscript, Subscript, Data } from "../types.js";
import { LazyChildResolver } from "../lazy-child-resolver.js";
import type { MdastHandle } from "../handles.js";
import type { Blockquote, Break, Code, Definition, Delete, Emphasis, FootnoteDefinition, FootnoteReference, Heading, Html, Image, ImageReference, InlineCode, Link, LinkReference, List, ListItem, Paragraph, Strong, Table, TableRow, TableCell, Text, ThematicBreak, Yaml, Parents as MdastParents, Root as MdastRoot } from "mdast";
import type { MdxJsxFlowElement, MdxJsxTextElement } from "../mdx-types.js";

@@ -8,2 +11,10 @@ import type { MdxFlowExpression, MdxTextExpression } from "../mdx-types.js";

import type { ContainerDirective, LeafDirective, TextDirective } from "../directive-types.js";
/** New content for a structural mutation: a declarative node, or a raw markdown
* / HTML escape hatch. Declarative nodes compile to the op-stream; a type the
* op-stream can't encode is a hard error. */
export type MdastContent = MdastNode | {
raw: string;
} | {
rawHtml: string;
};
export interface MdastDiagnostic {

@@ -23,7 +34,15 @@ message: string;

readonly fileURL: URL | undefined;
constructor(handle: MdastHandle, getSource: () => string, fileURL: URL | undefined);
/**
* Document-level data bag, shared across every plugin in the compile and
* across the mdast→hast phase boundary. Mutate keys directly
* (`ctx.data.foo = x`); the bag itself isn't reassignable. Values are kept
* on the JS side, so any value is allowed, including functions and class
* instances. Returned to the caller as `result.data`.
*/
readonly data: Data;
constructor(handle: MdastHandle, getSource: () => string, fileURL: URL | undefined, resolver: LazyChildResolver<MdastReader, MdastNode>, data: Data);
get source(): string;
removeNode(node: Readonly<MdastNode>): void;
insertBefore(node: Readonly<MdastNode>, newNode: MdastNode | MdastNode[]): void;
insertAfter(node: Readonly<MdastNode>, newNode: MdastNode | MdastNode[]): void;
insertBefore(node: Readonly<MdastNode>, newNode: MdastContent | MdastContent[]): void;
insertAfter(node: Readonly<MdastNode>, newNode: MdastContent | MdastContent[]): void;
/**

@@ -33,11 +52,18 @@ * Wrap `node` in `parentNode`, making it `parentNode`'s first child. Any

*/
wrapNode(node: Readonly<MdastNode>, parentNode: MdastNode): void;
prependChild(node: Readonly<MdastNode>, childNode: MdastNode | MdastNode[]): void;
appendChild(node: Readonly<MdastNode>, childNode: MdastNode | MdastNode[]): void;
wrapNode(node: Readonly<MdastNode>, parentNode: MdastContent): void;
prependChild(node: Readonly<MdastNode>, childNode: MdastContent | MdastContent[]): void;
appendChild(node: Readonly<MdastNode>, childNode: MdastContent | MdastContent[]): void;
/** Insert one node or an array at `index`; clamps (`0` or less prepends, past the end appends). */
insertChildAt(node: Readonly<MdastNode>, index: number, childNode: MdastNode | MdastNode[]): void;
insertChildAt(node: Readonly<MdastNode>, index: number, childNode: MdastContent | MdastContent[]): void;
/** Remove the `index`-th child of `node`; a no-op when there is no such child. */
removeChildAt(node: Readonly<MdastNode>, index: number): void;
replaceNode(node: Readonly<MdastNode>, newNode: MdastNode): void;
replaceNode(node: Readonly<MdastNode>, newNode: MdastContent): void;
setProperty<N extends MdastNode, K extends keyof N & string>(node: Readonly<N>, key: K, value: N[K]): void;
/** `children` is structural and every parent accepts it, so the key also
* works on node-type unions (e.g. a node returned by `parent()`). */
setProperty(node: Readonly<MdastNode>, key: "children", value: readonly MdastNode[]): void;
/** `data` is an open per-node bag serialized to JSON on the wire, so it
* accepts any record (hName/hProperties/custom fields), not just the node's
* declared `data` shape. `null` clears it. */
setProperty(node: Readonly<MdastNode>, key: "data", value: Record<string, unknown> | null): void;
/** Collect the concatenated text of all descendant text nodes (like mdast-util-to-string). */

@@ -48,2 +74,14 @@ textContent(node: Readonly<MdastNode>, options?: {

}): string;
/**
* The parent of a node, or `undefined` at the root. Within a pass the same
* parent is always the same object, so visitors on sibling nodes can dedupe
* by identity.
*/
parent<N extends Exclude<MdastNode, MdastRoot>>(node: Readonly<N>): Readonly<MdastParents>;
parent(node: Readonly<MdastNode>): Readonly<MdastParents> | undefined;
/**
* Index of `node` within its parent's children, or `undefined` at the root.
* Use this rather than `parent.children.indexOf(node)`, which won't find it.
*/
indexOf(node: Readonly<MdastNode>): number | undefined;
report({ message, node, severity, }: {

@@ -96,2 +134,4 @@ message: string;

textDirective?: MdastVisitorFn<TextDirective>;
superscript?: MdastVisitorFn<Superscript>;
subscript?: MdastVisitorFn<Subscript>;
mdxJsxFlowElement?: MdastVisitorFn<MdxJsxFlowElement>;

@@ -109,3 +149,3 @@ mdxJsxTextElement?: MdastVisitorFn<MdxJsxTextElement>;

}
export type MdastHandle = any;
export type { MdastHandle };
interface MdastSubscription {

@@ -115,3 +155,2 @@ nodeType: number;

}
/** Resolve subscriptions from a plugin instance. */
export declare function resolveMdastSubscriptions(plugin: MdastPluginInstance): MdastSubscription[];

@@ -125,3 +164,2 @@ /**

*/
export declare function visitMdastHandle(handle: MdastHandle, plugin: MdastPluginInstance, subs: MdastSubscription[], source: string | (() => string), fileURL: URL | undefined): MdastVisitResult | Promise<MdastVisitResult>;
export {};
export declare function visitMdastHandle(handle: MdastHandle, plugin: MdastPluginInstance, subs: MdastSubscription[], source: string | (() => string), fileURL: URL | undefined, data?: Data): MdastVisitResult | Promise<MdastVisitResult>;

@@ -1,61 +0,29 @@

import { materializeNode, TYPE_NAMES } from "./mdast-materializer.js";
import { materializeNode } from "./mdast-materializer.js";
import { MdastReader } from "./mdast-reader.js";
import { CommandBuffer, classifyReturn } from "../command-buffer.js";
import { walkMdastHandle, serializeHandle, getNodeData as napiGetNodeData, mdastTextContentHandle, } from "#binding";
const MutationType = {
Replace: "replace",
Remove: "remove",
InsertBefore: "insertBefore",
InsertAfter: "insertAfter",
Wrap: "wrap",
PrependChild: "prependChild",
AppendChild: "appendChild",
SetProperty: "setProperty",
};
const VISITOR_KEYS = new Set([
"paragraph",
"heading",
"thematicBreak",
"blockquote",
"list",
"listItem",
"html",
"code",
"definition",
"text",
"emphasis",
"strong",
"inlineCode",
"break",
"link",
"image",
"linkReference",
"imageReference",
"footnoteDefinition",
"footnoteReference",
"table",
"tableRow",
"tableCell",
"delete",
"yaml",
"toml",
"math",
"inlineMath",
"containerDirective",
"leafDirective",
"textDirective",
"mdxJsxFlowElement",
"mdxJsxTextElement",
"mdxFlowExpression",
"mdxTextExpression",
"mdxjsEsm",
]);
import { ru32, rstr, readPosition } from "../wire-read.js";
import { decodeMdastTypeData } from "./generated/layout.js";
import { TYPE_NAMES, NAME_TO_TYPE, VISITOR_KEYS, MDAST_OPSTREAM_TYPES, } from "./generated/node-types.js";
import { OpWriter, OF_VALUE, OF_URL, OF_TITLE, OF_ALT, OF_LANG, OF_META, OF_IDENTIFIER, OF_LABEL, OF_NAME, OF_REFERENCE_TYPE, OF_DEPTH, OF_CHECKED, OF_START, OF_ORDERED, OF_SPREAD, OF_EXPLICIT, PROP_STRING, emitMdxAttr, } from "../op-stream.js";
import { walkMdastHandle, mdastTextContentHandle } from "#binding";
import { asArray, makeRequireNid, mergeAndReset, unencodableContentError, } from "../visitor-shared.js";
import { LazyChildResolver } from "../lazy-child-resolver.js";
import { MdastChildStub } from "./child-stub.js";
/** Maps MdastNode objects to their arena node IDs without Object.defineProperty overhead. */
const mdastNodeIdMap = new WeakMap();
function nid(node) {
return mdastNodeIdMap.get(node) ?? node._nodeId;
// Genuine stubs carry their id as a plain field; a spread copy is not
// `instanceof` and has no `_nodeId`, so it correctly reads as new content.
if (node instanceof MdastChildStub)
return node._id;
const id = mdastNodeIdMap.get(node);
if (id !== undefined)
return id;
// Plain objects are trusted only via the WeakMap or a NON-enumerable
// `_nodeId` (the materializer's convention, which spread cannot copy) — an
// enumerable one rode in on a copy and must read as new content.
const d = Object.getOwnPropertyDescriptor(node, "_nodeId");
return d !== undefined && !d.enumerable ? d.value : undefined;
}
function asArray(value) {
return Array.isArray(value) ? value : [value];
}
const requireNid = makeRequireNid(nid);
export class MdastVisitorContext {

@@ -66,2 +34,6 @@ #commandBuffer = new CommandBuffer();

#getSource;
#resolver;
/** One canonical object per parent id, so visitors can dedupe by identity.
* Null until the first `parent()` call; most passes never make one. */
#parentsById = null;
/**

@@ -73,6 +45,16 @@ * The URL of the document being processed (the compile `fileURL` option),

fileURL;
constructor(handle, getSource, fileURL) {
/**
* Document-level data bag, shared across every plugin in the compile and
* across the mdast→hast phase boundary. Mutate keys directly
* (`ctx.data.foo = x`); the bag itself isn't reassignable. Values are kept
* on the JS side, so any value is allowed, including functions and class
* instances. Returned to the caller as `result.data`.
*/
data;
constructor(handle, getSource, fileURL, resolver, data) {
this.#handle = handle;
this.#getSource = getSource;
this.fileURL = fileURL;
this.#resolver = resolver;
this.data = data;
}

@@ -85,13 +67,13 @@ get source() {

removeNode(node) {
this.#commandBuffer.removeNode(nid(node));
this.#commandBuffer.removeNode(requireNid(node, "removeNode"));
}
insertBefore(node, newNode) {
const id = nid(node);
const id = requireNid(node, "insertBefore");
for (const n of asArray(newNode))
this.#commandBuffer.insertBefore(id, n);
emitMdastTree(this.#commandBuffer, "insertBefore", id, n);
}
insertAfter(node, newNode) {
const id = nid(node);
const id = requireNid(node, "insertAfter");
for (const n of asArray(newNode))
this.#commandBuffer.insertAfter(id, n);
emitMdastTree(this.#commandBuffer, "insertAfter", id, n);
}

@@ -103,13 +85,14 @@ /**

wrapNode(node, parentNode) {
this.#commandBuffer.wrapNode(nid(node), parentNode);
const id = requireNid(node, "wrapNode");
emitMdastTree(this.#commandBuffer, "wrapNode", id, parentNode);
}
prependChild(node, childNode) {
const id = nid(node);
const id = requireNid(node, "prependChild");
for (const n of asArray(childNode))
this.#commandBuffer.prependChild(id, n);
emitMdastTree(this.#commandBuffer, "prependChild", id, n);
}
appendChild(node, childNode) {
const id = nid(node);
const id = requireNid(node, "appendChild");
for (const n of asArray(childNode))
this.#commandBuffer.appendChild(id, n);
emitMdastTree(this.#commandBuffer, "appendChild", id, n);
}

@@ -136,3 +119,4 @@ /** Insert one node or an array at `index`; clamps (`0` or less prepends, past the end appends). */

replaceNode(node, newNode) {
this.#commandBuffer.replace(nid(node), newNode);
const id = requireNid(node, "replaceNode");
emitMdastTree(this.#commandBuffer, "replace", id, newNode, true);
}

@@ -143,4 +127,7 @@ setProperty(node, key, value) {

// child list (reused children keep their id).
const wrapper = refifyReusedNodes({ type: "root", children: value }, true);
this.#commandBuffer.setChildren(nid(node), JSON.stringify(wrapper));
const id = requireNid(node, "setProperty");
const ops = compileMdastChildrenToOpstream(value);
if (!ops)
throw unencodableContentError(value);
this.#commandBuffer.setChildrenOpstream(id, ops);
return;

@@ -150,11 +137,30 @@ }

// data is stored as JSON in the arena, serialize it for the command buffer
this.#commandBuffer.setProperty(nid(node), key, value != null ? JSON.stringify(value) : null);
this.#commandBuffer.setProperty(requireNid(node, "setProperty"), key, value != null ? JSON.stringify(value) : null);
return;
}
this.#commandBuffer.setProperty(nid(node), key, value);
this.#commandBuffer.setProperty(requireNid(node, "setProperty"), key, value);
}
/** Collect the concatenated text of all descendant text nodes (like mdast-util-to-string). */
textContent(node, options) {
return mdastTextContentHandle(this.#handle, nid(node), options);
return mdastTextContentHandle(this.#handle, requireNid(node, "textContent"), options);
}
parent(node) {
const parentId = this.#resolver.parentIdOf(requireNid(node, "parent"));
if (parentId === undefined)
return undefined;
const byId = (this.#parentsById ??= new Map());
let parent = byId.get(parentId);
if (parent === undefined) {
parent = this.#resolver.materializeOne(parentId);
byId.set(parentId, parent);
}
return parent;
}
/**
* Index of `node` within its parent's children, or `undefined` at the root.
* Use this rather than `parent.children.indexOf(node)`, which won't find it.
*/
indexOf(node) {
return this.#resolver.indexInParent(requireNid(node, "indexOf"));
}
report({ message, node, severity = "error", }) {

@@ -176,28 +182,2 @@ this.#diagnostics.push({

}
/** Merge return-value + context command buffers and release internals. */
function mergeAndReset(returnBuffer, ctx) {
const ctxCmdBuf = ctx.getCommandBuffer();
const ctxBuf = ctxCmdBuf.getBuffer();
const retBuf = returnBuffer.getBuffer();
const totalLen = retBuf.length + ctxBuf.length;
let merged;
if (totalLen === 0) {
merged = new Uint8Array(0);
}
else {
merged = new Uint8Array(totalLen);
merged.set(retBuf, 0);
merged.set(ctxBuf, retBuf.length);
}
returnBuffer.reset();
ctxCmdBuf.reset();
return { merged, hasMutations: totalLen > 0 };
}
const textDecoder = new TextDecoder("utf-8");
/** Build name→nodeType map from TYPE_NAMES (reverse of TYPE_NAMES). */
const NAME_TO_TYPE = {};
for (const [num, name] of Object.entries(TYPE_NAMES)) {
NAME_TO_TYPE[name] = Number(num);
}
/** Resolve subscriptions from a plugin instance. */
export function resolveMdastSubscriptions(plugin) {

@@ -218,54 +198,47 @@ const subs = [];

}
/** Read a u16 from buf at offset (LE). */
function ru16(view, off) {
return view.getUint16(off, true);
class MdastLazyChildResolver extends LazyChildResolver {
createReader(wire) {
return new MdastReader(wire);
}
materializeNode(reader, nodeId) {
return materializeNode(reader, nodeId);
}
readParentId(reader, nodeId) {
return reader.getParentId(nodeId);
}
readChildIds(reader, nodeId) {
return reader.getChildIds(nodeId);
}
}
/** Read a u32 from buf at offset (LE). */
function ru32(view, off) {
return view.getUint32(off, true);
/** Build the child-stub list for a matched node from the wire's `[child_ids]
* [child_types]` blocks — no arena snapshot. The seal check still applies:
* post-pass ids are stale, and a stub built from them could later splice the
* wrong node as a ref. */
function readMdastChildStubs(view, buf, idsPos, typesPos, count, resolver) {
resolver.assertUnsealed();
const stubs = new Array(count);
for (let i = 0; i < count; i++) {
stubs[i] = new MdastChildStub(resolver, ru32(view, idsPos + i * 4), buf[typesPos + i]);
}
return stubs;
}
/** Read a utf8 string from buf. */
function rstr(buf, off, len) {
return len === 0 ? "" : textDecoder.decode(buf.subarray(off, off + len));
}
/**
* Lazy child materializer for the MDAST handle walk path.
* Serializes the handle once on first child access, then materializes
* children via MdastReader + materializeNode.
*/
class MdastLazyChildResolver {
#handle;
#reader = null;
constructor(handle) {
this.#handle = handle;
}
#ensure() {
if (!this.#reader) {
this.#reader = new MdastReader(serializeHandle(this.#handle));
}
return this.#reader;
}
materializeChildren(childIds) {
const reader = this.#ensure();
const handle = this.#handle;
return childIds.map((id) => {
const node = materializeNode(reader, id);
Object.defineProperty(node, "data", {
get() {
const json = napiGetNodeData(handle, id);
const val = json ? JSON.parse(json) : null;
Object.defineProperty(this, "data", {
value: val,
writable: true,
enumerable: true,
configurable: true,
});
return val;
},
/** Install `children` as an own enumerable getter (spread must carry it),
* self-replacing with the one stable stub array on first read. One closure
* and one define per node — installing the wire locals as hidden slots
* instead measurably regressed every matching pipeline. */
function makeLazyChildren(node, view, buf, childIdsPos, childTypesPos, childCount, resolver) {
Object.defineProperty(node, "children", {
get() {
const val = readMdastChildStubs(view, buf, childIdsPos, childTypesPos, childCount, resolver);
Object.defineProperty(this, "children", {
value: val,
writable: true,
enumerable: true,
configurable: true,
enumerable: true,
});
return node;
});
}
return val;
},
enumerable: true,
configurable: true,
});
}

@@ -276,8 +249,7 @@ /**

* Inline format (from Rust serialize_mdast_node_inline):
* [node_data: u32+bytes][position: 6×u32 = 24B][child_count: u16][child_ids: N×u32][type-specific data]
* [node_data: u32+bytes][position: 6×u32 = 24B][child_count: u32][child_ids: N×u32]
* [child_types: N×u8][type-specific data]
*/
const encoder = new TextEncoder();
function readMdastMatchedNode(view, buf, dataOffset, nodeId, nodeType, resolver) {
let pos = dataOffset;
// Node data (JSON bytes), always first
const dataJsonLen = ru32(view, pos);

@@ -291,268 +263,50 @@ pos += 4;

}
catch {
/* ignore */
catch (err) {
if (process.env.NODE_ENV !== "production") {
console.warn(`readMdastMatchedNode: malformed node_data for nodeId=${nodeId}`, err);
}
}
pos += dataJsonLen;
}
// Position
const position = {
start: { offset: ru32(view, pos), line: ru32(view, pos + 8), column: ru32(view, pos + 12) },
end: { offset: ru32(view, pos + 4), line: ru32(view, pos + 16), column: ru32(view, pos + 20) },
};
const position = readPosition(view, pos);
pos += 24;
// Children, read IDs, materialize lazily via resolver
const childCount = ru16(view, pos);
pos += 2;
const childIds = [];
for (let i = 0; i < childCount; i++) {
childIds.push(ru32(view, pos));
pos += 4;
}
const childCount = ru32(view, pos);
pos += 4;
// Ids/types decode lazily with `.children` — most matched nodes never read them.
const childIdsPos = pos;
pos += childCount * 4;
const childTypesPos = pos;
pos += childCount;
const typeName = TYPE_NAMES[nodeType] ?? `unknown(${nodeType})`;
// Build node with type-specific fields
const node = { type: typeName, position };
const node = { type: typeName };
if (position !== undefined)
node.position = position;
if (childCount > 0) {
Object.defineProperty(node, "children", {
get() {
const val = resolver.materializeChildren(childIds);
Object.defineProperty(this, "children", {
value: val,
writable: true,
enumerable: true,
configurable: true,
});
return val;
},
configurable: true,
enumerable: true,
});
makeLazyChildren(node, view, buf, childIdsPos, childTypesPos, childCount, resolver);
}
switch (nodeType) {
case 2: {
// heading
node.depth = buf[pos];
break;
}
case 10:
case 13:
case 7:
case 25:
case 26: {
// text, inlineCode, html, yaml, toml
const vlen = ru32(view, pos);
node.value = rstr(buf, pos + 4, vlen);
break;
}
case 8: {
// code
const langLen = ru16(view, pos);
pos += 2;
node.lang = langLen > 0 ? rstr(buf, pos, langLen) : null;
pos += langLen;
const metaLen = ru16(view, pos);
pos += 2;
node.meta = metaLen > 0 ? rstr(buf, pos, metaLen) : null;
pos += metaLen;
const valLen = ru32(view, pos);
pos += 4;
node.value = rstr(buf, pos, valLen);
break;
}
case 27:
case 28: {
// math, inlineMath
const metaLen = ru16(view, pos);
pos += 2;
node.meta = metaLen > 0 ? rstr(buf, pos, metaLen) : null;
pos += metaLen;
const valLen = ru32(view, pos);
pos += 4;
node.value = rstr(buf, pos, valLen);
break;
}
case 15: {
// link
const urlLen = ru16(view, pos);
pos += 2;
node.url = rstr(buf, pos, urlLen);
pos += urlLen;
const titleLen = ru16(view, pos);
pos += 2;
node.title = titleLen > 0 ? rstr(buf, pos, titleLen) : null;
break;
}
case 16: {
// image
const urlLen = ru16(view, pos);
pos += 2;
node.url = rstr(buf, pos, urlLen);
pos += urlLen;
const altLen = ru16(view, pos);
pos += 2;
node.alt = rstr(buf, pos, altLen);
pos += altLen;
const titleLen = ru16(view, pos);
pos += 2;
node.title = titleLen > 0 ? rstr(buf, pos, titleLen) : null;
break;
}
case 9: {
// definition
const urlLen = ru16(view, pos);
pos += 2;
node.url = rstr(buf, pos, urlLen);
pos += urlLen;
const titleLen = ru16(view, pos);
pos += 2;
node.title = titleLen > 0 ? rstr(buf, pos, titleLen) : null;
pos += titleLen;
const idLen = ru16(view, pos);
pos += 2;
node.identifier = rstr(buf, pos, idLen);
pos += idLen;
const labelLen = ru16(view, pos);
pos += 2;
node.label = rstr(buf, pos, labelLen);
break;
}
case 5: {
// list
node.start = ru32(view, pos);
node.ordered = buf[pos + 4] !== 0;
node.spread = buf[pos + 5] !== 0;
if (!node.ordered)
node.start = null;
break;
}
case 6: {
// listItem
const checked = buf[pos];
node.checked = checked === 2 ? null : checked === 1;
node.spread = buf[pos + 1] !== 0;
break;
}
case 17:
case 18:
case 20: {
// linkReference, imageReference, footnoteReference
const idLen = ru16(view, pos);
pos += 2;
node.identifier = rstr(buf, pos, idLen);
pos += idLen;
const labelLen = ru16(view, pos);
pos += 2;
node.label = rstr(buf, pos, labelLen);
pos += labelLen;
const kind = buf[pos];
pos += 1;
// Only link/image references carry `referenceType`; the mdast spec
// defines it for those two, not for `footnoteReference`.
if (nodeType !== 20) {
node.referenceType = ["shortcut", "collapsed", "full"][kind] ?? "shortcut";
// Fixed-field types decode from the generated layout table; the rest
// (variable-length / cross-field) stay in the hand-written switch.
if (!decodeMdastTypeData(view, buf, pos, nodeType, node)) {
switch (nodeType) {
case 5: {
// list
node.start = ru32(view, pos);
node.ordered = buf[pos + 4] !== 0;
node.spread = buf[pos + 5] !== 0;
if (!node.ordered)
node.start = null;
break;
}
// imageReference also carries `alt` (the serializer appends it).
if (nodeType === 18) {
const altLen = ru16(view, pos);
pos += 2;
node.alt = rstr(buf, pos, altLen);
case 6: {
// listItem
const checked = buf[pos];
node.checked = checked === 2 ? null : checked === 1;
node.spread = buf[pos + 1] !== 0;
break;
}
break;
// table (21), directives (30/31/32) and mdxJsx elements (100/101) are
// decoded by the generated `decodeMdastTypeData` from their tails.
// root(0), paragraph(1), thematicBreak(3), blockquote(4), emphasis(11),
// strong(12), break(14), tableRow(22), tableCell(23), delete(24): no extra data
}
case 19: {
// footnoteDefinition
const idLen = ru16(view, pos);
pos += 2;
node.identifier = rstr(buf, pos, idLen);
pos += idLen;
const labelLen = ru16(view, pos);
pos += 2;
node.label = rstr(buf, pos, labelLen);
break;
}
case 21: {
// table
const count = ru16(view, pos);
pos += 2;
const alignNames = [null, "left", "right", "center"];
node.align = Array.from({ length: count }, (_, i) => alignNames[buf[pos + i]] ?? null);
break;
}
case 30:
case 31:
case 32: {
// containerDirective, leafDirective, textDirective
const nameLen = ru16(view, pos);
pos += 2;
node.name = rstr(buf, pos, nameLen);
pos += nameLen;
const attrCount = ru16(view, pos);
pos += 2;
const attributes = {};
for (let i = 0; i < attrCount; i++) {
const keyLen = ru16(view, pos);
pos += 2;
const key = rstr(buf, pos, keyLen);
pos += keyLen;
const valLen = ru16(view, pos);
pos += 2;
const val = rstr(buf, pos, valLen);
pos += valLen;
attributes[key] = val;
}
node.attributes = attributes;
break;
}
case 100:
case 101: {
// mdxJsxFlowElement, mdxJsxTextElement
const nameLen = ru16(view, pos);
pos += 2;
node.name = nameLen > 0 ? rstr(buf, pos, nameLen) : null;
pos += nameLen;
const attrCount = ru16(view, pos);
pos += 2;
const attributes = [];
for (let i = 0; i < attrCount; i++) {
const kind = buf[pos];
pos += 1;
const anLen = ru16(view, pos);
pos += 2;
const an = rstr(buf, pos, anLen);
pos += anLen;
const avLen = ru32(view, pos);
pos += 4;
const av = rstr(buf, pos, avLen);
pos += avLen;
switch (kind) {
case 0:
attributes.push({ type: "mdxJsxAttribute", name: an, value: null });
break;
case 1:
attributes.push({ type: "mdxJsxAttribute", name: an, value: av });
break;
case 2:
attributes.push({
type: "mdxJsxAttribute",
name: an,
value: { type: "mdxJsxAttributeValueExpression", value: av },
});
break;
case 3:
attributes.push({ type: "mdxJsxExpressionAttribute", value: av });
break;
}
}
node.attributes = attributes;
break;
}
case 102:
case 103:
case 104: {
// mdxFlowExpression, mdxTextExpression, mdxjsEsm
const vlen = ru32(view, pos);
node.value = rstr(buf, pos + 4, vlen);
break;
}
// root(0), paragraph(1), thematicBreak(3), blockquote(4), emphasis(11),
// strong(12), break(14), tableRow(22), tableCell(23), delete(24): no extra data
}

@@ -565,5 +319,2 @@ mdastNodeIdMap.set(node, nodeId);

}
/** Apply a sync visitor result to the return buffer.
* If the result is the same object as the input node, treat it as a no-op
* so that context mutations (e.g. setProperty) are not clobbered. */
/** The arena id of a node if it is an existing (materialized) node, else

@@ -577,24 +328,189 @@ * undefined for a freshly-built one. */

}
// Reused across every replacement in a pass: compile is synchronous and its
// result is copied into the command buffer before the next call, so a single
// writer is safe and avoids a 512-byte allocation per built node.
const mdastWriter = new OpWriter();
/**
* Rewrite a returned replacement tree so every *reused* node (one that came
* from the arena, at any depth) becomes a `{ _ref: id }` placeholder. The
* rebuild splices those originals back in place, preserving their ids — so a
* patch a nested visitor queued on a passed-through child still lands, in the
* same pass. Freshly-built nodes serialize as before. The root is never reffed:
* it is the new shape replacing the visited node.
* Compile a declarative MDAST replacement tree to the op-stream — the only
* structural encoding. Reused nodes (those still carrying an arena id) become
* `ref`s so the rebuild splices the original back in place. Returns null when
* the replay can't reproduce the tree identically (unsupported node type or
* out-of-range numeric field); the caller turns that into a hard error.
*/
function refifyReusedNodes(node, isRoot) {
function compileMdastToOpstream(root, forReplace = false) {
mdastWriter.begin();
try {
if (!emitMdastOp(mdastWriter, root, true, forReplace))
return null;
return mdastWriter.take();
}
finally {
mdastWriter.end();
}
}
/** Compile a set-children payload: a root-wrapped child list, the shape
* `Patch::SetChildren` splices in. Reused children become refs. */
function compileMdastChildrenToOpstream(children) {
if (!Array.isArray(children))
return null;
mdastWriter.begin();
try {
mdastWriter.open(NAME_TO_TYPE.root);
for (const c of children) {
if (!emitMdastOp(mdastWriter, c, false, false))
return null;
}
mdastWriter.close();
return mdastWriter.take();
}
finally {
mdastWriter.end();
}
}
function emitMdastOp(w, node, isRoot, forReplace) {
if (node === null || typeof node !== "object")
return node;
return false;
if (!isRoot) {
const id = reusedId(node);
if (id !== undefined)
return { _ref: id };
if (id !== undefined) {
w.ref(id);
return true;
}
}
const children = node.children;
if (Array.isArray(children)) {
return { ...node, children: children.map((c) => refifyReusedNodes(c, false)) };
const n = node;
const type = MDAST_OPSTREAM_TYPES[n.type];
if (type === undefined)
return false;
w.open(type);
if (typeof n.value === "string")
w.str(OF_VALUE, n.value);
if (typeof n.url === "string")
w.str(OF_URL, n.url);
if (typeof n.title === "string")
w.str(OF_TITLE, n.title);
if (typeof n.alt === "string")
w.str(OF_ALT, n.alt);
if (typeof n.lang === "string")
w.str(OF_LANG, n.lang);
if (typeof n.meta === "string")
w.str(OF_META, n.meta);
if (typeof n.identifier === "string")
w.str(OF_IDENTIFIER, n.identifier);
if (typeof n.label === "string")
w.str(OF_LABEL, n.label);
if (typeof n.referenceType === "string")
w.str(OF_REFERENCE_TYPE, n.referenceType);
// Out-of-range numbers compile to null and the caller throws — a visible
// error instead of silently masking the bits.
if (typeof n.depth === "number") {
if (!Number.isInteger(n.depth) || n.depth < 0 || n.depth > 255)
return false;
w.u8(OF_DEPTH, n.depth);
}
return node;
if (typeof n.checked === "boolean")
w.u8(OF_CHECKED, n.checked ? 1 : 0);
if (typeof n.start === "number") {
if (!Number.isInteger(n.start) || n.start < 0 || n.start > 4294967295)
return false;
w.u32(OF_START, n.start);
}
if (typeof n.ordered === "boolean")
w.bool(OF_ORDERED, n.ordered);
if (typeof n.spread === "boolean")
w.bool(OF_SPREAD, n.spread);
if (typeof n.name === "string")
w.str(OF_NAME, n.name);
const attrs = n.attributes;
if (Array.isArray(attrs)) {
for (const a of attrs)
emitMdxAttr(w, a);
}
else if (attrs !== null && typeof attrs === "object") {
// Directive attributes: a string→string map; non-string values are
// dropped, since the stored form holds only strings.
for (const key in attrs) {
const v = attrs[key];
if (typeof v === "string")
w.prop(key, PROP_STRING, v);
}
}
if (Array.isArray(n.align))
w.align(n.align.map(alignCode));
if (n.data?._mdxExplicitJsx === true) {
w.bool(OF_EXPLICIT, true);
}
if (n.data != null)
w.data(n.data);
if (isRoot && forReplace && n._keepChildren === true) {
// Replace splices the target's original children, discarding any the
// replacement declares.
w.keepChildren();
}
else {
// `_keepChildren` only applies to replace; other ops ignore the marker
// and emit the declared children.
const children = n.children;
if (Array.isArray(children)) {
for (const c of children)
if (!emitMdastOp(w, c, false, forReplace))
return false;
}
}
w.close();
return true;
}
/** Map a table `align` entry to its arena code (none=0). */
function alignCode(a) {
return a === "left" ? 1 : a === "right" ? 2 : a === "center" ? 3 : 0;
}
/** True for the `{raw}` / `{rawHtml}` escape hatches — re-parsed by Rust rather
* than compiled to an op-stream, so they ride the RAW_MARKDOWN / RAW_HTML
* payloads instead of the declarative encoder. */
function isRawMdastContent(content) {
const c = content;
return typeof c.raw === "string" || typeof c.rawHtml === "string";
}
/** Encode `content` as the `op` structural command. Declarative nodes compile
* to the op-stream; the `{raw}`/`{rawHtml}` escape hatches ride the raw
* re-parse payloads. Anything that compiles to neither is a hard error — the
* op-stream is the only declarative encoding. The switches stay inline so the
* buffer calls are monomorphic (computed method names defeat inline caches on
* this warm path). */
function emitMdastTree(buffer, op, id, content, forReplace = false) {
if (isRawMdastContent(content)) {
switch (op) {
case "replace":
return buffer.replace(id, content);
case "insertBefore":
return buffer.insertBefore(id, content);
case "insertAfter":
return buffer.insertAfter(id, content);
case "prependChild":
return buffer.prependChild(id, content);
case "appendChild":
return buffer.appendChild(id, content);
case "wrapNode":
return buffer.wrapNode(id, content);
}
}
const ops = compileMdastToOpstream(content, forReplace);
if (ops === null)
throw unencodableContentError(content);
switch (op) {
case "replace":
return buffer.replaceOpstream(id, ops);
case "insertBefore":
return buffer.insertBeforeOpstream(id, ops);
case "insertAfter":
return buffer.insertAfterOpstream(id, ops);
case "prependChild":
return buffer.prependChildOpstream(id, ops);
case "appendChild":
return buffer.appendChildOpstream(id, ops);
case "wrapNode":
return buffer.wrapNodeOpstream(id, ops);
}
}
/** A result that is the same object as the input node is a no-op, so context
* mutations (e.g. setProperty) are not clobbered. */
function applyMdastVisitResult(result, nodeId, returnBuffer, originalNode) {

@@ -614,3 +530,3 @@ if (result === undefined || result === null)

case "structured_node":
returnBuffer.replace(nodeId, refifyReusedNodes(result, true));
emitMdastTree(returnBuffer, "replace", nodeId, result, true);
break;

@@ -626,7 +542,7 @@ }

*/
export function visitMdastHandle(handle, plugin, subs, source, fileURL) {
export function visitMdastHandle(handle, plugin, subs, source, fileURL, data = {}) {
const getSource = typeof source === "function" ? source : () => source;
const context = new MdastVisitorContext(handle, getSource, fileURL);
const resolver = new MdastLazyChildResolver(handle);
const context = new MdastVisitorContext(handle, getSource, fileURL, resolver, data);
const returnBuffer = new CommandBuffer();
const resolver = new MdastLazyChildResolver(handle);
const rustSubs = subs.map((s) => ({ nodeType: s.nodeType, tagFilter: [] }));

@@ -638,3 +554,3 @@ const matchBuf = walkMdastHandle(handle, rustSubs);

for (let i = 0; i < matchCount; i++) {
const indexBase = 4 + i * 12;
const indexBase = 4 + i * 10;
const nodeId = ru32(matchView, indexBase);

@@ -659,5 +575,10 @@ const subIndex = matchBuf[indexBase + 4];

}
// End of the pass — the caller applies the returned command buffer next,
// renumbering the arena, so later snapshots would resolve match-time
// child ids against wrong nodes. This is the last point we control.
resolver.seal();
return finalizeMdastVisit(handle, context, returnBuffer);
});
}
resolver.seal();
return finalizeMdastVisit(handle, context, returnBuffer);

@@ -664,0 +585,0 @@ }

import type { Position } from "unist";
import type { Literal as MdastLiteral, Nodes as MdastStdNodes } from "mdast";
import type { Nodes as HastStdNodes } from "hast";
import type { Literal as MdastLiteral, Nodes as MdastStdNodes, Parent as MdastParent, PhrasingContent } from "mdast";
import type { Literal as HastLiteral, Nodes as HastStdNodes } from "hast";
export type { Position, Point } from "unist";

@@ -19,2 +19,10 @@ export type { MdxJsxFlowElement, MdxJsxTextElement, MdxJsxAttribute as MdxJsxAttributeNode, MdxJsxExpressionAttribute as MdxJsxExpressionAttributeNode, MdxJsxAttributeValueExpression as MdxJsxAttributeValueExpressionNode, MdxFlowExpression, MdxTextExpression, MdxjsEsm, MdxJsxFlowElementHast, MdxJsxTextElementHast, MdxFlowExpressionHast, MdxTextExpressionHast, MdxjsEsmHast, } from "./mdx-types.js";

}
export interface Superscript extends MdastParent {
type: "superscript";
children: PhrasingContent[];
}
export interface Subscript extends MdastParent {
type: "subscript";
children: PhrasingContent[];
}
declare module "mdast" {

@@ -28,5 +36,9 @@ interface FrontmatterContentMap {

inlineMath: InlineMath;
superscript: Superscript;
subscript: Subscript;
}
interface PhrasingContentMap {
inlineMath: InlineMath;
superscript: Superscript;
subscript: Subscript;
}

@@ -37,5 +49,4 @@ interface BlockContentMap {

}
export interface HastRaw {
export interface HastRaw extends HastLiteral {
type: "raw";
value: string;
}

@@ -62,2 +73,21 @@ declare module "hast" {

export type HastNode = HastStdNodes;
/**
* Registry for typing well-known keys on the plugin data bag. Augment it to
* give specific `ctx.data` / `result.data` keys a type:
*
* ```ts
* declare module "satteri" {
* interface DataMap {
* toc: TocEntry[];
* }
* }
* ```
*/
export interface DataMap {
}
/**
* The document-level plugin data bag (`ctx.data` and `result.data`): any
* string key holding any value, plus the typed keys registered in {@link DataMap}.
*/
export type Data = Record<string, unknown> & Partial<DataMap>;
export interface StringRefRaw {

@@ -64,0 +94,0 @@ offset: number;

+52
-52

@@ -84,4 +84,4 @@ // prettier-ignore

const bindingPackageVersion = require('@bruits/satteri-android-arm64/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -101,4 +101,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-android-arm-eabi/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -123,4 +123,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-win32-x64-gnu/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -140,4 +140,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-win32-x64-msvc/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -158,4 +158,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-win32-ia32-msvc/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -175,4 +175,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-win32-arm64-msvc/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -195,4 +195,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-darwin-universal/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -212,4 +212,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-darwin-x64/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -229,4 +229,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-darwin-arm64/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -250,4 +250,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-freebsd-x64/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -267,4 +267,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-freebsd-arm64/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -289,4 +289,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-linux-x64-musl/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -306,4 +306,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-linux-x64-gnu/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -325,4 +325,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-linux-arm64-musl/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -342,4 +342,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-linux-arm64-gnu/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -361,4 +361,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-linux-arm-musleabihf/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -378,4 +378,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-linux-arm-gnueabihf/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -397,4 +397,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-linux-loong64-musl/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -414,4 +414,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-linux-loong64-gnu/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -433,4 +433,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-linux-riscv64-musl/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -450,4 +450,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-linux-riscv64-gnu/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -468,4 +468,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-linux-ppc64-gnu/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -485,4 +485,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-linux-s390x-gnu/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -506,4 +506,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-openharmony-arm64/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -523,4 +523,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-openharmony-x64/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -540,4 +540,4 @@ return binding

const bindingPackageVersion = require('@bruits/satteri-openharmony-arm/package.json').version
if (bindingPackageVersion !== '0.7.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.7.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
if (bindingPackageVersion !== '0.8.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.8.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}

@@ -544,0 +544,0 @@ return binding

{
"name": "satteri",
"version": "0.8.1",
"version": "0.9.0",
"description": "High-performance Markdown and MDX processing",

@@ -83,7 +83,7 @@ "repository": {

"optionalDependencies": {
"@bruits/satteri-linux-x64-gnu": "0.8.1",
"@bruits/satteri-darwin-x64": "0.8.1",
"@bruits/satteri-darwin-arm64": "0.8.1",
"@bruits/satteri-win32-x64-msvc": "0.8.1",
"@bruits/satteri-wasm32-wasi": "0.8.1"
"@bruits/satteri-linux-x64-gnu": "0.9.0",
"@bruits/satteri-darwin-x64": "0.9.0",
"@bruits/satteri-darwin-arm64": "0.9.0",
"@bruits/satteri-win32-x64-msvc": "0.9.0",
"@bruits/satteri-wasm32-wasi": "0.9.0"
},

@@ -90,0 +90,0 @@ "scripts": {

@@ -166,2 +166,37 @@ # satteri

### Sharing data between plugins
Each context exposes a `data` object, a document-scoped grab bag shared across every visitor in the compile. Writes from one plugin are visible to later plugins, and the bag persists across the mdast→hast boundary, so hast plugins can read what mdast plugins wrote. After compilation the final state is returned on `result.data`, making it suitable for things like extracting a table of contents.
```ts
const collectHeadings = defineMdastPlugin({
name: "collect-headings",
heading(node, ctx) {
const list = (ctx.data.headings as string[]) ?? [];
const first = node.children[0];
if (first && "value" in first) list.push(first.value as string);
ctx.data.headings = list;
},
});
const { html, data } = markdownToHtml("# A\n\n# B", { mdastPlugins: [collectHeadings] });
console.log(data?.headings); // ["A", "B"]
```
The bag lives entirely on the JS side, so any value is allowed, including functions, class instances, and `Map`/`Set`. References are preserved across plugins and across the mdast→hast boundary. A fresh, empty bag is created for every compile.
By default keys are typed as `unknown`. To give a key a type, augment the `DataMap` interface, much like `vfile`'s `DataMap`:
```ts
declare module "satteri" {
interface DataMap {
headings: string[];
}
}
// now ctx.data.headings and result.data.headings are typed as string[] | undefined
```
Unregistered keys stay `unknown`, so the bag remains open-ended.
### How transforms compose

@@ -168,0 +203,0 @@