satteri 0.9.4 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +11 -1
  2. package/dist/binding.browser.d.ts +1 -1
  3. package/dist/binding.browser.js +1 -1
  4. package/dist/binding.d.ts +1 -1
  5. package/dist/binding.js +1 -1
  6. package/dist/byte-writer.d.ts +4 -0
  7. package/dist/byte-writer.js +13 -4
  8. package/dist/child-stub.d.ts +7 -3
  9. package/dist/child-stub.js +18 -7
  10. package/dist/command-buffer.d.ts +36 -50
  11. package/dist/command-buffer.js +102 -49
  12. package/dist/compile.d.ts +80 -32
  13. package/dist/compile.js +295 -70
  14. package/dist/freeze.d.ts +2 -0
  15. package/dist/freeze.js +10 -0
  16. package/dist/generated/wire-constants.d.ts +3 -2
  17. package/dist/generated/wire-constants.js +4 -2
  18. package/dist/hast/child-stub.d.ts +2 -3
  19. package/dist/hast/child-stub.js +18 -11
  20. package/dist/hast/element-props.d.ts +1 -1
  21. package/dist/hast/element-props.js +14 -6
  22. package/dist/hast/hast-materializer.d.ts +6 -2
  23. package/dist/hast/hast-materializer.js +48 -134
  24. package/dist/hast/hast-reader.d.ts +7 -13
  25. package/dist/hast/hast-reader.js +76 -21
  26. package/dist/hast/hast-visitor.d.ts +55 -5
  27. package/dist/hast/hast-visitor.js +320 -154
  28. package/dist/index.d.ts +6 -6
  29. package/dist/index.js +1 -1
  30. package/dist/lazy-child-resolver.d.ts +20 -13
  31. package/dist/lazy-child-resolver.js +66 -38
  32. package/dist/materializer-cache.d.ts +42 -0
  33. package/dist/materializer-cache.js +192 -0
  34. package/dist/mdast/child-stub.d.ts +6 -3
  35. package/dist/mdast/child-stub.js +71 -8
  36. package/dist/mdast/generated/layout.d.ts +2 -3
  37. package/dist/mdast/generated/layout.js +24 -28
  38. package/dist/mdast/generated/node-types.d.ts +4 -0
  39. package/dist/mdast/generated/node-types.js +25 -0
  40. package/dist/mdast/mdast-materializer.d.ts +12 -3
  41. package/dist/mdast/mdast-materializer.js +83 -78
  42. package/dist/mdast/mdast-reader.d.ts +15 -0
  43. package/dist/mdast/mdast-reader.js +55 -10
  44. package/dist/mdast/mdast-visitor.d.ts +79 -32
  45. package/dist/mdast/mdast-visitor.js +305 -122
  46. package/dist/op-stream.d.ts +5 -12
  47. package/dist/op-stream.js +6 -21
  48. package/dist/plugin.d.ts +36 -10
  49. package/dist/plugin.js +50 -2
  50. package/dist/string-pool.d.ts +13 -0
  51. package/dist/string-pool.js +91 -0
  52. package/dist/types.d.ts +44 -1
  53. package/dist/visitor-shared.d.ts +27 -4
  54. package/dist/visitor-shared.js +36 -10
  55. package/dist/wire-read.js +3 -1
  56. package/index.d.ts +130 -7
  57. package/index.js +89 -68
  58. package/package.json +24 -19
  59. package/dist/lazy-props.d.ts +0 -10
  60. package/dist/lazy-props.js +0 -50
  61. package/satteri_napi.wasi-browser.js +0 -83
  62. package/satteri_napi.wasi.cjs +0 -134
  63. package/wasi-worker-browser.mjs +0 -36
  64. package/wasi-worker.mjs +0 -63
@@ -1,7 +1,7 @@
1
1
  /** Decode a HAST element property value from its wire `(kind, value)` — shared
2
2
  * by the walk decoder and the snapshot reader so the kind dispatch lives once.
3
3
  * The bool kinds carry no value string (callers pass `""`). */
4
- import { PROP_BOOL_TRUE, PROP_BOOL_FALSE, PROP_SPACE_SEP, PROP_COMMA_SEP, PROP_INT, } from "../generated/wire-constants.js";
4
+ import { PROP_BOOL_TRUE, PROP_BOOL_FALSE, PROP_SPACE_SEP, PROP_COMMA_SEP, PROP_COMMA_SEP_NUM, PROP_INT, } from "../generated/wire-constants.js";
5
5
  export function decodeElementProp(kind, value) {
6
6
  switch (kind) {
7
7
  case PROP_BOOL_TRUE:
@@ -10,11 +10,19 @@ export function decodeElementProp(kind, value) {
10
10
  return false;
11
11
  case PROP_SPACE_SEP:
12
12
  return value.split(" ").filter((s) => s.length > 0);
13
- case PROP_COMMA_SEP:
14
- return value
15
- .split(",")
16
- .map((s) => s.trim())
17
- .filter((s) => s.length > 0);
13
+ case PROP_COMMA_SEP: {
14
+ // Interior empty items are kept; only a trailing empty is dropped.
15
+ const items = value.split(",").map((s) => s.trim());
16
+ if (items[items.length - 1] === "")
17
+ items.pop();
18
+ return items;
19
+ }
20
+ case PROP_COMMA_SEP_NUM: {
21
+ const items = value.split(",").map((s) => s.trim());
22
+ if (items[items.length - 1] === "")
23
+ items.pop();
24
+ return items.map((s) => (s !== "" && !Number.isNaN(Number(s)) ? Number(s) : s));
25
+ }
18
26
  case PROP_INT:
19
27
  return Number(value);
20
28
  default:
@@ -2,10 +2,14 @@ import { HastReader } from "./hast-reader.js";
2
2
  import type { Root } from "hast";
3
3
  import type { HastNode } from "../types.js";
4
4
  export type { HastNode };
5
+ /** Container node types (the ones that carry `children`); everything else is a leaf. */
6
+ export declare const HAST_CONTAINER_TYPES: ReadonlySet<number>;
5
7
  /**
6
- * Materialize a single HAST node from a binary buffer as a lazy JS object.
8
+ * Materialize a single HAST node; scalars eager, `children` lazy, memoized per
9
+ * `(reader, id)`; `frozen` (the plugin walk path) deep-freezes so plugins
10
+ * cannot corrupt the shared cache.
7
11
  */
8
- export declare function materializeHastNode(reader: HastReader, nodeId: number): HastNode;
12
+ export declare const materializeHastNode: (reader: HastReader, nodeId: number, frozen?: boolean, refs?: import("../visitor-shared.js").NodeRefs) => import("hast").Nodes;
9
13
  /**
10
14
  * Materialize the full HAST tree from root (nodeId=0).
11
15
  */
@@ -1,143 +1,57 @@
1
- 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";
1
+ import { HastReader, HAST_ROOT, 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";
2
2
  import { TYPE_NAMES } from "./generated/node-types.js";
3
- import { lazyProp, lazyGroup } from "../lazy-props.js";
4
3
  import { restorePhantomSpaces } from "../phantom.js";
5
- function propsToRecord(props) {
6
- const result = {};
7
- for (const p of props) {
8
- result[p.name] = p.value;
9
- }
10
- return result;
11
- }
12
- /**
13
- * Materialize a single HAST node from a binary buffer as a lazy JS object.
14
- */
15
- export function materializeHastNode(reader, nodeId) {
16
- const nodeType = reader.getNodeType(nodeId);
17
- const typeName = TYPE_NAMES[nodeType] ?? `unknown(${nodeType})`;
18
- const node = { type: typeName };
19
- // Position decodes to three objects; defer it — passthrough children (e.g.
20
- // replaceNode keeping `node.children`) never read it.
21
- if (reader.hasPosition(nodeId)) {
22
- Object.defineProperty(node, "position", lazyProp("position", () => reader.getPosition(nodeId)));
23
- }
24
- // _nodeId: non-enumerable internal reference
25
- Object.defineProperty(node, "_nodeId", {
26
- value: nodeId,
27
- writable: false,
28
- configurable: true,
29
- enumerable: false,
30
- });
31
- switch (nodeType) {
32
- case HAST_ROOT:
33
- // children: lazy getter
34
- Object.defineProperty(node, "children", {
35
- get() {
36
- const childIds = reader.getChildIds(nodeId);
37
- const children = childIds.map((id) => materializeHastNode(reader, id));
38
- Object.defineProperty(this, "children", {
39
- value: children,
40
- writable: true,
41
- configurable: true,
42
- enumerable: true,
43
- });
44
- return children;
45
- },
46
- configurable: true,
47
- enumerable: true,
48
- });
49
- break;
50
- case HAST_ELEMENT: {
51
- // tagName and properties: lazy, resolved together from one reader call
52
- lazyGroup(node, ["tagName", "properties"], () => {
53
- const { tagName, properties } = reader.getElementData(nodeId);
54
- return { tagName, properties: propsToRecord(properties) };
55
- });
56
- // children: lazy
57
- Object.defineProperty(node, "children", {
58
- get() {
59
- const childIds = reader.getChildIds(nodeId);
60
- const children = childIds.map((id) => materializeHastNode(reader, id));
61
- Object.defineProperty(this, "children", {
62
- value: children,
63
- writable: true,
64
- configurable: true,
65
- enumerable: true,
66
- });
67
- return children;
68
- },
69
- configurable: true,
70
- enumerable: true,
71
- });
72
- break;
73
- }
74
- case HAST_TEXT:
75
- case HAST_COMMENT:
76
- case HAST_RAW:
77
- Object.defineProperties(node, {
78
- value: lazyProp("value", () => reader.getTextValue(nodeId)),
79
- });
80
- break;
81
- case HAST_DOCTYPE:
82
- // No extra properties
83
- break;
84
- case HAST_MDX_JSX_ELEMENT:
85
- case HAST_MDX_JSX_TEXT_ELEMENT:
86
- lazyGroup(node, ["name", "attributes"], () => reader.getMdxJsxElementData(nodeId));
87
- Object.defineProperty(node, "children", {
88
- get() {
89
- const childIds = reader.getChildIds(nodeId);
90
- const children = childIds.map((id) => materializeHastNode(reader, id));
91
- Object.defineProperty(this, "children", {
92
- value: children,
93
- writable: true,
94
- configurable: true,
95
- enumerable: true,
96
- });
97
- return children;
98
- },
99
- configurable: true,
100
- enumerable: true,
101
- });
102
- break;
103
- case HAST_MDX_FLOW_EXPRESSION:
104
- case HAST_MDX_TEXT_EXPRESSION:
105
- Object.defineProperties(node, {
106
- value: lazyProp("value", () => restorePhantomSpaces(reader.getTextValue(nodeId))),
107
- });
108
- break;
109
- case HAST_MDX_ESM:
110
- Object.defineProperties(node, {
111
- value: lazyProp("value", () => reader.getTextValue(nodeId)),
112
- });
113
- break;
114
- }
115
- // Plugins can set `data` on any node type, so rehydrate generically
116
- // (see website/content/docs/divergences.md for the code-block case).
117
- const rawData = reader.getNodeData(nodeId);
118
- if (rawData !== null) {
119
- try {
120
- const parsed = JSON.parse(rawData);
121
- if (parsed && typeof parsed === "object" && Object.keys(parsed).length > 0) {
122
- Object.defineProperty(node, "data", {
123
- value: parsed,
124
- writable: true,
125
- configurable: true,
126
- enumerable: true,
127
- });
4
+ import { createMaterializer } from "../materializer-cache.js";
5
+ /** Container node types (the ones that carry `children`); everything else is a leaf. */
6
+ export const HAST_CONTAINER_TYPES = new Set([
7
+ HAST_ROOT,
8
+ HAST_ELEMENT,
9
+ HAST_MDX_JSX_ELEMENT,
10
+ HAST_MDX_JSX_TEXT_ELEMENT,
11
+ ]);
12
+ const hastMaterializer = createMaterializer({
13
+ label: "materializeHastNode",
14
+ typeNames: TYPE_NAMES,
15
+ hasChildren: (nodeType) => HAST_CONTAINER_TYPES.has(nodeType),
16
+ populate(node, reader, nodeId, nodeType) {
17
+ switch (nodeType) {
18
+ case HAST_ELEMENT:
19
+ reader.readElementInto(nodeId, node);
20
+ break;
21
+ case HAST_TEXT:
22
+ case HAST_COMMENT:
23
+ case HAST_RAW:
24
+ node.value = reader.getTextValue(nodeId);
25
+ break;
26
+ case HAST_MDX_JSX_ELEMENT:
27
+ case HAST_MDX_JSX_TEXT_ELEMENT: {
28
+ const { name, attributes } = reader.getMdxJsxElementData(nodeId);
29
+ node.name = name;
30
+ node.attributes = attributes;
31
+ break;
128
32
  }
33
+ case HAST_MDX_FLOW_EXPRESSION:
34
+ case HAST_MDX_TEXT_EXPRESSION:
35
+ node.value = restorePhantomSpaces(reader.getTextValue(nodeId));
36
+ break;
37
+ case HAST_MDX_ESM:
38
+ node.value = reader.getTextValue(nodeId);
39
+ break;
40
+ // HAST_ROOT / HAST_DOCTYPE: no extra properties
41
+ default:
42
+ break;
129
43
  }
130
- catch (err) {
131
- if (process.env.NODE_ENV !== "production") {
132
- console.warn(`materializeHastNode: malformed node_data for nodeId=${nodeId}`, err);
133
- }
134
- }
135
- }
136
- return node;
137
- }
44
+ },
45
+ });
46
+ /**
47
+ * Materialize a single HAST node; scalars eager, `children` lazy, memoized per
48
+ * `(reader, id)`; `frozen` (the plugin walk path) deep-freezes so plugins
49
+ * cannot corrupt the shared cache.
50
+ */
51
+ export const materializeHastNode = hastMaterializer.node;
138
52
  /**
139
53
  * Materialize the full HAST tree from root (nodeId=0).
140
54
  */
141
55
  export function materializeHastTree(reader) {
142
- return materializeHastNode(reader, 0);
56
+ return hastMaterializer.tree(reader, 0);
143
57
  }
@@ -6,7 +6,6 @@ export declare const HAST_ROOT: number;
6
6
  export declare const HAST_ELEMENT: number;
7
7
  export declare const HAST_TEXT: number;
8
8
  export declare const HAST_COMMENT: number;
9
- export declare const HAST_DOCTYPE: number;
10
9
  export declare const HAST_RAW: number;
11
10
  export declare const HAST_MDX_JSX_ELEMENT: number;
12
11
  export declare const HAST_MDX_JSX_TEXT_ELEMENT: number;
@@ -15,7 +14,7 @@ export declare const HAST_MDX_ESM: number;
15
14
  export declare const HAST_MDX_TEXT_EXPRESSION: number;
16
15
  export interface HastProperty {
17
16
  name: string;
18
- value: string | number | boolean | string[];
17
+ value: string | number | boolean | (string | number)[];
19
18
  }
20
19
  export declare class HastReader {
21
20
  #private;
@@ -36,9 +35,6 @@ export declare class HastReader {
36
35
  getString(offset: number, len: number): string;
37
36
  /** Get position data for a node. */
38
37
  getPosition(nodeId: number): Position | undefined;
39
- /** Whether the node carries a source position (line 0 + offset 0 is the
40
- * synthesized-node sentinel), without decoding the three position objects. */
41
- hasPosition(nodeId: number): boolean;
42
38
  /** Get the node_type byte for a given node ID. */
43
39
  getNodeType(nodeId: number): number;
44
40
  /** Get the parent id for a given node (0xffffffff at the root). */
@@ -49,14 +45,12 @@ export declare class HastReader {
49
45
  pushChildIds(nodeId: number, stack: number[]): void;
50
46
  /** Get the raw type_data bytes for a node. */
51
47
  getTypeData(nodeId: number): Uint8Array;
52
- /**
53
- * Get element data for a HAST_ELEMENT node.
54
- *
55
- * Element type_data layout:
56
- * [tag_name: StringRef(8B)][prop_count: u32(4B)][_pad: u32(4B)] = 16-byte header
57
- * then prop_count * 20 bytes:
58
- * [name: StringRef(8B)][value_type: u8(1B)][_pad: [u8;3](3B)][value: StringRef(8B)]
59
- */
48
+ /** Element `tagName`, `properties` count, and the i-th property, read without
49
+ * building the intermediate views and records `getElementData` allocates. */
50
+ getElementTagName(nodeId: number): string;
51
+ getElementPropCount(nodeId: number): number;
52
+ getElementPropName(nodeId: number, index: number): string;
53
+ getElementPropValue(nodeId: number, index: number): HastProperty["value"];
60
54
  getElementData(nodeId: number): {
61
55
  tagName: string;
62
56
  properties: HastProperty[];
@@ -2,13 +2,13 @@ import { restorePhantomSpaces } from "../phantom.js";
2
2
  import { readPosition } from "../wire-read.js";
3
3
  import { MDX_ATTR_BOOLEAN_PROP, MDX_ATTR_LITERAL_PROP, MDX_ATTR_EXPRESSION_PROP, MDX_ATTR_SPREAD, } from "../op-stream.js";
4
4
  import { decodeElementProp } from "./element-props.js";
5
+ import { PoolOffsets } from "../string-pool.js";
5
6
  import { NAME_TO_TYPE } from "./generated/node-types.js";
6
7
  import { ARENA_MAGIC, KIND_HAST, FIELD, HEADER } from "../generated/arena-layout.js";
7
8
  export const HAST_ROOT = NAME_TO_TYPE.root;
8
9
  export const HAST_ELEMENT = NAME_TO_TYPE.element;
9
10
  export const HAST_TEXT = NAME_TO_TYPE.text;
10
11
  export const HAST_COMMENT = NAME_TO_TYPE.comment;
11
- export const HAST_DOCTYPE = NAME_TO_TYPE.doctype;
12
12
  export const HAST_RAW = NAME_TO_TYPE.raw;
13
13
  export const HAST_MDX_JSX_ELEMENT = NAME_TO_TYPE.mdxJsxFlowElement;
14
14
  export const HAST_MDX_JSX_TEXT_ELEMENT = NAME_TO_TYPE.mdxJsxTextElement;
@@ -20,6 +20,8 @@ export class HastReader {
20
20
  #header;
21
21
  #textDecoder;
22
22
  #stringPoolCache = null;
23
+ #poolChecked = false;
24
+ #poolOffsets = null;
23
25
  constructor(buffer) {
24
26
  if (buffer instanceof Uint8Array) {
25
27
  this.#view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
@@ -27,7 +29,7 @@ export class HastReader {
27
29
  else {
28
30
  this.#view = new DataView(buffer);
29
31
  }
30
- this.#textDecoder = new TextDecoder("utf-8");
32
+ this.#textDecoder = new TextDecoder("utf-8", { ignoreBOM: true });
31
33
  this.#header = this.#readHeader();
32
34
  }
33
35
  #readHeader() {
@@ -101,23 +103,27 @@ export class HastReader {
101
103
  getString(offset, len) {
102
104
  if (len === 0)
103
105
  return "";
104
- const { stringPoolOffset } = this.#header;
105
- const bytes = new Uint8Array(this.#view.buffer, this.#view.byteOffset + stringPoolOffset + offset, len);
106
- return this.#textDecoder.decode(bytes);
106
+ const pool = this.getStringPool();
107
+ if (!this.#poolChecked) {
108
+ this.#poolChecked = true;
109
+ const { stringPoolOffset, stringPoolLen } = this.#header;
110
+ const extraBytes = stringPoolLen - pool.length;
111
+ if (extraBytes > 0) {
112
+ const bytes = new Uint8Array(this.#view.buffer, this.#view.byteOffset + stringPoolOffset, stringPoolLen);
113
+ this.#poolOffsets = new PoolOffsets(bytes, extraBytes);
114
+ }
115
+ }
116
+ // An all-ASCII pool has byte offsets equal to its UTF-16 indices.
117
+ const offsets = this.#poolOffsets;
118
+ return offsets === null
119
+ ? pool.substring(offset, offset + len)
120
+ : offsets.slice(pool, offset, len);
107
121
  }
108
122
  /** Get position data for a node. */
109
123
  getPosition(nodeId) {
110
124
  const base = this.#header.nodesOffset + nodeId * this.#header.nodeStructSize;
111
125
  return readPosition(this.#view, base + FIELD.start_offset);
112
126
  }
113
- /** Whether the node carries a source position (line 0 + offset 0 is the
114
- * synthesized-node sentinel), without decoding the three position objects. */
115
- hasPosition(nodeId) {
116
- const base = this.#header.nodesOffset + nodeId * this.#header.nodeStructSize;
117
- const v = this.#view;
118
- return (v.getUint32(base + FIELD.start_line, true) !== 0 ||
119
- v.getUint32(base + FIELD.start_offset, true) !== 0);
120
- }
121
127
  /** Get the node_type byte for a given node ID. */
122
128
  getNodeType(nodeId) {
123
129
  const { nodesOffset, nodeStructSize } = this.#header;
@@ -168,10 +174,10 @@ export class HastReader {
168
174
  }
169
175
  /** Read a StringRef (offset: u32 LE, len: u32 LE) from a byte array at byteOffset. */
170
176
  #readStringRef(data, byteOffset) {
171
- const view = new DataView(data.buffer, data.byteOffset + byteOffset);
177
+ const at = data.byteOffset - this.#view.byteOffset + byteOffset;
172
178
  return {
173
- offset: view.getUint32(0, true),
174
- len: view.getUint32(4, true),
179
+ offset: this.#view.getUint32(at, true),
180
+ len: this.#view.getUint32(at + 4, true),
175
181
  };
176
182
  }
177
183
  /**
@@ -182,6 +188,58 @@ export class HastReader {
182
188
  * then prop_count * 20 bytes:
183
189
  * [name: StringRef(8B)][value_type: u8(1B)][_pad: [u8;3](3B)][value: StringRef(8B)]
184
190
  */
191
+ /** Byte position of a node's `type_data`, relative to this reader's view, or
192
+ * `-1` when the node stores fewer than `min` bytes. */
193
+ #typeDataAt(nodeId, min) {
194
+ const base = this.#header.nodesOffset + nodeId * this.#header.nodeStructSize;
195
+ const v = this.#view;
196
+ if (v.getUint32(base + FIELD.data_len, true) < min)
197
+ return -1;
198
+ return this.#header.typeDataOffset + v.getUint32(base + FIELD.data_offset, true);
199
+ }
200
+ #stringAt(at) {
201
+ return this.getString(this.#view.getUint32(at, true), this.#view.getUint32(at + 4, true));
202
+ }
203
+ /** @internal Both element fields in one pass; the per-field accessors below
204
+ * each re-resolve the node's `type_data` position. */
205
+ readElementInto(nodeId, node) {
206
+ const at = this.#typeDataAt(nodeId, 16);
207
+ if (at === -1) {
208
+ node.tagName = "";
209
+ node.properties = {};
210
+ return;
211
+ }
212
+ const v = this.#view;
213
+ node.tagName = this.#stringAt(at);
214
+ const count = v.getUint32(at + 8, true);
215
+ const properties = {};
216
+ for (let i = 0; i < count; i++) {
217
+ const base = at + 16 + i * 20;
218
+ properties[this.#stringAt(base)] = decodeElementProp(v.getUint8(base + 8), this.#stringAt(base + 12));
219
+ }
220
+ node.properties = properties;
221
+ }
222
+ /** Element `tagName`, `properties` count, and the i-th property, read without
223
+ * building the intermediate views and records `getElementData` allocates. */
224
+ getElementTagName(nodeId) {
225
+ const at = this.#typeDataAt(nodeId, 16);
226
+ return at === -1 ? "" : this.#stringAt(at);
227
+ }
228
+ getElementPropCount(nodeId) {
229
+ const at = this.#typeDataAt(nodeId, 16);
230
+ return at === -1 ? 0 : this.#view.getUint32(at + 8, true);
231
+ }
232
+ getElementPropName(nodeId, index) {
233
+ const at = this.#typeDataAt(nodeId, 16);
234
+ return at === -1 ? "" : this.#stringAt(at + 16 + index * 20);
235
+ }
236
+ getElementPropValue(nodeId, index) {
237
+ const at = this.#typeDataAt(nodeId, 16);
238
+ if (at === -1)
239
+ return "";
240
+ const base = at + 16 + index * 20;
241
+ return decodeElementProp(this.#view.getUint8(base + 8), this.#stringAt(base + 12));
242
+ }
185
243
  getElementData(nodeId) {
186
244
  const data = this.getTypeData(nodeId);
187
245
  if (data.length < 16) {
@@ -266,10 +324,7 @@ export class HastReader {
266
324
  * These store a single StringRef (8 bytes) as their type_data.
267
325
  */
268
326
  getTextValue(nodeId) {
269
- const data = this.getTypeData(nodeId);
270
- if (data.length < 8)
271
- return "";
272
- const ref = this.#readStringRef(data, 0);
273
- return this.getString(ref.offset, ref.len);
327
+ const at = this.#typeDataAt(nodeId, 8);
328
+ return at === -1 ? "" : this.#stringAt(at);
274
329
  }
275
330
  }
@@ -1,10 +1,11 @@
1
1
  import { type HastNode } from "./hast-materializer.js";
2
- import type { HastRaw, Data } from "../types.js";
2
+ import type { HastRaw, Data, SourceFormat } from "../types.js";
3
3
  import type { Element, Text, Comment, Doctype, Parents as HastParents, Root as HastRoot } from "hast";
4
4
  import type { Program } from "estree-jsx";
5
5
  import type { MdxJsxFlowElementHast, MdxJsxTextElementHast } from "../mdx-types.js";
6
6
  import type { MdxFlowExpressionHast, MdxTextExpressionHast } from "../mdx-types.js";
7
7
  import type { MdxjsEsmHast } from "../mdx-types.js";
8
+ import { type PluginOptions } from "../visitor-shared.js";
8
9
  import type { HastHandle } from "../handles.js";
9
10
  export type { HastHandle };
10
11
  /** ESTree-compatible Program node returned by `parseExpression()`. */
@@ -30,16 +31,30 @@ export interface HastVisitorContext {
30
31
  * instances. Returned to the caller as `result.data`.
31
32
  */
32
33
  readonly data: Data;
34
+ /**
35
+ * The source format this compile is processing: `"markdown"` for a plain
36
+ * Markdown compile, `"mdx"` for an MDX one. Lets a plugin shared between both
37
+ * pipelines branch on which it is handling.
38
+ */
39
+ readonly sourceFormat: SourceFormat;
33
40
  removeNode(node: Readonly<HastNode>): void;
34
- replaceNode(node: Readonly<HastNode>, newNode: HastContent): void;
41
+ /**
42
+ * Swap `node` for one node, or for an array of nodes placed in order at its
43
+ * position. An empty array drops the node, the same as `removeNode`.
44
+ * The document root takes a `root` and nothing else: the one place a `root`
45
+ * is accepted as content.
46
+ */
47
+ replaceNode(node: Readonly<HastNode>, newNode: HastContent | HastContent[]): void;
35
48
  insertBefore(node: Readonly<HastNode>, newNode: HastContent | HastContent[]): void;
36
49
  insertAfter(node: Readonly<HastNode>, newNode: HastContent | HastContent[]): void;
37
50
  /**
38
51
  * Wrap `node` in `parentNode`, making it `parentNode`'s first child. Any
39
52
  * children `parentNode` declares are kept after it, so a `div` with an anchor
40
- * child wraps a heading as `div > [heading, anchor]`.
53
+ * child wraps a heading as `div > [heading, anchor]`. `parentNode` is an
54
+ * element, an MDX JSX element, or `{ raw }` HTML parsing to exactly one
55
+ * element, never a void element, whose children would not render.
41
56
  */
42
- wrapNode(node: Readonly<HastNode>, parentNode: HastContent): void;
57
+ wrapNode(node: Readonly<HastNode>, parentNode: HastParentContent | RawHastContent | RawHtmlHastContent): void;
43
58
  prependChild(node: Readonly<HastNode>, childNode: HastContent | HastContent[]): void;
44
59
  appendChild(node: Readonly<HastNode>, childNode: HastContent | HastContent[]): void;
45
60
  /** Insert one node or an array at `index`; clamps (`0` or less prepends, past the end appends). */
@@ -71,13 +86,37 @@ export interface HastVisitorContext {
71
86
  /** New content for a HAST structural mutation. Unlike [`MdastContent`], HAST has
72
87
  * a `raw` node type, so it needs no raw/rawHtml escape hatch. */
73
88
  export type HastContent = HastNode;
89
+ /** A `wrapNode` wrapper: node types that can hold children. */
90
+ export type HastParentContent = Exclude<Extract<HastNode, {
91
+ children: unknown[];
92
+ }>, HastRoot>;
93
+ /** Raw `wrapNode` wrapper: the HTML is parsed at apply time (not call time)
94
+ * and must yield exactly one non-void element, which becomes the wrapper.
95
+ * `mdxExpressions` is accepted for parity with the MDAST phase and has no
96
+ * effect: braces in HTML text are always literal. */
97
+ export interface RawHastContent {
98
+ raw: string;
99
+ mdxExpressions?: boolean;
100
+ }
101
+ export interface RawHtmlHastContent {
102
+ /** @deprecated Use the equivalent `{ raw }`. */
103
+ rawHtml: string;
104
+ }
74
105
  /** A filtered visitor: Rust filters by tag/component name, only matched nodes cross the boundary. */
75
106
  export interface HastFilteredVisitor<N extends HastNode = HastNode> {
76
107
  filter: string[];
77
108
  visit(node: Readonly<N>, ctx: HastVisitorContext): HastNode | void | Promise<HastNode | void>;
78
109
  }
79
110
  type HastVisitorFn<N extends HastNode = HastNode> = (node: Readonly<N>, ctx: HastVisitorContext) => HastNode | void | Promise<HastNode | void>;
111
+ export type HastHookFn = (root: Readonly<HastRoot>, ctx: HastVisitorContext) => void | Promise<void>;
80
112
  export interface HastVisitorInstance {
113
+ /** Plugin-level configuration (e.g. `{ position: true }` to read positions). */
114
+ options?: PluginOptions;
115
+ /** Runs once per document, before the plugin's visitors. Awaited when async. */
116
+ before?: HastHookFn;
117
+ /** Runs once per document, after the plugin's visitors have settled. Awaited
118
+ * when async. */
119
+ after?: HastHookFn;
81
120
  element?: HastFilteredVisitor<Element> | HastFilteredVisitor<Element>[];
82
121
  mdxJsxFlowElement?: HastFilteredVisitor<MdxJsxFlowElementHast> | HastFilteredVisitor<MdxJsxFlowElementHast>[];
83
122
  mdxJsxTextElement?: HastFilteredVisitor<MdxJsxTextElementHast> | HastFilteredVisitor<MdxJsxTextElementHast>[];
@@ -109,4 +148,15 @@ export declare function resolveSubscriptions(plugin: HastVisitorInstance): Resol
109
148
  * replaced earlier in the same pass (the caller warns when non-zero), or a
110
149
  * Promise of that count if any visitor is async.
111
150
  */
112
- export declare function visitHastHandle(handle: HastHandle, plugin: HastVisitorInstance, subs: ResolvedSubscription[], source: string | (() => string), fileURL: URL | undefined, data?: Data): number | Promise<number>;
151
+ export declare function visitHastHandle(handle: HastHandle, plugin: HastVisitorInstance, subs: ResolvedSubscription[], source: string | (() => string), fileURL: URL | undefined, data?: Data, sourceFormat?: SourceFormat, diagnostics?: HastDiagnostic[]): number | Promise<number>;
152
+ /** Run a HAST visitor, build the command buffer, but do NOT apply it. Returns
153
+ * the merged commands so the caller can choose how to dispatch: either via
154
+ * `applyCommandsToHandle` (intermediate plugins in a chain) or via a fused
155
+ * NAPI call like `applyCommandsAndRenderHandle` (final plugin, saves one
156
+ * apply + one render + one drop crossing). Empty result means no mutations.
157
+ */
158
+ export declare function visitHastHandleCollect(handle: HastHandle, plugin: HastVisitorInstance, subs: ResolvedSubscription[], source: string | (() => string), fileURL: URL | undefined, data?: Data, sourceFormat?: SourceFormat, diagnostics?: HastDiagnostic[]): Uint8Array | Promise<Uint8Array>;
159
+ /** Its own pass so the caller can apply what `before` queued before the
160
+ * visitors walk, and the visitors' mutations before `after` reads the tree. */
161
+ export declare function visitHastHookCollect(handle: HastHandle, plugin: HastVisitorInstance, hook: HastHookFn, source: string | (() => string), fileURL: URL | undefined, data?: Data, sourceFormat?: SourceFormat, diagnostics?: HastDiagnostic[]): Uint8Array | Promise<Uint8Array>;
162
+ export declare function visitHastHook(handle: HastHandle, plugin: HastVisitorInstance, hook: HastHookFn, source: string | (() => string), fileURL: URL | undefined, data?: Data, sourceFormat?: SourceFormat, diagnostics?: HastDiagnostic[]): number | Promise<number>;