satteri 0.8.0 → 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 -24
  7. package/dist/command-buffer.js +65 -128
  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 +38 -12
  28. package/dist/hast/hast-visitor.js +417 -281
  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 +55 -13
  47. package/dist/mdast/mdast-visitor.js +375 -410
  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
@@ -0,0 +1,2 @@
1
+ import type { MdxJsxAttributeUnion } from "./types.js";
2
+ export declare function decodeMdxJsxAttr(kind: number, name: string, value: string): MdxJsxAttributeUnion;
@@ -0,0 +1,20 @@
1
+ /** Decode an MDX JSX attribute from its wire `(kind, name, value)` — shared by
2
+ * the generated mdast tail decoder and the hast path so the kind dispatch lives
3
+ * once. Expression/spread values carry phantom-space sentinels, restored here. */
4
+ import { restorePhantomSpaces } from "./phantom.js";
5
+ export function decodeMdxJsxAttr(kind, name, value) {
6
+ switch (kind) {
7
+ case 0:
8
+ return { type: "mdxJsxAttribute", name, value: null };
9
+ case 1:
10
+ return { type: "mdxJsxAttribute", name, value };
11
+ case 2:
12
+ return {
13
+ type: "mdxJsxAttribute",
14
+ name,
15
+ value: { type: "mdxJsxAttributeValueExpression", value: restorePhantomSpaces(value) },
16
+ };
17
+ default:
18
+ return { type: "mdxJsxExpressionAttribute", value: restorePhantomSpaces(value) };
19
+ }
20
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Low-level op-stream writer shared by the MDAST/HAST declarative compilers.
3
+ *
4
+ * Emits the compact OPEN/CLOSE/field/REF/KEEP_CHILDREN/PROP stream that Rust
5
+ * replays straight into the arena (`replay_opstream` in js_commands.rs; byte
6
+ * values in generated/wire-constants.ts) — the only structural encoding for
7
+ * plugin-built content. Strings ride ByteWriter's zero-alloc path (inline
8
+ * char codes when short ASCII, `encodeInto` otherwise).
9
+ */
10
+ 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";
12
+ 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;
23
+ open(type: number): void;
24
+ close(): void;
25
+ str(field: number, s: string): void;
26
+ u8(field: number, v: number): void;
27
+ u32(field: number, v: number): void;
28
+ bool(field: number, v: boolean): void;
29
+ data(value: unknown): void;
30
+ prop(name: string, kind: number, value: string): void;
31
+ ref(id: number): void;
32
+ /** Table column-alignment codes (0=none, 1=left, 2=right, 3=center). */
33
+ align(codes: readonly number[]): void;
34
+ keepChildren(): void;
35
+ }
36
+ /** Emit one MDX JSX attribute as an OP_PROP, mirroring `encode_js_jsx_attrs`:
37
+ * null→boolean, string→literal, `{ value }` object→expression, else→boolean. */
38
+ export declare function emitMdxAttr(w: OpWriter, a: Record<string, unknown>): void;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Low-level op-stream writer shared by the MDAST/HAST declarative compilers.
3
+ *
4
+ * Emits the compact OPEN/CLOSE/field/REF/KEEP_CHILDREN/PROP stream that Rust
5
+ * replays straight into the arena (`replay_opstream` in js_commands.rs; byte
6
+ * values in generated/wire-constants.ts) — the only structural encoding for
7
+ * plugin-built content. Strings ride ByteWriter's zero-alloc path (inline
8
+ * char codes when short ASCII, `encodeInto` otherwise).
9
+ */
10
+ import { ByteWriter } from "./byte-writer.js";
11
+ 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
+ // 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";
14
+ 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;
34
+ }
35
+ open(type) {
36
+ this.ensure(2);
37
+ this.buf[this.n++] = OP_OPEN;
38
+ this.buf[this.n++] = type;
39
+ }
40
+ close() {
41
+ this.ensure(1);
42
+ this.buf[this.n++] = OP_CLOSE;
43
+ }
44
+ str(field, s) {
45
+ this.ensure(2);
46
+ this.buf[this.n++] = OP_STR;
47
+ this.buf[this.n++] = field;
48
+ this.utf8WithU32Len(s);
49
+ }
50
+ u8(field, v) {
51
+ this.ensure(3);
52
+ this.buf[this.n++] = OP_U8;
53
+ this.buf[this.n++] = field;
54
+ this.buf[this.n++] = v & 255;
55
+ }
56
+ u32(field, v) {
57
+ this.ensure(6);
58
+ this.buf[this.n++] = OP_U32;
59
+ this.buf[this.n++] = field;
60
+ this.writeU32(v);
61
+ }
62
+ bool(field, v) {
63
+ this.ensure(3);
64
+ this.buf[this.n++] = OP_BOOL;
65
+ this.buf[this.n++] = field;
66
+ this.buf[this.n++] = v ? 1 : 0;
67
+ }
68
+ data(value) {
69
+ const json = JSON.stringify(value);
70
+ // `JSON.stringify` yields undefined for a function or a `toJSON`
71
+ // returning undefined; a clear error beats a TypeError deep in the
72
+ // string encoder.
73
+ if (typeof json !== "string") {
74
+ throw new Error("node `data` is not JSON-serializable");
75
+ }
76
+ this.ensure(1);
77
+ this.buf[this.n++] = OP_DATA;
78
+ this.utf8WithU32Len(json);
79
+ }
80
+ prop(name, kind, value) {
81
+ this.ensure(1);
82
+ this.buf[this.n++] = OP_PROP;
83
+ this.utf8WithU32Len(name);
84
+ this.ensure(1);
85
+ this.buf[this.n++] = kind;
86
+ this.utf8WithU32Len(value);
87
+ }
88
+ ref(id) {
89
+ this.ensure(5);
90
+ this.buf[this.n++] = OP_REF;
91
+ this.writeU32(id);
92
+ }
93
+ /** Table column-alignment codes (0=none, 1=left, 2=right, 3=center). */
94
+ align(codes) {
95
+ this.ensure(5 + codes.length);
96
+ this.buf[this.n++] = OP_ALIGN;
97
+ this.writeU32(codes.length);
98
+ for (let i = 0; i < codes.length; i++)
99
+ this.buf[this.n++] = codes[i] & 255;
100
+ }
101
+ keepChildren() {
102
+ this.ensure(1);
103
+ this.buf[this.n++] = OP_KEEP_CHILDREN;
104
+ }
105
+ }
106
+ /** Emit one MDX JSX attribute as an OP_PROP, mirroring `encode_js_jsx_attrs`:
107
+ * null→boolean, string→literal, `{ value }` object→expression, else→boolean. */
108
+ export function emitMdxAttr(w, a) {
109
+ if (a.type === "mdxJsxExpressionAttribute") {
110
+ w.prop("", MDX_ATTR_SPREAD, typeof a.value === "string" ? a.value : "");
111
+ return;
112
+ }
113
+ const name = typeof a.name === "string" ? a.name : "";
114
+ const val = a.value;
115
+ if (typeof val === "string") {
116
+ w.prop(name, MDX_ATTR_LITERAL_PROP, val);
117
+ }
118
+ else if (val !== null && typeof val === "object" && !Array.isArray(val)) {
119
+ const expr = val.value;
120
+ w.prop(name, MDX_ATTR_EXPRESSION_PROP, typeof expr === "string" ? expr : "");
121
+ }
122
+ else {
123
+ w.prop(name, MDX_ATTR_BOOLEAN_PROP, "");
124
+ }
125
+ }
@@ -0,0 +1,2 @@
1
+ /** Restore phantom-space sentinels to real spaces, allocating only when present. */
2
+ export declare function restorePhantomSpaces(value: string): string;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Sentinel the MDX parser substitutes for spaces inside expression and JSX
3
+ * attribute-expression values (see `PHANTOM_SPACE` in
4
+ * satteri-pulldown-cmark/src/mdx.rs). mdast and hast keep the sentinel in the
5
+ * stored value; readers and visitors restore the real space on the way out.
6
+ * Both plugin-facing paths (direct walk match and materialized child) must
7
+ * restore it so a node's `value` reads the same regardless of how it's reached.
8
+ */
9
+ const PHANTOM_SPACE = "\uF002";
10
+ /** Restore phantom-space sentinels to real spaces, allocating only when present. */
11
+ export function restorePhantomSpaces(value) {
12
+ return value.includes(PHANTOM_SPACE) ? value.replaceAll(PHANTOM_SPACE, " ") : value;
13
+ }
package/dist/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Position } from "unist";
2
- import type { Literal as MdastLiteral, Nodes as MdastStdNodes } from "mdast";
3
- import type { Nodes as HastStdNodes } from "hast";
2
+ import type { Literal as MdastLiteral, Nodes as MdastStdNodes, Parent as MdastParent, PhrasingContent } from "mdast";
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";
6
6
  export type { ContainerDirective, LeafDirective, TextDirective, DirectiveAttributes, } from "./directive-types.js";
@@ -16,6 +16,14 @@ export interface MathNode extends MdastLiteral {
16
16
  export interface InlineMath extends MdastLiteral {
17
17
  type: "inlineMath";
18
18
  }
19
+ export interface Superscript extends MdastParent {
20
+ type: "superscript";
21
+ children: PhrasingContent[];
22
+ }
23
+ export interface Subscript extends MdastParent {
24
+ type: "subscript";
25
+ children: PhrasingContent[];
26
+ }
19
27
  declare module "mdast" {
20
28
  interface FrontmatterContentMap {
21
29
  toml: Toml;
@@ -24,17 +32,20 @@ declare module "mdast" {
24
32
  toml: Toml;
25
33
  math: MathNode;
26
34
  inlineMath: InlineMath;
35
+ superscript: Superscript;
36
+ subscript: Subscript;
27
37
  }
28
38
  interface PhrasingContentMap {
29
39
  inlineMath: InlineMath;
40
+ superscript: Superscript;
41
+ subscript: Subscript;
30
42
  }
31
43
  interface BlockContentMap {
32
44
  math: MathNode;
33
45
  }
34
46
  }
35
- export interface HastRaw {
47
+ export interface HastRaw extends HastLiteral {
36
48
  type: "raw";
37
- value: string;
38
49
  }
39
50
  declare module "hast" {
40
51
  interface RootContentMap {
@@ -56,6 +67,25 @@ export type MdastNode = MdastStdNodes;
56
67
  * (e.g. `tagName` on `"element"`, `value` on `"text"`).
57
68
  */
58
69
  export type HastNode = HastStdNodes;
70
+ /**
71
+ * Registry for typing well-known keys on the plugin data bag. Augment it to
72
+ * give specific `ctx.data` / `result.data` keys a type:
73
+ *
74
+ * ```ts
75
+ * declare module "satteri" {
76
+ * interface DataMap {
77
+ * toc: TocEntry[];
78
+ * }
79
+ * }
80
+ * ```
81
+ */
82
+ export interface DataMap {
83
+ }
84
+ /**
85
+ * The document-level plugin data bag (`ctx.data` and `result.data`): any
86
+ * string key holding any value, plus the typed keys registered in {@link DataMap}.
87
+ */
88
+ export type Data = Record<string, unknown> & Partial<DataMap>;
59
89
  export interface StringRefRaw {
60
90
  offset: number;
61
91
  len: number;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Cold/shape-stable helpers shared by the mdast and hast visitors. The
3
+ * per-node hot decoders stay duplicated in each visitor on purpose: sharing
4
+ * them would feed differently-shaped arguments through one call site and turn
5
+ * it polymorphic.
6
+ */
7
+ import type { CommandBuffer } from "./command-buffer.js";
8
+ export declare function asArray<T>(value: T | T[]): T[];
9
+ /** Thrown when declarative replacement content can't be compiled to the
10
+ * structural op-stream — an unsupported node type (e.g. a bare `root`/`doctype`
11
+ * handed in as content) or an out-of-range numeric field. The op-stream is the
12
+ * only structural encoding, so this is a hard error rather than a fallback. */
13
+ export declare function unencodableContentError(content: unknown): Error;
14
+ /**
15
+ * 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.
19
+ */
20
+ export declare function makeRequireNid<TNode>(nid: (node: TNode) => number | undefined): (node: TNode, method: string) => number;
21
+ /** Concatenate the return-value and context command buffers for one pass,
22
+ * resetting both for reuse. */
23
+ export declare function mergeAndReset(returnBuffer: CommandBuffer, ctx: {
24
+ getCommandBuffer(): CommandBuffer;
25
+ }): {
26
+ merged: Uint8Array;
27
+ hasMutations: boolean;
28
+ };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Cold/shape-stable helpers shared by the mdast and hast visitors. The
3
+ * per-node hot decoders stay duplicated in each visitor on purpose: sharing
4
+ * them would feed differently-shaped arguments through one call site and turn
5
+ * it polymorphic.
6
+ */
7
+ const EMPTY_BYTES = new Uint8Array(0);
8
+ export function asArray(value) {
9
+ return Array.isArray(value) ? value : [value];
10
+ }
11
+ /** Thrown when declarative replacement content can't be compiled to the
12
+ * structural op-stream — an unsupported node type (e.g. a bare `root`/`doctype`
13
+ * handed in as content) or an out-of-range numeric field. The op-stream is the
14
+ * only structural encoding, so this is a hard error rather than a fallback. */
15
+ export function unencodableContentError(content) {
16
+ const type = content?.type;
17
+ return new Error(`satteri: cannot encode replacement content${typeof type === "string" ? ` of type "${type}"` : ""} ` +
18
+ "into the structural op-stream — unsupported node type or out-of-range numeric field.");
19
+ }
20
+ /**
21
+ * 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.
25
+ */
26
+ 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;
34
+ };
35
+ }
36
+ /** Concatenate the return-value and context command buffers for one pass,
37
+ * resetting both for reuse. */
38
+ export function mergeAndReset(returnBuffer, ctx) {
39
+ const ctxCmdBuf = ctx.getCommandBuffer();
40
+ // The common case — no mutations this pass — allocates nothing.
41
+ if (returnBuffer.length === 0 && ctxCmdBuf.length === 0) {
42
+ return { merged: EMPTY_BYTES, hasMutations: false };
43
+ }
44
+ const ctxBuf = ctxCmdBuf.getBuffer();
45
+ const retBuf = returnBuffer.getBuffer();
46
+ const merged = new Uint8Array(retBuf.length + ctxBuf.length);
47
+ merged.set(retBuf, 0);
48
+ merged.set(ctxBuf, retBuf.length);
49
+ returnBuffer.reset();
50
+ ctxCmdBuf.reset();
51
+ return { merged, hasMutations: true };
52
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Little-endian primitives for reading the walk/snapshot wire buffers. Shared by
3
+ * the hand-written decoders and the generated layout decoder so both interpret
4
+ * the bytes identically.
5
+ */
6
+ import type { Position } from "unist";
7
+ /** Read a u16 (LE) at `off`. */
8
+ export declare function ru16(view: DataView, off: number): number;
9
+ /** Read a u32 (LE) at `off`. */
10
+ export declare function ru32(view: DataView, off: number): number;
11
+ /** Read a UTF-8 string of `len` bytes at `off`. Very short ASCII runs decode
12
+ * with a charCode loop — TextDecoder's per-call overhead dominates there. The
13
+ * threshold is lower than the encode-side `INLINE_STR_MAX`: decoding builds a
14
+ * JS string by concatenation, which costs more per char than storing bytes.
15
+ * Measured crossover ~8-12 bytes (Node 24: 8B 30 vs 57 ns, 16B 64 vs 57). */
16
+ export declare function rstr(buf: Uint8Array, off: number, len: number): string;
17
+ /**
18
+ * Decode the 24-byte position block ([startOffset, endOffset, startLine,
19
+ * startColumn, endLine, endColumn], u32 LE each) shared by the walk prefixes
20
+ * and the snapshot node structs. A zero start line is the sentinel for
21
+ * synthesized nodes with no source range (e.g. GFM autolink-literal nodes, or a
22
+ * plugin-built replacement spliced by the rebuild): unist lines are 1-based, so
23
+ * line 0 always means "no source position" — even when the rebuild has rebased
24
+ * the (otherwise zero) offset by the spliced subtree's source base. Surfaced as
25
+ * `undefined` so a node reads the same however it's reached.
26
+ */
27
+ export declare function readPosition(view: DataView, off: number): Position | undefined;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Little-endian primitives for reading the walk/snapshot wire buffers. Shared by
3
+ * the hand-written decoders and the generated layout decoder so both interpret
4
+ * the bytes identically.
5
+ */
6
+ const textDecoder = new TextDecoder("utf-8");
7
+ /** Read a u16 (LE) at `off`. */
8
+ export function ru16(view, off) {
9
+ return view.getUint16(off, true);
10
+ }
11
+ /** Read a u32 (LE) at `off`. */
12
+ export function ru32(view, off) {
13
+ return view.getUint32(off, true);
14
+ }
15
+ /** Read a UTF-8 string of `len` bytes at `off`. Very short ASCII runs decode
16
+ * with a charCode loop — TextDecoder's per-call overhead dominates there. The
17
+ * threshold is lower than the encode-side `INLINE_STR_MAX`: decoding builds a
18
+ * JS string by concatenation, which costs more per char than storing bytes.
19
+ * Measured crossover ~8-12 bytes (Node 24: 8B 30 vs 57 ns, 16B 64 vs 57). */
20
+ export function rstr(buf, off, len) {
21
+ if (len === 0)
22
+ return "";
23
+ if (len <= 8) {
24
+ let ascii = true;
25
+ for (let i = 0; i < len; i++) {
26
+ if (buf[off + i] > 127) {
27
+ ascii = false;
28
+ break;
29
+ }
30
+ }
31
+ if (ascii) {
32
+ let s = "";
33
+ for (let i = 0; i < len; i++)
34
+ s += String.fromCharCode(buf[off + i]);
35
+ return s;
36
+ }
37
+ }
38
+ return textDecoder.decode(buf.subarray(off, off + len));
39
+ }
40
+ /**
41
+ * Decode the 24-byte position block ([startOffset, endOffset, startLine,
42
+ * startColumn, endLine, endColumn], u32 LE each) shared by the walk prefixes
43
+ * and the snapshot node structs. A zero start line is the sentinel for
44
+ * synthesized nodes with no source range (e.g. GFM autolink-literal nodes, or a
45
+ * plugin-built replacement spliced by the rebuild): unist lines are 1-based, so
46
+ * line 0 always means "no source position" — even when the rebuild has rebased
47
+ * the (otherwise zero) offset by the spliced subtree's source base. Surfaced as
48
+ * `undefined` so a node reads the same however it's reached.
49
+ */
50
+ export function readPosition(view, off) {
51
+ const startLine = ru32(view, off + 8);
52
+ if (startLine === 0)
53
+ return undefined;
54
+ const startOffset = ru32(view, off);
55
+ return {
56
+ start: { offset: startOffset, line: startLine, column: ru32(view, off + 12) },
57
+ end: { offset: ru32(view, off + 4), line: ru32(view, off + 16), column: ru32(view, off + 20) },
58
+ };
59
+ }