satteri 0.8.1 → 0.9.1

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 +54 -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 +13 -1
  9. package/dist/compile.js +44 -28
  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,57 @@ 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 unless you seed one with the `data` option.
186
+
187
+ To seed data into the bag before plugins run, pass an object to the `data` option. The same object is available in all plugins and returned as `result.data` after compilation, so you can pre-populate it with values or methods that plugins can read and update.
188
+
189
+ ```ts
190
+ const data = { title: "Hello" };
191
+ const rewriteTitle = defineMdastPlugin({
192
+ name: "rewrite-title",
193
+ heading(_node, ctx) {
194
+ ctx.data.title = "Updated";
195
+ },
196
+ });
197
+
198
+ const result = markdownToHtml("# A", { mdastPlugins: [rewriteTitle], data });
199
+ console.log(result.data === data); // true, the seeded object is returned
200
+ console.log(data.title); // "Updated"
201
+ ```
202
+
203
+ By default keys are typed as `unknown`. To give a key a type, augment the `DataMap` interface:
204
+
205
+ ```ts
206
+ declare module "satteri" {
207
+ interface DataMap {
208
+ headings: string[];
209
+ }
210
+ }
211
+
212
+ // now ctx.data.headings and result.data.headings are typed as string[] | undefined
213
+ ```
214
+
215
+ Unregistered keys stay `unknown`, so the bag remains open-ended.
216
+
166
217
  ### How transforms compose
167
218
 
168
219
  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:
@@ -299,6 +350,7 @@ interface CompileOptions {
299
350
  hastPlugins?: HastPluginDefinition[];
300
351
  features?: Features;
301
352
  fileURL?: URL;
353
+ data?: Data;
302
354
  }
303
355
 
304
356
  // mdxToJs accepts MdxCompileOptions, which extends CompileOptions
@@ -309,6 +361,8 @@ interface MdxCompileOptions extends CompileOptions {
309
361
 
310
362
  `fileURL` is the `URL` of the document being processed, surfaced to plugins as `ctx.fileURL` (a `URL`, or `undefined` when omitted). Pass a file URL such as Astro's `fileURL`, or convert a filesystem path with Node's `pathToFileURL`; read it back with `fileURLToPath(ctx.fileURL)`.
311
363
 
364
+ `data` seeds the document-level data bag before plugins run, surfaced as `ctx.data` and returned as `result.data`. See [Sharing data between plugins](#sharing-data-between-plugins).
365
+
312
366
  `Features` controls the Markdown extensions Sätteri's parser recognizes: `gfm`, `frontmatter`, `math`, `directive`, `smartPunctuation`, etc. `gfm`, `math`, and `smartPunctuation` also accept granular options. For example, `math: { singleDollarTextMath: false }` keeps single `$` as literal text. See the [Features reference](https://satteri.dev/docs/features/) for the full list.
313
367
 
314
368
  ## License
@@ -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
  }