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.
- package/README.md +54 -0
- package/dist/byte-writer.d.ts +31 -0
- package/dist/byte-writer.js +106 -0
- package/dist/child-stub.d.ts +17 -0
- package/dist/child-stub.js +57 -0
- package/dist/command-buffer.d.ts +23 -26
- package/dist/command-buffer.js +64 -132
- package/dist/compile.d.ts +13 -1
- package/dist/compile.js +44 -28
- package/dist/directive-types.d.ts +4 -2
- package/dist/generated/arena-layout.d.ts +37 -0
- package/dist/generated/arena-layout.js +41 -0
- package/dist/generated/wire-constants.d.ts +51 -0
- package/dist/generated/wire-constants.js +63 -0
- package/dist/handles.d.ts +21 -0
- package/dist/handles.js +1 -0
- package/dist/hast/child-stub.d.ts +18 -0
- package/dist/hast/child-stub.js +43 -0
- package/dist/hast/element-props.d.ts +2 -0
- package/dist/hast/element-props.js +23 -0
- package/dist/hast/generated/node-types.d.ts +14 -0
- package/dist/hast/generated/node-types.js +64 -0
- package/dist/hast/hast-materializer.d.ts +2 -1
- package/dist/hast/hast-materializer.js +9 -45
- package/dist/hast/hast-reader.d.ts +18 -23
- package/dist/hast/hast-reader.js +48 -115
- package/dist/hast/hast-visitor.d.ts +35 -13
- package/dist/hast/hast-visitor.js +395 -302
- package/dist/index.d.ts +10 -4
- package/dist/index.js +23 -1
- package/dist/lazy-child-resolver.d.ts +36 -0
- package/dist/lazy-child-resolver.js +95 -0
- package/dist/lazy-props.d.ts +1 -1
- package/dist/mdast/child-stub.d.ts +18 -0
- package/dist/mdast/child-stub.js +46 -0
- package/dist/mdast/column-align.d.ts +1 -0
- package/dist/mdast/column-align.js +6 -0
- package/dist/mdast/generated/layout.d.ts +19 -0
- package/dist/mdast/generated/layout.js +272 -0
- package/dist/mdast/generated/node-types.d.ts +58 -0
- package/dist/mdast/generated/node-types.js +266 -0
- package/dist/mdast/mdast-materializer.d.ts +3 -2
- package/dist/mdast/mdast-materializer.js +9 -93
- package/dist/mdast/mdast-reader.d.ts +4 -120
- package/dist/mdast/mdast-reader.js +45 -272
- package/dist/mdast/mdast-visitor.d.ts +52 -14
- package/dist/mdast/mdast-visitor.js +348 -427
- package/dist/mdx-attr.d.ts +2 -0
- package/dist/mdx-attr.js +20 -0
- package/dist/op-stream.d.ts +38 -0
- package/dist/op-stream.js +125 -0
- package/dist/phantom.d.ts +2 -0
- package/dist/phantom.js +13 -0
- package/dist/types.d.ts +34 -4
- package/dist/visitor-shared.d.ts +28 -0
- package/dist/visitor-shared.js +52 -0
- package/dist/wire-read.d.ts +27 -0
- package/dist/wire-read.js +59 -0
- package/index.js +52 -52
- 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
|
+
}
|
package/dist/command-buffer.d.ts
CHANGED
|
@@ -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
|
|
6
|
-
* (
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
private
|
|
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
|
-
/**
|
|
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 {};
|
package/dist/command-buffer.js
CHANGED
|
@@ -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
|
|
6
|
-
* (
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
59
|
-
this.
|
|
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
|
|
41
|
+
let str;
|
|
67
42
|
if (value === null || value === undefined) {
|
|
68
43
|
valueType = PROP_NULL;
|
|
69
|
-
|
|
44
|
+
str = "";
|
|
70
45
|
}
|
|
71
46
|
else if (value === true) {
|
|
72
47
|
valueType = PROP_BOOL_TRUE;
|
|
73
|
-
|
|
48
|
+
str = "";
|
|
74
49
|
}
|
|
75
50
|
else if (value === false) {
|
|
76
51
|
valueType = PROP_BOOL_FALSE;
|
|
77
|
-
|
|
52
|
+
str = "";
|
|
78
53
|
}
|
|
79
54
|
else if (typeof value === "number") {
|
|
80
55
|
valueType = PROP_INT;
|
|
81
|
-
|
|
56
|
+
str = String(value);
|
|
82
57
|
}
|
|
83
58
|
else if (Array.isArray(value)) {
|
|
84
59
|
valueType = PROP_SPACE_SEP;
|
|
85
|
-
|
|
60
|
+
str = value.join(" ");
|
|
86
61
|
}
|
|
87
62
|
else {
|
|
88
63
|
valueType = PROP_STRING;
|
|
89
|
-
|
|
64
|
+
str = String(value);
|
|
90
65
|
}
|
|
91
|
-
// 1(cmd) + 4(nodeId) + 1(valueType)
|
|
92
|
-
this.
|
|
93
|
-
this.
|
|
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.
|
|
96
|
-
this.
|
|
97
|
-
this.
|
|
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
|
-
/**
|
|
120
|
-
|
|
121
|
-
this.
|
|
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 (
|
|
124
|
-
|
|
125
|
-
this.
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
this.writeRawJsonCommand(CMD_INSERT_BEFORE, nodeId, json);
|
|
107
|
+
insertBeforeOpstream(nodeId, ops) {
|
|
108
|
+
this.writeOpstreamCommand(CMD_INSERT_BEFORE, nodeId, ops);
|
|
130
109
|
}
|
|
131
|
-
|
|
132
|
-
this.
|
|
110
|
+
insertAfterOpstream(nodeId, ops) {
|
|
111
|
+
this.writeOpstreamCommand(CMD_INSERT_AFTER, nodeId, ops);
|
|
133
112
|
}
|
|
134
|
-
|
|
135
|
-
this.
|
|
113
|
+
prependChildOpstream(nodeId, ops) {
|
|
114
|
+
this.writeOpstreamCommand(CMD_PREPEND_CHILD, nodeId, ops);
|
|
136
115
|
}
|
|
137
|
-
|
|
138
|
-
this.
|
|
116
|
+
appendChildOpstream(nodeId, ops) {
|
|
117
|
+
this.writeOpstreamCommand(CMD_APPEND_CHILD, nodeId, ops);
|
|
139
118
|
}
|
|
140
|
-
|
|
141
|
-
this.
|
|
119
|
+
wrapNodeOpstream(nodeId, ops) {
|
|
120
|
+
this.writeOpstreamCommand(CMD_WRAP, nodeId, ops);
|
|
142
121
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
this.
|
|
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.
|
|
149
|
-
this.writeU32(
|
|
150
|
-
this.
|
|
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
|
|
155
|
-
}
|
|
156
|
-
/** Number of bytes written so far. */
|
|
157
|
-
get length() {
|
|
158
|
-
return this.offset;
|
|
133
|
+
return this.take();
|
|
159
134
|
}
|
|
160
|
-
/** Reset
|
|
135
|
+
/** Reset for reuse, releasing the old buffer (handed-out views stay intact). */
|
|
161
136
|
reset() {
|
|
162
|
-
this.
|
|
163
|
-
|
|
164
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|