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,6 @@
1
1
  // @generated by `cargo run -p satteri-layout-codegen`. Do not edit by hand.
2
2
  import { ru16, ru32, rstr } from "../../wire-read.js";
3
3
  import { restorePhantomSpaces } from "../../phantom.js";
4
- import { lazyGroup } from "../../lazy-props.js";
5
4
  import { decodeMdxJsxAttr } from "../../mdx-attr.js";
6
5
  import { decodeColumnAlign } from "../column-align.js";
7
6
  /** Walk-wire fields per node-type tag (see Rust `write_mdast_type_data_inline`). */
@@ -57,13 +56,16 @@ const MDAST_LAYOUTS = {
57
56
  { js: "value", offset: 8, kind: "str32" },
58
57
  ],
59
58
  28: [{ js: "value", offset: 8, kind: "str32" }],
59
+ 38: [
60
+ { js: "name", offset: 0, kind: "str16" },
61
+ { js: "value", offset: 8, kind: "str32" },
62
+ ],
60
63
  102: [{ js: "value", offset: 0, kind: "str32", phantom: true }],
61
64
  103: [{ js: "value", offset: 0, kind: "str32", phantom: true }],
62
65
  104: [{ js: "value", offset: 0, kind: "str32", phantom: true }],
63
66
  };
64
67
  /** Materialized property names per tag (the non-skip `js` names above),
65
- * precomputed so `materializeMdastFields` doesn't rebuild them per node.
66
- * Exported for the child-stub field tables. */
68
+ * exported for the child-stub field tables. */
67
69
  export const MDAST_LAYOUT_KEYS = {
68
70
  2: ["depth"],
69
71
  7: ["value"],
@@ -81,6 +83,7 @@ export const MDAST_LAYOUT_KEYS = {
81
83
  20: ["identifier", "label"],
82
84
  27: ["meta", "value"],
83
85
  28: ["value"],
86
+ 38: ["name", "value"],
84
87
  102: ["value"],
85
88
  103: ["value"],
86
89
  104: ["value"],
@@ -237,36 +240,29 @@ export function decodeMdastTypeData(view, buf, start, nodeType, node) {
237
240
  * Attach a node's fixed `type_data` fields, materialized from the arena
238
241
  * snapshot, driven by `MDAST_LAYOUTS`. Returns `false` for tags with no entry,
239
242
  * so the materializer falls through to its hand-written cases (list, table,
240
- * directives, MDX JSX). Fields resolve lazily on first access.
243
+ * directives, MDX JSX). Fields are eager plain stores.
241
244
  */
242
245
  export function materializeMdastFields(reader, node, nodeId, nodeType) {
243
246
  const fields = MDAST_LAYOUTS[nodeType];
244
247
  if (fields === undefined)
245
248
  return false;
246
- lazyGroup(node, MDAST_LAYOUT_KEYS[nodeType], () => {
247
- const data = reader.getTypeData(nodeId);
248
- const out = {};
249
- for (const f of fields) {
250
- if (f.kind === "u8") {
251
- // Short type_data falls back to the layout's declared default,
252
- // matching the Rust walk serializer.
253
- const b = data[f.offset] ?? f.default ?? 0;
254
- if (!f.skip)
255
- out[f.js] = f.values ? (f.values[b] ?? f.values[0]) : b;
256
- }
257
- else {
258
- // Short type_data (e.g. a 20-byte ReferenceData-only imageReference)
259
- // yields an empty ref, mirroring the Rust decoders' bounds checks.
260
- const ref = f.offset + 8 <= data.length
261
- ? reader.readStringRef(data, f.offset)
262
- : { offset: 0, len: 0 };
263
- if (f.skip)
264
- continue;
265
- const s = f.nullable && ref.len === 0 ? null : reader.getString(ref.offset, ref.len);
266
- out[f.js] = f.phantom && typeof s === "string" ? restorePhantomSpaces(s) : s;
267
- }
249
+ const out = node;
250
+ for (const f of fields) {
251
+ if (f.skip)
252
+ continue;
253
+ if (f.kind === "u8") {
254
+ // Short type_data falls back to the layout's declared default,
255
+ // matching the Rust walk serializer.
256
+ const b = reader.fieldU8(nodeId, f.offset, f.default ?? 0);
257
+ out[f.js] = f.values ? (f.values[b] ?? f.values[0]) : b;
268
258
  }
269
- return out;
270
- });
259
+ else {
260
+ // Short type_data (e.g. a 20-byte ReferenceData-only imageReference)
261
+ // reads as empty, mirroring the Rust decoders' bounds checks.
262
+ const raw = reader.fieldString(nodeId, f.offset);
263
+ const s = f.nullable && raw === "" ? null : raw;
264
+ out[f.js] = f.phantom && typeof s === "string" ? restorePhantomSpaces(s) : s;
265
+ }
266
+ }
271
267
  return true;
272
268
  }
@@ -38,6 +38,10 @@ export declare const NodeType: Readonly<{
38
38
  readonly TextDirective: 32;
39
39
  readonly Superscript: 33;
40
40
  readonly Subscript: 34;
41
+ readonly DescriptionList: 35;
42
+ readonly DescriptionTerm: 36;
43
+ readonly DescriptionDetails: 37;
44
+ readonly Custom: 38;
41
45
  readonly MdxJsxFlowElement: 100;
42
46
  readonly MdxJsxTextElement: 101;
43
47
  readonly MdxFlowExpression: 102;
@@ -35,6 +35,10 @@ export const TYPE_NAMES = {
35
35
  32: "textDirective",
36
36
  33: "superscript",
37
37
  34: "subscript",
38
+ 35: "descriptionList",
39
+ 36: "descriptionTerm",
40
+ 37: "descriptionDetails",
41
+ 38: "custom",
38
42
  100: "mdxJsxFlowElement",
39
43
  101: "mdxJsxTextElement",
40
44
  102: "mdxFlowExpression",
@@ -77,6 +81,10 @@ export const NAME_TO_TYPE = {
77
81
  textDirective: 32,
78
82
  superscript: 33,
79
83
  subscript: 34,
84
+ descriptionList: 35,
85
+ descriptionTerm: 36,
86
+ descriptionDetails: 37,
87
+ custom: 38,
80
88
  mdxJsxFlowElement: 100,
81
89
  mdxJsxTextElement: 101,
82
90
  mdxFlowExpression: 102,
@@ -119,6 +127,10 @@ export const NodeType = Object.freeze({
119
127
  TextDirective: 32,
120
128
  Superscript: 33,
121
129
  Subscript: 34,
130
+ DescriptionList: 35,
131
+ DescriptionTerm: 36,
132
+ DescriptionDetails: 37,
133
+ Custom: 38,
122
134
  MdxJsxFlowElement: 100,
123
135
  MdxJsxTextElement: 101,
124
136
  MdxFlowExpression: 102,
@@ -161,6 +173,10 @@ export const NodeTypeName = {
161
173
  32: "TextDirective",
162
174
  33: "Superscript",
163
175
  34: "Subscript",
176
+ 35: "DescriptionList",
177
+ 36: "DescriptionTerm",
178
+ 37: "DescriptionDetails",
179
+ 38: "Custom",
164
180
  100: "MdxJsxFlowElement",
165
181
  101: "MdxJsxTextElement",
166
182
  102: "MdxFlowExpression",
@@ -202,6 +218,10 @@ export const VISITOR_KEYS = new Set([
202
218
  "textDirective",
203
219
  "superscript",
204
220
  "subscript",
221
+ "descriptionList",
222
+ "descriptionTerm",
223
+ "descriptionDetails",
224
+ "custom",
205
225
  "mdxJsxFlowElement",
206
226
  "mdxJsxTextElement",
207
227
  "mdxFlowExpression",
@@ -245,6 +265,10 @@ export const MDAST_OPSTREAM_TYPES = {
245
265
  textDirective: 32,
246
266
  superscript: 33,
247
267
  subscript: 34,
268
+ descriptionList: 35,
269
+ descriptionTerm: 36,
270
+ descriptionDetails: 37,
271
+ custom: 38,
248
272
  mdxJsxFlowElement: 100,
249
273
  mdxJsxTextElement: 101,
250
274
  mdxFlowExpression: 102,
@@ -261,6 +285,7 @@ export const MDAST_CUSTOM_TYPES = [
261
285
  "containerDirective",
262
286
  "leafDirective",
263
287
  "textDirective",
288
+ "descriptionDetails",
264
289
  "mdxJsxFlowElement",
265
290
  "mdxJsxTextElement",
266
291
  ];
@@ -1,10 +1,19 @@
1
1
  import type { Root } from "mdast";
2
- import type { MdastNode } from "../types.js";
3
2
  import type { MdastReader } from "./mdast-reader.js";
4
3
  export declare const LEAF_TYPES: ReadonlySet<number>;
4
+ /** A `custom` node is a leaf when it has a non-empty `value` and no children or
5
+ * `data.h*`. Leafness is per node there, not per type, so the read paths ask
6
+ * this instead of {@link LEAF_TYPES}.
7
+ * @see {@link Custom} */
8
+ export declare function isCustomLeaf(node: {
9
+ readonly value?: unknown;
10
+ readonly data?: unknown;
11
+ }, childCount: number): boolean;
5
12
  /**
6
- * Materialize a single MDAST node from a binary buffer as a lazy JS object.
13
+ * Materialize a single MDAST node; scalars eager, `children` lazy, memoized
14
+ * per `(reader, id)`; `frozen` (the plugin walk path) deep-freezes so plugins
15
+ * cannot corrupt the shared cache.
7
16
  */
8
- export declare function materializeNode(reader: MdastReader, nodeId: number): MdastNode;
17
+ export declare const materializeNode: (reader: MdastReader, nodeId: number, frozen?: boolean, refs?: import("../visitor-shared.js").NodeRefs) => import("mdast").Nodes;
9
18
  /** Materialize the full tree from root (nodeId=0). */
10
19
  export declare function materializeMdastTree(reader: MdastReader): Root;
@@ -1,47 +1,99 @@
1
- import { lazyProp, lazyGroup } from "../lazy-props.js";
2
- import { TYPE_NAMES } from "./generated/node-types.js";
1
+ import { NAME_TO_TYPE, TYPE_NAMES } from "./generated/node-types.js";
3
2
  import { materializeMdastFields } from "./generated/layout.js";
3
+ import { createMaterializer } from "../materializer-cache.js";
4
+ /** Internal tag for user-defined nodes; its stored `name` field carries the
5
+ * author's public `type` string. */
6
+ const MDAST_CUSTOM = NAME_TO_TYPE.custom;
4
7
  // Leaf node types that do NOT have children.
5
8
  // Type 9 = `definition`; type 18 = `imageReference` — leaves per mdast spec
6
9
  // (imageReference carries `alt` as a string, not children).
7
10
  export const LEAF_TYPES = new Set([
8
11
  9, 10, 13, 7, 8, 14, 3, 16, 18, 20, 25, 26, 27, 28, 102, 103, 104,
9
12
  ]);
13
+ /** A `custom` node is a leaf when it has a non-empty `value` and no children or
14
+ * `data.h*`. Leafness is per node there, not per type, so the read paths ask
15
+ * this instead of {@link LEAF_TYPES}.
16
+ * @see {@link Custom} */
17
+ export function isCustomLeaf(node, childCount) {
18
+ const { value, data } = node;
19
+ if (childCount !== 0 || typeof value !== "string" || value === "")
20
+ return false;
21
+ if (data === null || typeof data !== "object")
22
+ return true;
23
+ if ("hName" in data && typeof data.hName === "string")
24
+ return false;
25
+ if ("hChildren" in data && Array.isArray(data.hChildren))
26
+ return false;
27
+ if (!("hProperties" in data))
28
+ return true;
29
+ const props = data.hProperties;
30
+ return props === null || typeof props !== "object" || Array.isArray(props);
31
+ }
10
32
  /**
11
- * Add type-specific lazy properties to a node object.
33
+ * Add type-specific properties to a node object as eager plain stores.
12
34
  */
13
35
  function addTypeProperties(node, reader, nodeId, nodeType) {
14
36
  // Fixed-field types materialize from the generated layout table; the rest
15
37
  // (variable-length / cross-field) stay in the hand-written switch.
16
- if (materializeMdastFields(reader, node, nodeId, nodeType))
38
+ if (materializeMdastFields(reader, node, nodeId, nodeType)) {
39
+ // User-defined node: surface the stored `name` as the open `node.type`.
40
+ // Drop an empty `value` so a parent node isn't given a spurious leaf field.
41
+ if (nodeType === MDAST_CUSTOM) {
42
+ const n = node;
43
+ if (n.name !== undefined)
44
+ n.type = n.name;
45
+ delete n.name;
46
+ if (n.value === "")
47
+ delete n.value;
48
+ }
17
49
  return;
50
+ }
18
51
  switch (nodeType) {
19
52
  case 5: {
20
53
  // list
21
- const resolveList = () => {
22
- const d = reader.getListData(nodeId);
23
- return { ordered: d.ordered, start: d.ordered ? d.start : null, spread: d.spread };
24
- };
25
- lazyGroup(node, ["ordered", "start", "spread"], resolveList);
54
+ const d = reader.getListData(nodeId);
55
+ const n = node;
56
+ n.ordered = d.ordered;
57
+ n.start = d.ordered ? d.start : null;
58
+ n.spread = d.spread;
26
59
  break;
27
60
  }
28
- case 6: // listItem
29
- lazyGroup(node, ["spread", "checked"], () => reader.getListItemData(nodeId));
61
+ case 6: {
62
+ // listItem
63
+ const d = reader.getListItemData(nodeId);
64
+ const n = node;
65
+ n.spread = d.spread;
66
+ n.checked = d.checked;
30
67
  break;
68
+ }
69
+ case 37: {
70
+ // descriptionDetails
71
+ const d = reader.getDescriptionDetailsData(nodeId);
72
+ node.spread = d.spread;
73
+ break;
74
+ }
31
75
  case 21: // table
32
- Object.defineProperties(node, {
33
- align: lazyProp("align", () => reader.getTableAlign(nodeId)),
34
- });
76
+ node.align = reader.getTableAlign(nodeId);
35
77
  break;
36
78
  case 30: // containerDirective
37
79
  case 31: // leafDirective
38
- case 32: // textDirective
39
- lazyGroup(node, ["name", "attributes"], () => reader.getDirectiveData(nodeId));
80
+ case 32: {
81
+ // textDirective
82
+ const d = reader.getDirectiveData(nodeId);
83
+ const n = node;
84
+ n.name = d.name;
85
+ n.attributes = d.attributes;
40
86
  break;
87
+ }
41
88
  case 100: // mdxJsxFlowElement
42
- case 101: // mdxJsxTextElement
43
- lazyGroup(node, ["name", "attributes"], () => reader.getMdxJsxElementData(nodeId));
89
+ case 101: {
90
+ // mdxJsxTextElement
91
+ const d = reader.getMdxJsxElementData(nodeId);
92
+ const n = node;
93
+ n.name = d.name;
94
+ n.attributes = d.attributes;
44
95
  break;
96
+ }
45
97
  // Nodes with no type-specific props:
46
98
  // root(0), paragraph(1), thematicBreak(3), blockquote(4),
47
99
  // emphasis(11), strong(12), break(14), tableRow(22), tableCell(23), delete(24)
@@ -49,68 +101,21 @@ function addTypeProperties(node, reader, nodeId, nodeType) {
49
101
  break;
50
102
  }
51
103
  }
104
+ const mdastMaterializer = createMaterializer({
105
+ label: "materializeNode",
106
+ typeNames: TYPE_NAMES,
107
+ hasChildren: (nodeType, node, reader, nodeId) => nodeType === MDAST_CUSTOM
108
+ ? !isCustomLeaf(node, reader.getChildIds(nodeId).length)
109
+ : !LEAF_TYPES.has(nodeType),
110
+ populate: addTypeProperties,
111
+ });
52
112
  /**
53
- * Materialize a single MDAST node from a binary buffer as a lazy JS object.
113
+ * Materialize a single MDAST node; scalars eager, `children` lazy, memoized
114
+ * per `(reader, id)`; `frozen` (the plugin walk path) deep-freezes so plugins
115
+ * cannot corrupt the shared cache.
54
116
  */
55
- export function materializeNode(reader, nodeId) {
56
- const rawNode = reader.getNode(nodeId);
57
- const nodeType = rawNode.type;
58
- const typeName = TYPE_NAMES[nodeType] ?? `unknown(${nodeType})`;
59
- const node = { type: typeName };
60
- if (rawNode.position !== undefined) {
61
- node.position = rawNode.position;
62
- }
63
- // _nodeId: non-enumerable internal reference
64
- Object.defineProperty(node, "_nodeId", {
65
- value: nodeId,
66
- writable: false,
67
- configurable: true,
68
- enumerable: false,
69
- });
70
- // Type-specific lazy properties
71
- addTypeProperties(node, reader, nodeId, nodeType);
72
- // Plugin-set `data` survives the visitor walk via its own getter but
73
- // would be dropped when materialized from a serialized handle.
74
- const rawData = reader.getNodeData(nodeId);
75
- if (rawData !== null) {
76
- try {
77
- const parsed = JSON.parse(rawData);
78
- if (parsed && typeof parsed === "object" && Object.keys(parsed).length > 0) {
79
- Object.defineProperty(node, "data", {
80
- value: parsed,
81
- writable: true,
82
- configurable: true,
83
- enumerable: true,
84
- });
85
- }
86
- }
87
- catch (err) {
88
- if (process.env.NODE_ENV !== "production") {
89
- console.warn(`materializeNode: malformed node_data for nodeId=${nodeId}`, err);
90
- }
91
- }
92
- }
93
- // children: lazy getter (only for non-leaf nodes)
94
- if (!LEAF_TYPES.has(nodeType)) {
95
- Object.defineProperty(node, "children", {
96
- get() {
97
- const childIds = reader.getChildIds(nodeId);
98
- const children = childIds.map((id) => materializeNode(reader, id));
99
- Object.defineProperty(this, "children", {
100
- value: children,
101
- writable: true,
102
- configurable: true,
103
- enumerable: true,
104
- });
105
- return children;
106
- },
107
- configurable: true,
108
- enumerable: true,
109
- });
110
- }
111
- return node;
112
- }
117
+ export const materializeNode = mdastMaterializer.node;
113
118
  /** Materialize the full tree from root (nodeId=0). */
114
119
  export function materializeMdastTree(reader) {
115
- return materializeNode(reader, 0);
120
+ return mdastMaterializer.tree(reader, 0);
116
121
  }
@@ -1,4 +1,5 @@
1
1
  import type { MdastNodeRaw, BufferHeader, StringRefRaw, MdxJsxAttributeUnion } from "../types.js";
2
+ import { readPosition } from "../wire-read.js";
2
3
  export { NodeType, NodeTypeName } from "./generated/node-types.js";
3
4
  export declare class MdastReader {
4
5
  #private;
@@ -16,11 +17,19 @@ export declare class MdastReader {
16
17
  getNode(nodeId: number): MdastNodeRaw;
17
18
  /** Fast path: read only the type byte for a node. */
18
19
  getNodeType(nodeId: number): number;
20
+ getPosition(nodeId: number): ReturnType<typeof readPosition>;
19
21
  /** Fast path: read only the parent id for a node (0xffffffff at the root). */
20
22
  getParentId(nodeId: number): number;
21
23
  getChildIds(nodeId: number): number[];
22
24
  /** Push child node IDs directly onto a stack array (reverse order for depth-first). */
23
25
  pushChildIds(nodeId: number, stack: number[]): void;
26
+ /** Fixed-layout field reads that touch the buffer directly. The generated
27
+ * decoder runs once per node, so an intermediate view or ref object here
28
+ * costs an allocation per field. */
29
+ fieldU8(nodeId: number, offset: number, fallback: number): number;
30
+ /** `""` when the field is absent or empty; callers map that to `null` for
31
+ * nullable fields, matching the Rust decoders' bounds checks. */
32
+ fieldString(nodeId: number, offset: number): string;
24
33
  getTypeData(nodeId: number): Uint8Array;
25
34
  /** Read a StringRef (offset: u32 LE, len: u32 LE) from type data. */
26
35
  readStringRef(typeData: Uint8Array, byteOffset?: number): StringRefRaw;
@@ -46,6 +55,12 @@ export declare class MdastReader {
46
55
  checked: boolean | null;
47
56
  spread: boolean;
48
57
  };
58
+ /**
59
+ * DescriptionDetailsData #[repr(C)]: spread(0). Valid for descriptionDetails.
60
+ */
61
+ getDescriptionDetailsData(nodeId: number): {
62
+ spread: boolean;
63
+ };
49
64
  /**
50
65
  * TableData #[repr(C)]: align_count(0..4), then align_count bytes.
51
66
  * Alignment bytes: 0=none, 1=left, 2=right, 3=center.
@@ -1,6 +1,7 @@
1
1
  import { restorePhantomSpaces } from "../phantom.js";
2
2
  import { readPosition } from "../wire-read.js";
3
3
  import { decodeColumnAlign } from "./column-align.js";
4
+ import { PoolOffsets } from "../string-pool.js";
4
5
  import { NodeTypeName } from "./generated/node-types.js";
5
6
  import { ARENA_MAGIC, KIND_MDAST, FIELD, HEADER } from "../generated/arena-layout.js";
6
7
  export { NodeType, NodeTypeName } from "./generated/node-types.js";
@@ -9,6 +10,8 @@ export class MdastReader {
9
10
  #header;
10
11
  #textDecoder;
11
12
  #stringPoolCache = null;
13
+ #poolChecked = false;
14
+ #poolOffsets = null;
12
15
  constructor(buffer) {
13
16
  if (buffer instanceof Uint8Array) {
14
17
  this.#view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
@@ -16,7 +19,7 @@ export class MdastReader {
16
19
  else {
17
20
  this.#view = new DataView(buffer);
18
21
  }
19
- this.#textDecoder = new TextDecoder("utf-8");
22
+ this.#textDecoder = new TextDecoder("utf-8", { ignoreBOM: true });
20
23
  this.#header = this.#readHeader();
21
24
  }
22
25
  #readHeader() {
@@ -86,9 +89,21 @@ export class MdastReader {
86
89
  getString(offset, len) {
87
90
  if (len === 0)
88
91
  return "";
89
- const { stringPoolOffset } = this.#header;
90
- const bytes = new Uint8Array(this.#view.buffer, this.#view.byteOffset + stringPoolOffset + offset, len);
91
- return this.#textDecoder.decode(bytes);
92
+ const pool = this.getStringPool();
93
+ if (!this.#poolChecked) {
94
+ this.#poolChecked = true;
95
+ const { stringPoolOffset, stringPoolLen } = this.#header;
96
+ const extraBytes = stringPoolLen - pool.length;
97
+ if (extraBytes > 0) {
98
+ const bytes = new Uint8Array(this.#view.buffer, this.#view.byteOffset + stringPoolOffset, stringPoolLen);
99
+ this.#poolOffsets = new PoolOffsets(bytes, extraBytes);
100
+ }
101
+ }
102
+ // An all-ASCII pool has byte offsets equal to its UTF-16 indices.
103
+ const offsets = this.#poolOffsets;
104
+ return offsets === null
105
+ ? pool.substring(offset, offset + len)
106
+ : offsets.slice(pool, offset, len);
92
107
  }
93
108
  getNode(nodeId) {
94
109
  const { nodesOffset, nodeStructSize, nodeCount } = this.#header;
@@ -116,6 +131,10 @@ export class MdastReader {
116
131
  const { nodesOffset, nodeStructSize } = this.#header;
117
132
  return this.#view.getUint8(nodesOffset + nodeId * nodeStructSize + FIELD.node_type);
118
133
  }
134
+ getPosition(nodeId) {
135
+ const base = this.#header.nodesOffset + nodeId * this.#header.nodeStructSize;
136
+ return readPosition(this.#view, base + FIELD.start_offset);
137
+ }
119
138
  /** Fast path: read only the parent id for a node (0xffffffff at the root). */
120
139
  getParentId(nodeId) {
121
140
  const { nodesOffset, nodeStructSize } = this.#header;
@@ -148,6 +167,27 @@ export class MdastReader {
148
167
  stack.push(v.getUint32(childrenOffset + (childrenStart + i) * 4, true));
149
168
  }
150
169
  }
170
+ /** Fixed-layout field reads that touch the buffer directly. The generated
171
+ * decoder runs once per node, so an intermediate view or ref object here
172
+ * costs an allocation per field. */
173
+ fieldU8(nodeId, offset, fallback) {
174
+ const base = this.#header.nodesOffset + nodeId * this.#header.nodeStructSize;
175
+ const v = this.#view;
176
+ if (offset >= v.getUint32(base + FIELD.data_len, true))
177
+ return fallback;
178
+ const at = this.#header.typeDataOffset + v.getUint32(base + FIELD.data_offset, true);
179
+ return v.getUint8(at + offset);
180
+ }
181
+ /** `""` when the field is absent or empty; callers map that to `null` for
182
+ * nullable fields, matching the Rust decoders' bounds checks. */
183
+ fieldString(nodeId, offset) {
184
+ const base = this.#header.nodesOffset + nodeId * this.#header.nodeStructSize;
185
+ const v = this.#view;
186
+ if (offset + 8 > v.getUint32(base + FIELD.data_len, true))
187
+ return "";
188
+ const at = this.#header.typeDataOffset + v.getUint32(base + FIELD.data_offset, true) + offset;
189
+ return this.getString(v.getUint32(at, true), v.getUint32(at + 4, true));
190
+ }
151
191
  getTypeData(nodeId) {
152
192
  const base = this.#header.nodesOffset + nodeId * this.#header.nodeStructSize;
153
193
  const v = this.#view;
@@ -159,10 +199,10 @@ export class MdastReader {
159
199
  }
160
200
  /** Read a StringRef (offset: u32 LE, len: u32 LE) from type data. */
161
201
  readStringRef(typeData, byteOffset = 0) {
162
- const view = new DataView(typeData.buffer, typeData.byteOffset + byteOffset);
202
+ const at = typeData.byteOffset - this.#view.byteOffset + byteOffset;
163
203
  return {
164
- offset: view.getUint32(0, true),
165
- len: view.getUint32(4, true),
204
+ offset: this.#view.getUint32(at, true),
205
+ len: this.#view.getUint32(at + 4, true),
166
206
  };
167
207
  }
168
208
  /**
@@ -170,9 +210,7 @@ export class MdastReader {
170
210
  * These store a single StringRef as their type data.
171
211
  */
172
212
  getTextValue(nodeId) {
173
- const data = this.getTypeData(nodeId);
174
- const ref = this.readStringRef(data);
175
- return this.getString(ref.offset, ref.len);
213
+ return this.fieldString(nodeId, 0);
176
214
  }
177
215
  /**
178
216
  * ListData #[repr(C)]: start(0..4), ordered(4), spread(5), _pad(6..8).
@@ -199,6 +237,13 @@ export class MdastReader {
199
237
  spread: data[1] !== 0,
200
238
  };
201
239
  }
240
+ /**
241
+ * DescriptionDetailsData #[repr(C)]: spread(0). Valid for descriptionDetails.
242
+ */
243
+ getDescriptionDetailsData(nodeId) {
244
+ const data = this.getTypeData(nodeId);
245
+ return { spread: data.length > 0 && data[0] !== 0 };
246
+ }
202
247
  /**
203
248
  * TableData #[repr(C)]: align_count(0..4), then align_count bytes.
204
249
  * Alignment bytes: 0=none, 1=left, 2=right, 3=center.