satteri 0.8.1 → 0.9.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 (60) hide show
  1. package/README.md +35 -0
  2. package/dist/byte-writer.d.ts +31 -0
  3. package/dist/byte-writer.js +106 -0
  4. package/dist/child-stub.d.ts +17 -0
  5. package/dist/child-stub.js +57 -0
  6. package/dist/command-buffer.d.ts +23 -26
  7. package/dist/command-buffer.js +64 -132
  8. package/dist/compile.d.ts +5 -1
  9. package/dist/compile.js +40 -26
  10. package/dist/directive-types.d.ts +4 -2
  11. package/dist/generated/arena-layout.d.ts +37 -0
  12. package/dist/generated/arena-layout.js +41 -0
  13. package/dist/generated/wire-constants.d.ts +51 -0
  14. package/dist/generated/wire-constants.js +63 -0
  15. package/dist/handles.d.ts +21 -0
  16. package/dist/handles.js +1 -0
  17. package/dist/hast/child-stub.d.ts +18 -0
  18. package/dist/hast/child-stub.js +43 -0
  19. package/dist/hast/element-props.d.ts +2 -0
  20. package/dist/hast/element-props.js +23 -0
  21. package/dist/hast/generated/node-types.d.ts +14 -0
  22. package/dist/hast/generated/node-types.js +64 -0
  23. package/dist/hast/hast-materializer.d.ts +2 -1
  24. package/dist/hast/hast-materializer.js +9 -45
  25. package/dist/hast/hast-reader.d.ts +18 -23
  26. package/dist/hast/hast-reader.js +48 -115
  27. package/dist/hast/hast-visitor.d.ts +35 -13
  28. package/dist/hast/hast-visitor.js +395 -302
  29. package/dist/index.d.ts +10 -4
  30. package/dist/index.js +23 -1
  31. package/dist/lazy-child-resolver.d.ts +36 -0
  32. package/dist/lazy-child-resolver.js +95 -0
  33. package/dist/lazy-props.d.ts +1 -1
  34. package/dist/mdast/child-stub.d.ts +18 -0
  35. package/dist/mdast/child-stub.js +46 -0
  36. package/dist/mdast/column-align.d.ts +1 -0
  37. package/dist/mdast/column-align.js +6 -0
  38. package/dist/mdast/generated/layout.d.ts +19 -0
  39. package/dist/mdast/generated/layout.js +272 -0
  40. package/dist/mdast/generated/node-types.d.ts +58 -0
  41. package/dist/mdast/generated/node-types.js +266 -0
  42. package/dist/mdast/mdast-materializer.d.ts +3 -2
  43. package/dist/mdast/mdast-materializer.js +9 -93
  44. package/dist/mdast/mdast-reader.d.ts +4 -120
  45. package/dist/mdast/mdast-reader.js +45 -272
  46. package/dist/mdast/mdast-visitor.d.ts +52 -14
  47. package/dist/mdast/mdast-visitor.js +348 -427
  48. package/dist/mdx-attr.d.ts +2 -0
  49. package/dist/mdx-attr.js +20 -0
  50. package/dist/op-stream.d.ts +38 -0
  51. package/dist/op-stream.js +125 -0
  52. package/dist/phantom.d.ts +2 -0
  53. package/dist/phantom.js +13 -0
  54. package/dist/types.d.ts +34 -4
  55. package/dist/visitor-shared.d.ts +28 -0
  56. package/dist/visitor-shared.js +52 -0
  57. package/dist/wire-read.d.ts +27 -0
  58. package/dist/wire-read.js +59 -0
  59. package/index.js +52 -52
  60. package/package.json +6 -6
package/README.md CHANGED
@@ -163,6 +163,41 @@ heading(node) {
163
163
  }
164
164
  ```
165
165
 
166
+ ### Sharing data between plugins
167
+
168
+ 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.
169
+
170
+ ```ts
171
+ const collectHeadings = defineMdastPlugin({
172
+ name: "collect-headings",
173
+ heading(node, ctx) {
174
+ const list = (ctx.data.headings as string[]) ?? [];
175
+ const first = node.children[0];
176
+ if (first && "value" in first) list.push(first.value as string);
177
+ ctx.data.headings = list;
178
+ },
179
+ });
180
+
181
+ const { html, data } = markdownToHtml("# A\n\n# B", { mdastPlugins: [collectHeadings] });
182
+ console.log(data?.headings); // ["A", "B"]
183
+ ```
184
+
185
+ 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.
186
+
187
+ By default keys are typed as `unknown`. To give a key a type, augment the `DataMap` interface, much like `vfile`'s `DataMap`:
188
+
189
+ ```ts
190
+ declare module "satteri" {
191
+ interface DataMap {
192
+ headings: string[];
193
+ }
194
+ }
195
+
196
+ // now ctx.data.headings and result.data.headings are typed as string[] | undefined
197
+ ```
198
+
199
+ Unregistered keys stay `unknown`, so the bag remains open-ended.
200
+
166
201
  ### How transforms compose
167
202
 
168
203
  Unlike remark and rehype, which re-walk the tree until it stops changing, each Sätteri plugin walks the tree **once**. Within that single pass:
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Growable little-endian byte buffer shared by the op-stream and command-buffer
3
+ * encoders — the two hottest write paths in the package.
4
+ *
5
+ * Subclasses write single bytes directly through the protected `buf`/`n`
6
+ * fields after an `ensure` for the whole record (one bounds check per record,
7
+ * not per byte, and no per-byte call in unoptimized tiers — CodSpeed's
8
+ * Simulation mode runs too few iterations for V8 to inline method calls).
9
+ * Only the non-trivial logic lives here: growth, u32 writes, and UTF-8
10
+ * string encoding.
11
+ */
12
+ export declare class ByteWriter {
13
+ #private;
14
+ protected buf: Uint8Array;
15
+ protected n: number;
16
+ constructor(initialSize: number);
17
+ /** Number of bytes written so far. */
18
+ get length(): number;
19
+ /** Reset for reuse; the grown buffer is retained so steady state is alloc-free. */
20
+ reset(): void;
21
+ /** View of the bytes written so far (no copy; valid until the next write or reset). */
22
+ take(): Uint8Array;
23
+ /** Release the backing (handed-out views stay intact); next write re-allocates. */
24
+ protected release(): void;
25
+ /** Grow (doubling) so `extra` more bytes fit; required before unchecked writes. */
26
+ ensure(extra: number): void;
27
+ /** Write a u32 (LE) at the cursor; caller must have `ensure`d 4 bytes. */
28
+ protected writeU32(v: number): void;
29
+ /** u32 byte length + UTF-8 bytes (self-ensuring). */
30
+ protected utf8WithU32Len(s: string): void;
31
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Growable little-endian byte buffer shared by the op-stream and command-buffer
3
+ * encoders — the two hottest write paths in the package.
4
+ *
5
+ * Subclasses write single bytes directly through the protected `buf`/`n`
6
+ * fields after an `ensure` for the whole record (one bounds check per record,
7
+ * not per byte, and no per-byte call in unoptimized tiers — CodSpeed's
8
+ * Simulation mode runs too few iterations for V8 to inline method calls).
9
+ * Only the non-trivial logic lives here: growth, u32 writes, and UTF-8
10
+ * string encoding.
11
+ */
12
+ const encoder = new TextEncoder();
13
+ const EMPTY = new Uint8Array(0);
14
+ /** Below this length the inline char-code copy beats `encodeInto`'s call
15
+ * overhead; above it the native bulk path wins (and skips the ASCII scan).
16
+ * Measured crossover ~16 bytes (Node 24: 8B 15 vs 41 ns, 16B 37 vs 43,
17
+ * 32B 73 vs 52). */
18
+ const INLINE_STR_MAX = 16;
19
+ export class ByteWriter {
20
+ // Backing allocates on first write: visitor passes construct buffers that
21
+ // often never receive a byte (read-only plugins).
22
+ buf = EMPTY;
23
+ n = 0;
24
+ #initialSize;
25
+ constructor(initialSize) {
26
+ this.#initialSize = initialSize;
27
+ }
28
+ /** Number of bytes written so far. */
29
+ get length() {
30
+ return this.n;
31
+ }
32
+ /** Reset for reuse; the grown buffer is retained so steady state is alloc-free. */
33
+ reset() {
34
+ this.n = 0;
35
+ }
36
+ /** View of the bytes written so far (no copy; valid until the next write or reset). */
37
+ take() {
38
+ return this.n === 0 ? EMPTY : this.buf.subarray(0, this.n);
39
+ }
40
+ /** Release the backing (handed-out views stay intact); next write re-allocates. */
41
+ release() {
42
+ this.buf = EMPTY;
43
+ this.n = 0;
44
+ }
45
+ /** Grow (doubling) so `extra` more bytes fit; required before unchecked writes. */
46
+ ensure(extra) {
47
+ if (this.n + extra <= this.buf.length)
48
+ return;
49
+ let size = Math.max(this.#initialSize, this.buf.length * 2);
50
+ while (this.n + extra > size)
51
+ size *= 2;
52
+ const grown = new Uint8Array(size);
53
+ grown.set(this.buf);
54
+ this.buf = grown;
55
+ }
56
+ /** Write a u32 (LE) at the cursor; caller must have `ensure`d 4 bytes. */
57
+ writeU32(v) {
58
+ const buf = this.buf;
59
+ let n = this.n;
60
+ buf[n++] = v & 255;
61
+ buf[n++] = (v >> 8) & 255;
62
+ buf[n++] = (v >> 16) & 255;
63
+ buf[n++] = (v >>> 24) & 255;
64
+ this.n = n;
65
+ }
66
+ /** u32 byte length + UTF-8 bytes (self-ensuring). */
67
+ utf8WithU32Len(s) {
68
+ const len = s.length;
69
+ this.ensure(4 + len * 3); // worst-case UTF-8 is 3 bytes per UTF-16 unit
70
+ // Short strings: inline char copy (cheaper than a native call), guarded by a
71
+ // quick ASCII scan. Anything longer — or non-ASCII — goes to encodeInto.
72
+ if (len <= INLINE_STR_MAX) {
73
+ let ascii = true;
74
+ for (let i = 0; i < len; i++) {
75
+ if (s.charCodeAt(i) > 127) {
76
+ ascii = false;
77
+ break;
78
+ }
79
+ }
80
+ if (ascii) {
81
+ // Inline stores, not writeU32: a per-string method call in the
82
+ // unoptimized tier is exactly what this class's header rules out.
83
+ const buf = this.buf;
84
+ let n = this.n;
85
+ buf[n++] = len & 255;
86
+ buf[n++] = (len >> 8) & 255;
87
+ buf[n++] = (len >> 16) & 255;
88
+ buf[n++] = (len >>> 24) & 255;
89
+ for (let i = 0; i < len; i++)
90
+ buf[n + i] = s.charCodeAt(i);
91
+ this.n = n + len;
92
+ return;
93
+ }
94
+ }
95
+ // Bulk path: encodeInto writes UTF-8 straight into the buffer (no alloc, no
96
+ // per-char loop); backpatch the byte length once it's known.
97
+ const lenPos = this.n;
98
+ this.n += 4;
99
+ const written = encoder.encodeInto(s, this.buf.subarray(this.n)).written;
100
+ this.buf[lenPos] = written & 255;
101
+ this.buf[lenPos + 1] = (written >> 8) & 255;
102
+ this.buf[lenPos + 2] = (written >> 16) & 255;
103
+ this.buf[lenPos + 3] = (written >>> 24) & 255;
104
+ this.n += written;
105
+ }
106
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Shared machinery for walk-path child stubs (see `hast/child-stub.ts` and
3
+ * `mdast/child-stub.ts`): lightweight stand-ins that carry arena id + type
4
+ * eagerly and defer the full-arena snapshot until a plugin reads a real field.
5
+ * Passthrough children (the common replaceNode shape) thus compile to one-word
6
+ * refs without ever serializing the arena.
7
+ */
8
+ /**
9
+ * Descriptor template for one node type's stub fields. Own enumerable getters
10
+ * so a spread copy carries correct values. `position`/`data` ride on every
11
+ * stub; they may yield `undefined` where a materialized node omits the key —
12
+ * accepted drift, invisible to `toEqual`.
13
+ */
14
+ export declare function stubDescriptors(fields: readonly string[]): PropertyDescriptorMap;
15
+ /** Node tags are dense small ints, so the per-child stub loop indexes flat
16
+ * arrays instead of paying Map/dictionary lookups. */
17
+ export declare function flatByTag<T>(table: Readonly<Record<number, T>>): readonly (T | undefined)[];
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Shared machinery for walk-path child stubs (see `hast/child-stub.ts` and
3
+ * `mdast/child-stub.ts`): lightweight stand-ins that carry arena id + type
4
+ * eagerly and defer the full-arena snapshot until a plugin reads a real field.
5
+ * Passthrough children (the common replaceNode shape) thus compile to one-word
6
+ * refs without ever serializing the arena.
7
+ */
8
+ /** Stub → materialized arena node, filled on the first real-field read. */
9
+ const REAL_NODES = new WeakMap();
10
+ /** One shared memoizing getter per field name, so stubs install shared
11
+ * descriptor templates instead of allocating per-field closures per node. */
12
+ const FIELD_GETTERS = new Map();
13
+ function fieldGetter(key) {
14
+ let getter = FIELD_GETTERS.get(key);
15
+ if (getter === undefined) {
16
+ getter = function () {
17
+ let real = REAL_NODES.get(this);
18
+ if (real === undefined) {
19
+ real = this._resolver.materializeOne(this._id);
20
+ REAL_NODES.set(this, real);
21
+ }
22
+ const value = real[key];
23
+ Object.defineProperty(this, key, {
24
+ value,
25
+ writable: true,
26
+ enumerable: true,
27
+ configurable: true,
28
+ });
29
+ return value;
30
+ };
31
+ FIELD_GETTERS.set(key, getter);
32
+ }
33
+ return getter;
34
+ }
35
+ /**
36
+ * Descriptor template for one node type's stub fields. Own enumerable getters
37
+ * so a spread copy carries correct values. `position`/`data` ride on every
38
+ * stub; they may yield `undefined` where a materialized node omits the key —
39
+ * accepted drift, invisible to `toEqual`.
40
+ */
41
+ export function stubDescriptors(fields) {
42
+ const map = {};
43
+ for (const key of [...fields, "position", "data"]) {
44
+ map[key] = { get: fieldGetter(key), enumerable: true, configurable: true };
45
+ }
46
+ return map;
47
+ }
48
+ /** Node tags are dense small ints, so the per-child stub loop indexes flat
49
+ * arrays instead of paying Map/dictionary lookups. */
50
+ export function flatByTag(table) {
51
+ const flat = [];
52
+ for (const tag of Object.keys(table)) {
53
+ const nodeType = Number(tag);
54
+ flat[nodeType] = table[nodeType];
55
+ }
56
+ return flat;
57
+ }
@@ -2,20 +2,22 @@
2
2
  * Binary command buffer for efficient JS→Rust mutation serialization.
3
3
  *
4
4
  * Simple mutations (remove, setProperty) are encoded as compact binary commands.
5
- * Structural mutations (insert, replace) carry payloads that can be raw strings
6
- * (for Rust to re-parse) or JSON-serialized node trees.
5
+ * Structural mutations (insert, replace, …) carry one of two payload kinds:
6
+ * compiled op-streams (`PAYLOAD_OPSTREAM`, replayed straight into the arena —
7
+ * see op-stream.ts) for declarative content, or raw markdown/HTML strings
8
+ * (re-parsed by Rust) for the `{raw}`/`{rawHtml}` escape hatches.
7
9
  *
8
10
  * All multi-byte integers are little-endian to match native x86/ARM layout and
9
11
  * avoid byte-swapping on the Rust side.
10
12
  */
13
+ import { ByteWriter } from "./byte-writer.js";
11
14
  import type { MdastNode } from "./types.js";
12
15
  type ReturnClass = "no_change" | "raw_markdown" | "raw_html" | "structured_node";
13
16
  export declare function classifyReturn(value: unknown): ReturnClass;
14
- export declare class CommandBuffer {
15
- private buffer;
16
- private view;
17
- private bytes;
18
- private offset;
17
+ /** Structural commands that carry a subtree payload. Each has an `${op}Opstream`
18
+ * twin (declarative content) and a raw twin (`{raw}`/`{rawHtml}` escape hatch). */
19
+ export type StructuralOp = "replace" | "insertBefore" | "insertAfter" | "prependChild" | "appendChild" | "wrapNode";
20
+ export declare class CommandBuffer extends ByteWriter {
19
21
  constructor();
20
22
  removeNode(nodeId: number): void;
21
23
  /** Unified set-property for both MDAST and HAST nodes. */
@@ -50,28 +52,23 @@ export declare class CommandBuffer {
50
52
  } | {
51
53
  rawHtml: string;
52
54
  }): void;
53
- /** Write a REPLACE command with a pre-serialized JSON payload. */
54
- replaceRawJson(nodeId: number, json: string): void;
55
- /** Replace a node's child list (Root-wrapped `json`) while keeping the node. */
56
- setChildren(nodeId: number, json: string): void;
57
- /** Write any structural command with a pre-serialized JSON payload. */
58
- insertBeforeRawJson(nodeId: number, json: string): void;
59
- insertAfterRawJson(nodeId: number, json: string): void;
60
- prependChildRawJson(nodeId: number, json: string): void;
61
- appendChildRawJson(nodeId: number, json: string): void;
62
- wrapNodeRawJson(nodeId: number, json: string): void;
63
- private writeRawJsonCommand;
55
+ /** Header (cmd + nodeId + payloadType) followed by a length-prefixed string payload. */
56
+ private writePayloadCommand;
57
+ replaceOpstream(nodeId: number, ops: Uint8Array): void;
58
+ /** Replace a node's child list (root-wrapped `ops`) while keeping the node. */
59
+ setChildrenOpstream(nodeId: number, ops: Uint8Array): void;
60
+ insertBeforeOpstream(nodeId: number, ops: Uint8Array): void;
61
+ insertAfterOpstream(nodeId: number, ops: Uint8Array): void;
62
+ prependChildOpstream(nodeId: number, ops: Uint8Array): void;
63
+ appendChildOpstream(nodeId: number, ops: Uint8Array): void;
64
+ wrapNodeOpstream(nodeId: number, ops: Uint8Array): void;
65
+ private writeOpstreamCommand;
64
66
  /** Return a Uint8Array view of the written bytes (no copy). */
65
67
  getBuffer(): Uint8Array;
66
- /** Number of bytes written so far. */
67
- get length(): number;
68
- /** Reset the buffer for reuse, releasing the old ArrayBuffer. */
68
+ /** Reset for reuse, releasing the old buffer (handed-out views stay intact). */
69
69
  reset(): void;
70
+ /** Raw structural content (`{raw}`/`{rawHtml}` escape hatches). Declarative
71
+ * nodes go through the `*Opstream` methods, not here. */
70
72
  private writeStructuralCommand;
71
- private ensureCapacity;
72
- private grow;
73
- private writeU8;
74
- private writeU32;
75
- private writeBytes;
76
73
  }
77
74
  export {};
@@ -2,33 +2,17 @@
2
2
  * Binary command buffer for efficient JS→Rust mutation serialization.
3
3
  *
4
4
  * Simple mutations (remove, setProperty) are encoded as compact binary commands.
5
- * Structural mutations (insert, replace) carry payloads that can be raw strings
6
- * (for Rust to re-parse) or JSON-serialized node trees.
5
+ * Structural mutations (insert, replace, …) carry one of two payload kinds:
6
+ * compiled op-streams (`PAYLOAD_OPSTREAM`, replayed straight into the arena —
7
+ * see op-stream.ts) for declarative content, or raw markdown/HTML strings
8
+ * (re-parsed by Rust) for the `{raw}`/`{rawHtml}` escape hatches.
7
9
  *
8
10
  * All multi-byte integers are little-endian to match native x86/ARM layout and
9
11
  * avoid byte-swapping on the Rust side.
10
12
  */
11
- // Command bytes (0x01–0x0F)
12
- const CMD_REMOVE = 0x01;
13
- const CMD_INSERT_BEFORE = 0x05;
14
- const CMD_INSERT_AFTER = 0x06;
15
- const CMD_PREPEND_CHILD = 0x07;
16
- const CMD_APPEND_CHILD = 0x08;
17
- const CMD_WRAP = 0x09;
18
- const CMD_REPLACE = 0x0b;
19
- const CMD_SET_PROPERTY = 0x0c;
20
- const CMD_SET_CHILDREN = 0x0d;
21
- // Payload types (0x10+, distinct range from commands)
22
- const PAYLOAD_RAW_MARKDOWN = 0x10;
23
- const PAYLOAD_RAW_HTML = 0x11;
24
- const PAYLOAD_SERDE_JSON = 0x12;
25
- // Value types for CMD_SET_PROPERTY (must match commands.rs PROP_* constants)
26
- const PROP_STRING = 0;
27
- const PROP_BOOL_TRUE = 1;
28
- const PROP_BOOL_FALSE = 2;
29
- const PROP_SPACE_SEP = 3;
30
- const PROP_INT = 5;
31
- const PROP_NULL = 6;
13
+ import { ByteWriter } from "./byte-writer.js";
14
+ import { PROP_STRING, PROP_BOOL_TRUE, PROP_BOOL_FALSE, PROP_SPACE_SEP, PROP_INT, PROP_NULL, } from "./op-stream.js";
15
+ 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";
32
16
  export function classifyReturn(value) {
33
17
  if (value === undefined || value === null)
34
18
  return "no_change";
@@ -42,61 +26,50 @@ export function classifyReturn(value) {
42
26
  throw new Error("Invalid return value from visitor: must have raw, rawHtml, or type");
43
27
  }
44
28
  const INITIAL_SIZE = 4096;
45
- const encoder = new TextEncoder();
46
- const EMPTY_U8 = new Uint8Array(0);
47
- export class CommandBuffer {
48
- buffer;
49
- view;
50
- bytes;
51
- offset = 0;
29
+ export class CommandBuffer extends ByteWriter {
52
30
  constructor() {
53
- this.buffer = new ArrayBuffer(INITIAL_SIZE);
54
- this.view = new DataView(this.buffer);
55
- this.bytes = new Uint8Array(this.buffer);
31
+ super(INITIAL_SIZE);
56
32
  }
57
33
  removeNode(nodeId) {
58
- this.ensureCapacity(5);
59
- this.writeU8(CMD_REMOVE);
34
+ this.ensure(5);
35
+ this.buf[this.n++] = CMD_REMOVE;
60
36
  this.writeU32(nodeId);
61
37
  }
62
38
  /** Unified set-property for both MDAST and HAST nodes. */
63
39
  setProperty(nodeId, key, value) {
64
- const encodedName = encoder.encode(key);
65
40
  let valueType;
66
- let encodedValue;
41
+ let str;
67
42
  if (value === null || value === undefined) {
68
43
  valueType = PROP_NULL;
69
- encodedValue = EMPTY_U8;
44
+ str = "";
70
45
  }
71
46
  else if (value === true) {
72
47
  valueType = PROP_BOOL_TRUE;
73
- encodedValue = EMPTY_U8;
48
+ str = "";
74
49
  }
75
50
  else if (value === false) {
76
51
  valueType = PROP_BOOL_FALSE;
77
- encodedValue = EMPTY_U8;
52
+ str = "";
78
53
  }
79
54
  else if (typeof value === "number") {
80
55
  valueType = PROP_INT;
81
- encodedValue = encoder.encode(String(value));
56
+ str = String(value);
82
57
  }
83
58
  else if (Array.isArray(value)) {
84
59
  valueType = PROP_SPACE_SEP;
85
- encodedValue = encoder.encode(value.join(" "));
60
+ str = value.join(" ");
86
61
  }
87
62
  else {
88
63
  valueType = PROP_STRING;
89
- encodedValue = encoder.encode(String(value));
64
+ str = String(value);
90
65
  }
91
- // 1(cmd) + 4(nodeId) + 1(valueType) + 4(nameLen) + name + 4(valueLen) + value
92
- this.ensureCapacity(14 + encodedName.length + encodedValue.length);
93
- this.writeU8(CMD_SET_PROPERTY);
66
+ // 1(cmd) + 4(nodeId) + 1(valueType); name and value are length-prefixed strings
67
+ this.ensure(6);
68
+ this.buf[this.n++] = CMD_SET_PROPERTY;
94
69
  this.writeU32(nodeId);
95
- this.writeU8(valueType);
96
- this.writeU32(encodedName.length);
97
- this.writeBytes(encodedName);
98
- this.writeU32(encodedValue.length);
99
- this.writeBytes(encodedValue);
70
+ this.buf[this.n++] = valueType;
71
+ this.utf8WithU32Len(key);
72
+ this.utf8WithU32Len(str);
100
73
  }
101
74
  insertBefore(nodeId, newNode) {
102
75
  this.writeStructuralCommand(CMD_INSERT_BEFORE, nodeId, newNode);
@@ -116,108 +89,67 @@ export class CommandBuffer {
116
89
  replace(nodeId, newNode) {
117
90
  this.writeStructuralCommand(CMD_REPLACE, nodeId, newNode);
118
91
  }
119
- /** Write a REPLACE command with a pre-serialized JSON payload. */
120
- replaceRawJson(nodeId, json) {
121
- this.writeRawJsonCommand(CMD_REPLACE, nodeId, json);
92
+ /** Header (cmd + nodeId + payloadType) followed by a length-prefixed string payload. */
93
+ writePayloadCommand(cmd, nodeId, payloadType, s) {
94
+ this.ensure(6);
95
+ this.buf[this.n++] = cmd;
96
+ this.writeU32(nodeId);
97
+ this.buf[this.n++] = payloadType;
98
+ this.utf8WithU32Len(s);
99
+ }
100
+ replaceOpstream(nodeId, ops) {
101
+ this.writeOpstreamCommand(CMD_REPLACE, nodeId, ops);
122
102
  }
123
- /** Replace a node's child list (Root-wrapped `json`) while keeping the node. */
124
- setChildren(nodeId, json) {
125
- this.writeRawJsonCommand(CMD_SET_CHILDREN, nodeId, json);
103
+ /** Replace a node's child list (root-wrapped `ops`) while keeping the node. */
104
+ setChildrenOpstream(nodeId, ops) {
105
+ this.writeOpstreamCommand(CMD_SET_CHILDREN, nodeId, ops);
126
106
  }
127
- /** Write any structural command with a pre-serialized JSON payload. */
128
- insertBeforeRawJson(nodeId, json) {
129
- this.writeRawJsonCommand(CMD_INSERT_BEFORE, nodeId, json);
107
+ insertBeforeOpstream(nodeId, ops) {
108
+ this.writeOpstreamCommand(CMD_INSERT_BEFORE, nodeId, ops);
130
109
  }
131
- insertAfterRawJson(nodeId, json) {
132
- this.writeRawJsonCommand(CMD_INSERT_AFTER, nodeId, json);
110
+ insertAfterOpstream(nodeId, ops) {
111
+ this.writeOpstreamCommand(CMD_INSERT_AFTER, nodeId, ops);
133
112
  }
134
- prependChildRawJson(nodeId, json) {
135
- this.writeRawJsonCommand(CMD_PREPEND_CHILD, nodeId, json);
113
+ prependChildOpstream(nodeId, ops) {
114
+ this.writeOpstreamCommand(CMD_PREPEND_CHILD, nodeId, ops);
136
115
  }
137
- appendChildRawJson(nodeId, json) {
138
- this.writeRawJsonCommand(CMD_APPEND_CHILD, nodeId, json);
116
+ appendChildOpstream(nodeId, ops) {
117
+ this.writeOpstreamCommand(CMD_APPEND_CHILD, nodeId, ops);
139
118
  }
140
- wrapNodeRawJson(nodeId, json) {
141
- this.writeRawJsonCommand(CMD_WRAP, nodeId, json);
119
+ wrapNodeOpstream(nodeId, ops) {
120
+ this.writeOpstreamCommand(CMD_WRAP, nodeId, ops);
142
121
  }
143
- writeRawJsonCommand(cmd, nodeId, json) {
144
- const encoded = encoder.encode(json);
145
- this.ensureCapacity(10 + encoded.length);
146
- this.writeU8(cmd);
122
+ writeOpstreamCommand(cmd, nodeId, ops) {
123
+ this.ensure(10 + ops.length);
124
+ this.buf[this.n++] = cmd;
147
125
  this.writeU32(nodeId);
148
- this.writeU8(PAYLOAD_SERDE_JSON);
149
- this.writeU32(encoded.length);
150
- this.writeBytes(encoded);
126
+ this.buf[this.n++] = PAYLOAD_OPSTREAM;
127
+ this.writeU32(ops.length);
128
+ this.buf.set(ops, this.n);
129
+ this.n += ops.length;
151
130
  }
152
131
  /** Return a Uint8Array view of the written bytes (no copy). */
153
132
  getBuffer() {
154
- return new Uint8Array(this.buffer, 0, this.offset);
155
- }
156
- /** Number of bytes written so far. */
157
- get length() {
158
- return this.offset;
133
+ return this.take();
159
134
  }
160
- /** Reset the buffer for reuse, releasing the old ArrayBuffer. */
135
+ /** Reset for reuse, releasing the old buffer (handed-out views stay intact). */
161
136
  reset() {
162
- this.buffer = new ArrayBuffer(INITIAL_SIZE);
163
- this.view = new DataView(this.buffer);
164
- this.bytes = new Uint8Array(this.buffer);
165
- this.offset = 0;
137
+ if (this.n === 0)
138
+ return;
139
+ this.release();
166
140
  }
141
+ /** Raw structural content (`{raw}`/`{rawHtml}` escape hatches). Declarative
142
+ * nodes go through the `*Opstream` methods, not here. */
167
143
  writeStructuralCommand(cmd, nodeId, node) {
168
144
  const v = node;
169
145
  if (typeof v.raw === "string") {
170
- const encoded = encoder.encode(v.raw);
171
- this.ensureCapacity(10 + encoded.length); // 1(cmd) + 4(nodeId) + 1(payloadType) + 4(len) + payload
172
- this.writeU8(cmd);
173
- this.writeU32(nodeId);
174
- this.writeU8(PAYLOAD_RAW_MARKDOWN);
175
- this.writeU32(encoded.length);
176
- this.writeBytes(encoded);
146
+ this.writePayloadCommand(cmd, nodeId, PAYLOAD_RAW_MARKDOWN, v.raw);
177
147
  }
178
148
  else if (typeof v.rawHtml === "string") {
179
- const encoded = encoder.encode(v.rawHtml);
180
- this.ensureCapacity(10 + encoded.length);
181
- this.writeU8(cmd);
182
- this.writeU32(nodeId);
183
- this.writeU8(PAYLOAD_RAW_HTML);
184
- this.writeU32(encoded.length);
185
- this.writeBytes(encoded);
149
+ this.writePayloadCommand(cmd, nodeId, PAYLOAD_RAW_HTML, v.rawHtml);
186
150
  }
187
151
  else {
188
- // Structured node, serialize as JSON
189
- const json = JSON.stringify(node);
190
- const encoded = encoder.encode(json);
191
- this.ensureCapacity(10 + encoded.length);
192
- this.writeU8(cmd);
193
- this.writeU32(nodeId);
194
- this.writeU8(PAYLOAD_SERDE_JSON);
195
- this.writeU32(encoded.length);
196
- this.writeBytes(encoded);
197
- }
198
- }
199
- ensureCapacity(needed) {
200
- while (this.offset + needed > this.buffer.byteLength) {
201
- this.grow();
152
+ throw new Error("CommandBuffer: structural content must be {raw} or {rawHtml}");
202
153
  }
203
154
  }
204
- grow() {
205
- const newBuffer = new ArrayBuffer(this.buffer.byteLength * 2);
206
- new Uint8Array(newBuffer).set(this.bytes);
207
- this.buffer = newBuffer;
208
- this.view = new DataView(this.buffer);
209
- this.bytes = new Uint8Array(this.buffer);
210
- }
211
- writeU8(val) {
212
- this.view.setUint8(this.offset, val);
213
- this.offset += 1;
214
- }
215
- writeU32(val) {
216
- this.view.setUint32(this.offset, val, true);
217
- this.offset += 4;
218
- }
219
- writeBytes(data) {
220
- this.bytes.set(data, this.offset);
221
- this.offset += data.length;
222
- }
223
155
  }
package/dist/compile.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { MdastPluginInput, HastPluginInput } from "./plugin.js";
2
- import type { MdastNode, HastNode } from "./types.js";
2
+ import type { MdastNode, HastNode, Data } from "./types.js";
3
3
  /** Configuration for static subtree collapsing during MDX compilation. */
4
4
  export interface OptimizeStaticConfig {
5
5
  component: string;
@@ -200,6 +200,8 @@ export interface MarkdownToHtmlResult {
200
200
  html: string;
201
201
  /** Frontmatter block at the start of the document, or `null` if none. */
202
202
  frontmatter: Frontmatter | null;
203
+ /** Document-level data bag populated by plugins via `ctx.data`. Empty `{}` if untouched. */
204
+ data: Data;
203
205
  }
204
206
  /** Result of {@link mdxToJs}. */
205
207
  export interface MdxToJsResult {
@@ -207,6 +209,8 @@ export interface MdxToJsResult {
207
209
  code: string;
208
210
  /** Frontmatter block at the start of the document, or `null` if none. */
209
211
  frontmatter: Frontmatter | null;
212
+ /** Document-level data bag populated by plugins via `ctx.data`. Empty `{}` if untouched. */
213
+ data: Data;
210
214
  }
211
215
  type AnyFn = (...args: any[]) => unknown;
212
216
  type ReturnsPromise<F> = F extends AnyFn ? Extract<ReturnType<F>, Promise<unknown>> extends never ? false : true : false;