| /** | ||
| * 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 {}; |
+64
-132
@@ -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; |
+40
-26
@@ -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[]; |
+48
-115
@@ -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>; |
+392
-299
| 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 @@ } |
+10
-4
@@ -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; |
+23
-1
@@ -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 @@ */ |
+45
-272
@@ -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>; |
+347
-426
@@ -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 @@ } |
+34
-4
| 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 |
+6
-6
| { | ||
| "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": { |
+35
-0
@@ -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 @@ |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 3 instances
305222
28.44%79
83.72%6882
23.75%352
11.04%81
1.25%