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
@@ -5,21 +5,14 @@
5
5
  * replays straight into the arena (`replay_opstream` in js_commands.rs; byte
6
6
  * values in generated/wire-constants.ts) — the only structural encoding for
7
7
  * plugin-built content. Strings ride ByteWriter's zero-alloc path (inline
8
- * char codes when short ASCII, `encodeInto` otherwise).
8
+ * char codes when short ASCII, `encodeInto` otherwise). `CommandBuffer`
9
+ * extends this class so payloads are emitted directly into the command
10
+ * bytes, with no intermediate opstream buffer or copy.
9
11
  */
10
12
  import { ByteWriter } from "./byte-writer.js";
11
- export { OF_VALUE, OF_URL, OF_TITLE, OF_ALT, OF_LANG, OF_META, OF_IDENTIFIER, OF_LABEL, OF_NAME, OF_REFERENCE_TYPE, OF_DEPTH, OF_CHECKED, OF_START, OF_ORDERED, OF_SPREAD, OF_TAGNAME, OF_EXPLICIT, PROP_STRING, PROP_BOOL_TRUE, PROP_BOOL_FALSE, PROP_SPACE_SEP, PROP_COMMA_SEP, PROP_INT, PROP_NULL, MDX_ATTR_BOOLEAN_PROP, MDX_ATTR_LITERAL_PROP, MDX_ATTR_EXPRESSION_PROP, MDX_ATTR_SPREAD, } from "./generated/wire-constants.js";
13
+ export { OF_VALUE, OF_URL, OF_TITLE, OF_ALT, OF_LANG, OF_META, OF_IDENTIFIER, OF_LABEL, OF_NAME, OF_REFERENCE_TYPE, OF_DEPTH, OF_CHECKED, OF_START, OF_ORDERED, OF_SPREAD, OF_TAGNAME, OF_EXPLICIT, PROP_STRING, PROP_BOOL_TRUE, PROP_BOOL_FALSE, PROP_SPACE_SEP, PROP_INT, PROP_NULL, MDX_ATTR_BOOLEAN_PROP, MDX_ATTR_LITERAL_PROP, MDX_ATTR_EXPRESSION_PROP, MDX_ATTR_SPREAD, } from "./generated/wire-constants.js";
12
14
  export declare class OpWriter extends ByteWriter {
13
- #private;
14
- constructor();
15
- /**
16
- * Start a compile on this (shared, module-level) writer. Encoding evaluates
17
- * plugin-supplied getters and `toJSON`; if one of those calls a context
18
- * mutation, the nested compile would reset the stream under the outer one
19
- * and corrupt it silently — throw instead. Pair with `end` in a `finally`.
20
- */
21
- begin(): void;
22
- end(): void;
15
+ constructor(initialSize?: number);
23
16
  open(type: number): void;
24
17
  close(): void;
25
18
  str(field: number, s: string): void;
package/dist/op-stream.js CHANGED
@@ -5,32 +5,17 @@
5
5
  * replays straight into the arena (`replay_opstream` in js_commands.rs; byte
6
6
  * values in generated/wire-constants.ts) — the only structural encoding for
7
7
  * plugin-built content. Strings ride ByteWriter's zero-alloc path (inline
8
- * char codes when short ASCII, `encodeInto` otherwise).
8
+ * char codes when short ASCII, `encodeInto` otherwise). `CommandBuffer`
9
+ * extends this class so payloads are emitted directly into the command
10
+ * bytes, with no intermediate opstream buffer or copy.
9
11
  */
10
12
  import { ByteWriter } from "./byte-writer.js";
11
13
  import { OP_OPEN, OP_CLOSE, OP_REF, OP_KEEP_CHILDREN, OP_STR, OP_U8, OP_U32, OP_BOOL, OP_DATA, OP_PROP, OP_ALIGN, MDX_ATTR_BOOLEAN_PROP, MDX_ATTR_LITERAL_PROP, MDX_ATTR_EXPRESSION_PROP, MDX_ATTR_SPREAD, } from "./generated/wire-constants.js";
12
14
  // Re-exported so visitors/readers keep importing wire constants from here.
13
- export { OF_VALUE, OF_URL, OF_TITLE, OF_ALT, OF_LANG, OF_META, OF_IDENTIFIER, OF_LABEL, OF_NAME, OF_REFERENCE_TYPE, OF_DEPTH, OF_CHECKED, OF_START, OF_ORDERED, OF_SPREAD, OF_TAGNAME, OF_EXPLICIT, PROP_STRING, PROP_BOOL_TRUE, PROP_BOOL_FALSE, PROP_SPACE_SEP, PROP_COMMA_SEP, PROP_INT, PROP_NULL, MDX_ATTR_BOOLEAN_PROP, MDX_ATTR_LITERAL_PROP, MDX_ATTR_EXPRESSION_PROP, MDX_ATTR_SPREAD, } from "./generated/wire-constants.js";
15
+ export { OF_VALUE, OF_URL, OF_TITLE, OF_ALT, OF_LANG, OF_META, OF_IDENTIFIER, OF_LABEL, OF_NAME, OF_REFERENCE_TYPE, OF_DEPTH, OF_CHECKED, OF_START, OF_ORDERED, OF_SPREAD, OF_TAGNAME, OF_EXPLICIT, PROP_STRING, PROP_BOOL_TRUE, PROP_BOOL_FALSE, PROP_SPACE_SEP, PROP_INT, PROP_NULL, MDX_ATTR_BOOLEAN_PROP, MDX_ATTR_LITERAL_PROP, MDX_ATTR_EXPRESSION_PROP, MDX_ATTR_SPREAD, } from "./generated/wire-constants.js";
14
16
  export class OpWriter extends ByteWriter {
15
- #encoding = false;
16
- constructor() {
17
- super(512);
18
- }
19
- /**
20
- * Start a compile on this (shared, module-level) writer. Encoding evaluates
21
- * plugin-supplied getters and `toJSON`; if one of those calls a context
22
- * mutation, the nested compile would reset the stream under the outer one
23
- * and corrupt it silently — throw instead. Pair with `end` in a `finally`.
24
- */
25
- begin() {
26
- if (this.#encoding) {
27
- throw new Error("reentrant op-stream compile: a node getter or toJSON invoked a context mutation while its node was being encoded");
28
- }
29
- this.#encoding = true;
30
- this.reset();
31
- }
32
- end() {
33
- this.#encoding = false;
17
+ constructor(initialSize = 512) {
18
+ super(initialSize);
34
19
  }
35
20
  open(type) {
36
21
  this.ensure(2);
package/dist/plugin.d.ts CHANGED
@@ -1,20 +1,46 @@
1
1
  import type { MdastPluginInstance } from "./mdast/mdast-visitor.js";
2
2
  import type { HastVisitorInstance } from "./hast/hast-visitor.js";
3
+ import type { Data, SourceFormat } from "./types.js";
4
+ /**
5
+ * What a plugin factory is told about the document, before it is parsed.
6
+ *
7
+ * Return `null`, `undefined` or `false` from the factory to leave the plugin
8
+ * out of the pipeline for this document.
9
+ */
10
+ export interface PluginFactoryContext {
11
+ /** The `fileURL` compile option, or `undefined` when none was given. */
12
+ readonly fileURL: URL | undefined;
13
+ /** Which kind of document is being compiled. */
14
+ readonly sourceFormat: SourceFormat;
15
+ /** The unparsed source, minus a leading BOM as the parser sees it. Intended
16
+ * for cheap checks, not for parsing Markdown. */
17
+ readonly source: string;
18
+ /** The document-level data bag, before any plugin has run. */
19
+ readonly data: Data;
20
+ }
3
21
  export type MdastPluginDefinition = MdastPluginInstance & {
4
22
  name: string;
5
23
  };
6
24
  export type HastPluginDefinition = HastVisitorInstance & {
7
25
  name: string;
8
26
  };
9
- /**
10
- * Entry accepted by `mdastPlugins`: a definition reused across documents,
11
- * or a factory called once per compile so closures reset per document.
12
- */
13
- export type MdastPluginInput = MdastPluginDefinition | (() => MdastPluginDefinition);
14
- /**
15
- * Entry accepted by `hastPlugins`: a definition reused across documents,
16
- * or a factory called once per compile so closures reset per document.
17
- */
18
- export type HastPluginInput = HastPluginDefinition | (() => HastPluginDefinition);
27
+ type PluginEntry<D> = D | ((ctx: PluginFactoryContext) => PluginEntry<D>) | readonly PluginEntry<D>[] | null | undefined | false;
28
+ /** Entry accepted by `mdastPlugins`. */
29
+ export type MdastPluginEntry = PluginEntry<MdastPluginDefinition>;
30
+ /** Entry accepted by `hastPlugins`. */
31
+ export type HastPluginEntry = PluginEntry<HastPluginDefinition>;
32
+ /** Value accepted by the `mdastPlugins` option. */
33
+ export type MdastPluginList = readonly MdastPluginEntry[];
34
+ /** Value accepted by the `hastPlugins` option. */
35
+ export type HastPluginList = readonly HastPluginEntry[];
36
+ /** Older name for {@link MdastPluginEntry}. */
37
+ export type MdastPluginInput = MdastPluginEntry;
38
+ /** Older name for {@link HastPluginEntry}. */
39
+ export type HastPluginInput = HastPluginEntry;
40
+ /** The one place a plugin option becomes the definition array the pipeline
41
+ * runs. Factories resolve here and nowhere else, so each is called once per
42
+ * compile no matter how deeply it is nested. */
43
+ export declare function normalizePlugins<D>(entries: readonly PluginEntry<D>[], option: string, source: string, fileURL: URL | undefined, sourceFormat: SourceFormat, data: Data): D[];
19
44
  export declare function defineMdastPlugin<P extends MdastPluginDefinition>(definition: P): P;
20
45
  export declare function defineHastPlugin<P extends HastPluginDefinition>(definition: P): P;
46
+ export {};
package/dist/plugin.js CHANGED
@@ -1,6 +1,54 @@
1
+ /** Bounds factory-in-factory nesting. Real presets nest one level; anything
2
+ * deeper is a factory that leads back to itself, which would otherwise recurse
3
+ * until the stack overflows. */
4
+ const MAX_FACTORY_DEPTH = 10;
5
+ /** The one place a plugin option becomes the definition array the pipeline
6
+ * runs. Factories resolve here and nowhere else, so each is called once per
7
+ * compile no matter how deeply it is nested. */
8
+ export function normalizePlugins(entries, option, source, fileURL, sourceFormat, data) {
9
+ const out = [];
10
+ // Built lazily so a list with no factories allocates no context.
11
+ let ctx;
12
+ const walk = (entry, factoryDepth) => {
13
+ if (entry === null || entry === undefined || entry === false)
14
+ return;
15
+ if (Array.isArray(entry)) {
16
+ for (const item of entry)
17
+ walk(item, factoryDepth);
18
+ return;
19
+ }
20
+ if (typeof entry === "function") {
21
+ if (factoryDepth === 0) {
22
+ throw new Error(`${option}: plugin factory nesting is too deep. A factory most likely returns itself. ` +
23
+ `A factory may return a plugin or a list of plugins, but that list must not lead back to the same factory.`);
24
+ }
25
+ // `data` stays mutable on purpose: it is the live bag the visitors share.
26
+ ctx ??= Object.freeze({
27
+ fileURL,
28
+ sourceFormat,
29
+ // The parser drops a leading BOM, so `ctx.source` in a visitor lacks it too.
30
+ source: source.startsWith("\uFEFF") ? source.slice(1) : source,
31
+ data,
32
+ });
33
+ walk(entry(ctx), factoryDepth - 1);
34
+ return;
35
+ }
36
+ if (typeof entry !== "object") {
37
+ throw new Error(`${option}: expected a plugin, a factory, a list, or null/undefined/false`);
38
+ }
39
+ if (typeof entry.then === "function") {
40
+ throw new Error(`${option}: a Promise is not a plugin. Plugin factories must be synchronous; ` +
41
+ `await the value first and pass the plugin itself.`);
42
+ }
43
+ out.push(entry);
44
+ };
45
+ for (const entry of entries)
46
+ walk(entry, MAX_FACTORY_DEPTH);
47
+ return out;
48
+ }
1
49
  // Generic so the inferred plugin type preserves each visitor's *actual* return
2
- // type. That lets call sites of `markdownToHtml`/`mdxToJs` distinguish sync
3
- // plugins from async ones in their conditional return type.
50
+ // type. That lets the compile entry points distinguish sync plugins from async
51
+ // ones in their conditional return type.
4
52
  export function defineMdastPlugin(definition) {
5
53
  if (!definition.name) {
6
54
  throw new Error("Plugin definition must have a name");
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Byte-offset to UTF-16-offset remap for a wire string pool holding multibyte
3
+ * sequences, so the once-decoded pool can still be sliced with `substring`
4
+ * instead of a `TextDecoder` call per string.
5
+ */
6
+ export declare class PoolOffsets {
7
+ #private;
8
+ /** `extraBytes` is the pool's byte length minus its UTF-16 length, which
9
+ * bounds the multibyte character count: each one costs at least one byte. */
10
+ constructor(bytes: Uint8Array, extraBytes: number);
11
+ /** Both ends of a string ref sit on character boundaries, so the remap is exact. */
12
+ slice(text: string, offset: number, len: number): string;
13
+ }
@@ -0,0 +1,91 @@
1
+ const NO_WORDS = new Uint32Array(0);
2
+ /**
3
+ * Byte-offset to UTF-16-offset remap for a wire string pool holding multibyte
4
+ * sequences, so the once-decoded pool can still be sliced with `substring`
5
+ * instead of a `TextDecoder` call per string.
6
+ */
7
+ export class PoolOffsets {
8
+ #starts;
9
+ #shifts;
10
+ #count;
11
+ #hint = 0;
12
+ /** `extraBytes` is the pool's byte length minus its UTF-16 length, which
13
+ * bounds the multibyte character count: each one costs at least one byte. */
14
+ constructor(bytes, extraBytes) {
15
+ const starts = new Uint32Array(extraBytes);
16
+ const shifts = new Uint32Array(extraBytes);
17
+ const n = bytes.length;
18
+ const pad = (4 - (bytes.byteOffset & 3)) & 3;
19
+ const wordCount = n > pad ? (n - pad) >> 2 : 0;
20
+ // A pool shorter than its alignment padding would put the view past the buffer.
21
+ const words = wordCount === 0 ? NO_WORDS : new Uint32Array(bytes.buffer, bytes.byteOffset + pad, wordCount);
22
+ let count = 0;
23
+ let shift = 0;
24
+ let i = 0;
25
+ for (;;) {
26
+ // `i` is always at a character boundary, so a word with no high bits set
27
+ // cannot hide the start of one and the whole word is skippable.
28
+ if (i >= pad && ((i - pad) & 3) === 0) {
29
+ let w = (i - pad) >> 2;
30
+ while (w < wordCount && ((words[w] ?? 0) & 0x80808080) === 0)
31
+ w++;
32
+ i = pad + (w << 2);
33
+ }
34
+ if (i >= n)
35
+ break;
36
+ const lead = bytes[i] ?? 0;
37
+ if (lead < 0x80) {
38
+ i++;
39
+ continue;
40
+ }
41
+ starts[count] = i;
42
+ if (lead < 0xe0) {
43
+ shift += 1;
44
+ i += 2;
45
+ }
46
+ else if (lead < 0xf0) {
47
+ shift += 2;
48
+ i += 3;
49
+ }
50
+ else {
51
+ shift += 2;
52
+ i += 4;
53
+ }
54
+ shifts[count] = shift;
55
+ count++;
56
+ }
57
+ this.#starts = starts;
58
+ this.#shifts = shifts;
59
+ this.#count = count;
60
+ }
61
+ /** How many multibyte characters start strictly before `byteOffset`. */
62
+ #seek(byteOffset) {
63
+ const starts = this.#starts;
64
+ const count = this.#count;
65
+ const hint = this.#hint;
66
+ if ((hint === count || (starts[hint] ?? 0) >= byteOffset) &&
67
+ (hint === 0 || (starts[hint - 1] ?? 0) < byteOffset)) {
68
+ return hint;
69
+ }
70
+ let lo = 0;
71
+ let hi = count;
72
+ while (lo < hi) {
73
+ const mid = (lo + hi) >> 1;
74
+ if ((starts[mid] ?? 0) < byteOffset)
75
+ lo = mid + 1;
76
+ else
77
+ hi = mid;
78
+ }
79
+ this.#hint = lo;
80
+ return lo;
81
+ }
82
+ #shiftAt(index) {
83
+ return index === 0 ? 0 : (this.#shifts[index - 1] ?? 0);
84
+ }
85
+ /** Both ends of a string ref sit on character boundaries, so the remap is exact. */
86
+ slice(text, offset, len) {
87
+ const start = offset - this.#shiftAt(this.#seek(offset));
88
+ const endByte = offset + len;
89
+ return text.substring(start, endByte - this.#shiftAt(this.#seek(endByte)));
90
+ }
91
+ }
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Position } from "unist";
2
- import type { Literal as MdastLiteral, Nodes as MdastStdNodes, Parent as MdastParent, PhrasingContent } from "mdast";
2
+ import type { Data as MdastData, Literal as MdastLiteral, Nodes as MdastStdNodes, Parent as MdastParent, PhrasingContent, BlockContent } from "mdast";
3
3
  import type { Literal as HastLiteral, Nodes as HastStdNodes } from "hast";
4
4
  export type { Position, Point } from "unist";
5
5
  export type { MdxJsxFlowElement, MdxJsxTextElement, MdxJsxAttribute as MdxJsxAttributeNode, MdxJsxExpressionAttribute as MdxJsxExpressionAttributeNode, MdxJsxAttributeValueExpression as MdxJsxAttributeValueExpressionNode, MdxFlowExpression, MdxTextExpression, MdxjsEsm, MdxJsxFlowElementHast, MdxJsxTextElementHast, MdxFlowExpressionHast, MdxTextExpressionHast, MdxjsEsmHast, } from "./mdx-types.js";
@@ -24,6 +24,40 @@ export interface Subscript extends MdastParent {
24
24
  type: "subscript";
25
25
  children: PhrasingContent[];
26
26
  }
27
+ export interface DescriptionList extends MdastParent {
28
+ type: "descriptionList";
29
+ children: (DescriptionTerm | DescriptionDetails)[];
30
+ }
31
+ export interface DescriptionTerm extends MdastParent {
32
+ type: "descriptionTerm";
33
+ children: PhrasingContent[];
34
+ }
35
+ export interface DescriptionDetails extends MdastParent {
36
+ type: "descriptionDetails";
37
+ children: BlockContent[];
38
+ /** Loose definitions wrap their content in a `<p>`; tight ones don't. */
39
+ spread?: boolean;
40
+ }
41
+ /**
42
+ * A user-defined mdast node. `type` is an arbitrary string the plugin chooses
43
+ * (e.g. `"section"`); it round-trips and is surfaced back as `node.type`. Pass
44
+ * one to `ctx.replaceNode` / `insertBefore` / `appendChild`, etc.
45
+ *
46
+ * Works as either shape:
47
+ * - a **parent** with `children` renders to an element through `data.hName`
48
+ * (default `<div>`), merges `data.hProperties`, and renders its children;
49
+ * - a **leaf** with a non-empty `value` (and no `children` or `data.h*`)
50
+ * renders to an HTML text node.
51
+ *
52
+ * Only these fields survive the round trip; put anything else in `data`.
53
+ */
54
+ export interface Custom {
55
+ type: string;
56
+ children?: (MdastNode | Custom)[];
57
+ value?: string;
58
+ data?: MdastData;
59
+ position?: Position;
60
+ }
27
61
  declare module "mdast" {
28
62
  interface FrontmatterContentMap {
29
63
  toml: Toml;
@@ -34,6 +68,9 @@ declare module "mdast" {
34
68
  inlineMath: InlineMath;
35
69
  superscript: Superscript;
36
70
  subscript: Subscript;
71
+ descriptionList: DescriptionList;
72
+ descriptionTerm: DescriptionTerm;
73
+ descriptionDetails: DescriptionDetails;
37
74
  }
38
75
  interface PhrasingContentMap {
39
76
  inlineMath: InlineMath;
@@ -86,6 +123,12 @@ export interface DataMap {
86
123
  * string key holding any value, plus the typed keys registered in {@link DataMap}.
87
124
  */
88
125
  export type Data = Record<string, unknown> & Partial<DataMap>;
126
+ /**
127
+ * The source format a plugin is running against, surfaced as `ctx.sourceFormat`:
128
+ * `"markdown"` for a plain Markdown compile, `"mdx"` for an MDX one. Lets a
129
+ * plugin shared between both pipelines branch on which it is handling.
130
+ */
131
+ export type SourceFormat = "markdown" | "mdx";
89
132
  export interface StringRefRaw {
90
133
  offset: number;
91
134
  len: number;
@@ -5,19 +5,42 @@
5
5
  * it polymorphic.
6
6
  */
7
7
  import type { CommandBuffer } from "./command-buffer.js";
8
+ /** Plugin-level configuration, set via `options` on a plugin definition. */
9
+ export interface PluginOptions {
10
+ /**
11
+ * Set to `true` if any visitor reads `node.position`. When no plugin in the
12
+ * pipeline opts in, source-position tracking is skipped during parsing
13
+ * (~15% faster parse), and `node.position` is `undefined`.
14
+ */
15
+ position?: boolean;
16
+ }
17
+ export declare const ROOT_NODE_ID = 0;
18
+ export declare function rootReplacementError(content: unknown): Error;
19
+ /** Hooks subscribe to node 0 by node type, so a document left headed by anything
20
+ * but a `root` silently stops firing them, in this phase and every later one. */
21
+ export declare function requireRootReplacement<T>(content: T): T;
8
22
  export declare function asArray<T>(value: T | T[]): T[];
9
23
  /** Thrown when declarative replacement content can't be compiled to the
10
24
  * structural op-stream — an unsupported node type (e.g. a bare `root`/`doctype`
11
25
  * handed in as content) or an out-of-range numeric field. The op-stream is the
12
26
  * only structural encoding, so this is a hard error rather than a fallback. */
13
27
  export declare function unencodableContentError(content: unknown): Error;
28
+ /** Per handle, so membership alone proves ownership and the read path needs no marker of its own. */
29
+ export type NodeRefs = WeakMap<object, number>;
30
+ /** Separates "belongs to another tree" from "never had an id"; arena ids are never negative. */
31
+ export declare const FOREIGN_REF = -1;
32
+ /** `_refs` rides on the prototype, surviving neither a spread copy nor an object literal. */
33
+ export declare function crossPipelineForeign(node: object): number | undefined;
14
34
  /**
15
35
  * Arena id for a node passed to a context method, via a per-kind `nid` lookup
16
- * (closure keeps each kind's call site monomorphic). Plugin-built nodes have
17
- * no id; without this check the id would coerce to 0 in the command buffer
18
- * and the mutation would silently target the document root.
36
+ * (closure keeps each kind's call site monomorphic) resolved against the
37
+ * edited tree's own refs. Plugin-built nodes have no id; without this check
38
+ * the id would coerce to 0 in the command buffer and the mutation would
39
+ * silently target the document root. A node read from another tree has an id
40
+ * that is meaningless here, and one that happens to be in range would edit an
41
+ * unrelated node.
19
42
  */
20
- export declare function makeRequireNid<TNode>(nid: (node: TNode) => number | undefined): (node: TNode, method: string) => number;
43
+ export declare function makeRequireNid<TNode>(nid: (node: TNode, refs: NodeRefs) => number | undefined): (node: TNode, method: string, refs: NodeRefs) => number;
21
44
  /** Concatenate the return-value and context command buffers for one pass,
22
45
  * resetting both for reuse. */
23
46
  export declare function mergeAndReset(returnBuffer: CommandBuffer, ctx: {
@@ -5,6 +5,19 @@
5
5
  * it polymorphic.
6
6
  */
7
7
  const EMPTY_BYTES = new Uint8Array(0);
8
+ export const ROOT_NODE_ID = 0;
9
+ export function rootReplacementError(content) {
10
+ const type = content?.type;
11
+ return new Error(`satteri: replaceNode on the document root takes a \`root\`${typeof type === "string" ? `, not "${type}"` : ""}. Pass { type: "root", children: [...] } to swap the document, ` +
12
+ 'or setProperty(root, "children", [...]) to swap only its children.');
13
+ }
14
+ /** Hooks subscribe to node 0 by node type, so a document left headed by anything
15
+ * but a `root` silently stops firing them, in this phase and every later one. */
16
+ export function requireRootReplacement(content) {
17
+ if (content?.type === "root")
18
+ return content;
19
+ throw rootReplacementError(content);
20
+ }
8
21
  export function asArray(value) {
9
22
  return Array.isArray(value) ? value : [value];
10
23
  }
@@ -17,20 +30,33 @@ export function unencodableContentError(content) {
17
30
  return new Error(`satteri: cannot encode replacement content${typeof type === "string" ? ` of type "${type}"` : ""} ` +
18
31
  "into the structural op-stream — unsupported node type or out-of-range numeric field.");
19
32
  }
33
+ /** Separates "belongs to another tree" from "never had an id"; arena ids are never negative. */
34
+ export const FOREIGN_REF = -1;
35
+ /** `_refs` rides on the prototype, surviving neither a spread copy nor an object literal. */
36
+ export function crossPipelineForeign(node) {
37
+ // Probed before the own-property call: plugin-built content misses here, and that is the hot case.
38
+ if (node._refs === undefined)
39
+ return undefined;
40
+ return Object.hasOwn(node, "_refs") ? undefined : FOREIGN_REF;
41
+ }
20
42
  /**
21
43
  * Arena id for a node passed to a context method, via a per-kind `nid` lookup
22
- * (closure keeps each kind's call site monomorphic). Plugin-built nodes have
23
- * no id; without this check the id would coerce to 0 in the command buffer
24
- * and the mutation would silently target the document root.
44
+ * (closure keeps each kind's call site monomorphic) resolved against the
45
+ * edited tree's own refs. Plugin-built nodes have no id; without this check
46
+ * the id would coerce to 0 in the command buffer and the mutation would
47
+ * silently target the document root. A node read from another tree has an id
48
+ * that is meaningless here, and one that happens to be in range would edit an
49
+ * unrelated node.
25
50
  */
26
51
  export function makeRequireNid(nid) {
27
- return (node, method) => {
28
- const id = nid(node);
29
- if (id === undefined) {
30
- throw new Error(`${method}: node has no arena id — it was built in JS, not read from this tree. ` +
31
- `Pass plugin-built nodes as new content (e.g. the second argument of insertAfter).`);
32
- }
33
- return id;
52
+ return (node, method, refs) => {
53
+ const id = nid(node, refs);
54
+ if (id !== undefined && id !== FOREIGN_REF)
55
+ return id;
56
+ throw new Error(`${method}: invalid node id — this node has no id in the tree being edited. Either it was ` +
57
+ `built in JS, in which case pass it as new content (e.g. the second argument of ` +
58
+ `insertAfter), or it was read from another tree (a different document, the mdast phase of ` +
59
+ `this one, or an earlier compile), in which case match it again in this pass.`);
34
60
  };
35
61
  }
36
62
  /** Concatenate the return-value and context command buffers for one pass,
package/dist/wire-read.js CHANGED
@@ -3,7 +3,9 @@
3
3
  * the hand-written decoders and the generated layout decoder so both interpret
4
4
  * the bytes identically.
5
5
  */
6
- const textDecoder = new TextDecoder("utf-8");
6
+ // `ignoreBOM` keeps a value-initial U+FEFF: each string is decoded from its
7
+ // own byte range, so the default BOM strip would eat it as content.
8
+ const textDecoder = new TextDecoder("utf-8", { ignoreBOM: true });
7
9
  /** Read a u16 (LE) at `off`. */
8
10
  export function ru16(view, off) {
9
11
  return view.getUint16(off, true);