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.
- package/README.md +11 -1
- package/dist/binding.browser.d.ts +1 -1
- package/dist/binding.browser.js +1 -1
- package/dist/binding.d.ts +1 -1
- package/dist/binding.js +1 -1
- package/dist/byte-writer.d.ts +4 -0
- package/dist/byte-writer.js +13 -4
- package/dist/child-stub.d.ts +7 -3
- package/dist/child-stub.js +18 -7
- package/dist/command-buffer.d.ts +36 -50
- package/dist/command-buffer.js +102 -49
- package/dist/compile.d.ts +80 -32
- package/dist/compile.js +295 -70
- package/dist/freeze.d.ts +2 -0
- package/dist/freeze.js +10 -0
- package/dist/generated/wire-constants.d.ts +3 -2
- package/dist/generated/wire-constants.js +4 -2
- package/dist/hast/child-stub.d.ts +2 -3
- package/dist/hast/child-stub.js +18 -11
- package/dist/hast/element-props.d.ts +1 -1
- package/dist/hast/element-props.js +14 -6
- package/dist/hast/hast-materializer.d.ts +6 -2
- package/dist/hast/hast-materializer.js +48 -134
- package/dist/hast/hast-reader.d.ts +7 -13
- package/dist/hast/hast-reader.js +76 -21
- package/dist/hast/hast-visitor.d.ts +55 -5
- package/dist/hast/hast-visitor.js +320 -154
- package/dist/index.d.ts +6 -6
- package/dist/index.js +1 -1
- package/dist/lazy-child-resolver.d.ts +20 -13
- package/dist/lazy-child-resolver.js +66 -38
- package/dist/materializer-cache.d.ts +42 -0
- package/dist/materializer-cache.js +192 -0
- package/dist/mdast/child-stub.d.ts +6 -3
- package/dist/mdast/child-stub.js +71 -8
- package/dist/mdast/generated/layout.d.ts +2 -3
- package/dist/mdast/generated/layout.js +24 -28
- package/dist/mdast/generated/node-types.d.ts +4 -0
- package/dist/mdast/generated/node-types.js +25 -0
- package/dist/mdast/mdast-materializer.d.ts +12 -3
- package/dist/mdast/mdast-materializer.js +83 -78
- package/dist/mdast/mdast-reader.d.ts +15 -0
- package/dist/mdast/mdast-reader.js +55 -10
- package/dist/mdast/mdast-visitor.d.ts +79 -32
- package/dist/mdast/mdast-visitor.js +305 -122
- package/dist/op-stream.d.ts +5 -12
- package/dist/op-stream.js +6 -21
- package/dist/plugin.d.ts +36 -10
- package/dist/plugin.js +50 -2
- package/dist/string-pool.d.ts +13 -0
- package/dist/string-pool.js +91 -0
- package/dist/types.d.ts +44 -1
- package/dist/visitor-shared.d.ts +27 -4
- package/dist/visitor-shared.js +36 -10
- package/dist/wire-read.js +3 -1
- package/index.d.ts +130 -7
- package/index.js +89 -68
- package/package.json +24 -19
- package/dist/lazy-props.d.ts +0 -10
- package/dist/lazy-props.js +0 -50
- package/satteri_napi.wasi-browser.js +0 -83
- package/satteri_napi.wasi.cjs +0 -134
- package/wasi-worker-browser.mjs +0 -36
- package/wasi-worker.mjs +0 -63
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
export { markdownToHtml, mdxToJs, evaluate, markdownToMdast, mdxToMdast, markdownToHast, mdxToHast, } from "./compile.js";
|
|
2
|
-
export type { CompileOptions, MdxCompileOptions, MdxOnlyOptions, EvaluateOptions, OptimizeStaticConfig, Features, SmartPunctuationOptions, Frontmatter, MarkdownToHtmlResult, MdxToJsResult, } from "./compile.js";
|
|
1
|
+
export { markdownToHtml, markdownToJs, mdxToJs, evaluate, markdownToMdast, mdxToMdast, markdownToHast, mdxToHast, htmlToHast, } from "./compile.js";
|
|
2
|
+
export type { CompileOptions, MdxCompileOptions, MdxOnlyOptions, MarkdownToJsOptions, EvaluateOptions, OptimizeStaticConfig, Features, TreeOptions, SmartPunctuationOptions, Frontmatter, MarkdownToHtmlResult, MarkdownToJsResult, MdxToJsResult, } from "./compile.js";
|
|
3
3
|
export { defineMdastPlugin, defineHastPlugin } from "./plugin.js";
|
|
4
|
-
export type { MdastPluginDefinition, HastPluginDefinition, MdastPluginInput, HastPluginInput, } from "./plugin.js";
|
|
5
|
-
export type { HastVisitorInstance, HastVisitorContext, HastFilteredVisitor, HastContent, EstreeProgram, } from "./hast/hast-visitor.js";
|
|
6
|
-
export type { MdastNode, HastNode, DataMap, Data, Position, Point, MdxJsxAttributeNode, MdxJsxExpressionAttributeNode, MdxJsxAttributeValueExpressionNode, MdxJsxAttributeUnion, MdxJsxFlowElement, MdxJsxTextElement, MdxFlowExpression, MdxTextExpression, MdxjsEsm, MdxJsxFlowElementHast, MdxJsxTextElementHast, MdxFlowExpressionHast, MdxTextExpressionHast, MdxjsEsmHast, } from "./types.js";
|
|
4
|
+
export type { MdastPluginDefinition, HastPluginDefinition, MdastPluginInput, HastPluginInput, MdastPluginEntry, HastPluginEntry, MdastPluginList, HastPluginList, PluginFactoryContext, } from "./plugin.js";
|
|
5
|
+
export type { HastVisitorInstance, HastVisitorContext, HastFilteredVisitor, HastContent, HastParentContent, RawHastContent, RawHtmlHastContent, EstreeProgram, } from "./hast/hast-visitor.js";
|
|
6
|
+
export type { MdastNode, HastNode, Custom, DataMap, Data, SourceFormat, Position, Point, MdxJsxAttributeNode, MdxJsxExpressionAttributeNode, MdxJsxAttributeValueExpressionNode, MdxJsxAttributeUnion, MdxJsxFlowElement, MdxJsxTextElement, MdxFlowExpression, MdxTextExpression, MdxjsEsm, MdxJsxFlowElementHast, MdxJsxTextElementHast, MdxFlowExpressionHast, MdxTextExpressionHast, MdxjsEsmHast, } from "./types.js";
|
|
7
7
|
export { visitMdastHandle, resolveMdastSubscriptions } from "./mdast/mdast-visitor.js";
|
|
8
|
-
export type { MdastPluginInstance, MdastVisitorContext, MdastContent, } from "./mdast/mdast-visitor.js";
|
|
8
|
+
export type { MdastPluginInstance, MdastVisitorContext, MdastContent, MdastTarget, MdastParentContent, RawMdastContent, RawHtmlMdastContent, } from "./mdast/mdast-visitor.js";
|
|
9
9
|
export { visitHastHandle, resolveSubscriptions as resolveHastSubscriptions, } from "./hast/hast-visitor.js";
|
|
10
10
|
export { MdastReader } from "./mdast/mdast-reader.js";
|
|
11
11
|
export { materializeMdastTree } from "./mdast/mdast-materializer.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Public API: compile functions
|
|
2
|
-
export { markdownToHtml, mdxToJs, evaluate, markdownToMdast, mdxToMdast, markdownToHast, mdxToHast, } from "./compile.js";
|
|
2
|
+
export { markdownToHtml, markdownToJs, mdxToJs, evaluate, markdownToMdast, mdxToMdast, markdownToHast, mdxToHast, htmlToHast, } from "./compile.js";
|
|
3
3
|
// Plugin definitions
|
|
4
4
|
export { defineMdastPlugin, defineHastPlugin } from "./plugin.js";
|
|
5
5
|
// Visitor pipeline (for manual plugin execution)
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
+
import type { NodeRefs } from "./visitor-shared.js";
|
|
1
2
|
import type { AnyHandle } from "./handles.js";
|
|
3
|
+
export declare function registerEpochCacheSlot<T extends object>(slot: WeakMap<AnyHandle, T>): WeakMap<AnyHandle, T>;
|
|
2
4
|
/** Record that `handle`'s arena was rebuilt. Resolvers created before the bump
|
|
3
5
|
* refuse to take a fresh snapshot afterwards (their ids are stale). */
|
|
4
6
|
export declare function markHandleMutated(handle: AnyHandle): void;
|
|
7
|
+
/** Snapshot for one `(handle, epoch)`. Immutable once built, so it is shared
|
|
8
|
+
* by every resolver of the same pass chain: nested matched containers dedup
|
|
9
|
+
* by id, and later read-only passes reuse the whole cache instead of
|
|
10
|
+
* re-serializing and re-materializing. */
|
|
11
|
+
export interface EpochCache<TReader> {
|
|
12
|
+
epoch: number;
|
|
13
|
+
reader: TReader;
|
|
14
|
+
}
|
|
5
15
|
/**
|
|
6
16
|
* Lazy node materializer for the walk paths: serializes the handle's arena
|
|
7
17
|
* once, on the first stub materialization, then materializes nodes from that
|
|
@@ -10,23 +20,20 @@ export declare function markHandleMutated(handle: AnyHandle): void;
|
|
|
10
20
|
*/
|
|
11
21
|
export declare abstract class LazyChildResolver<TReader, TNode> {
|
|
12
22
|
#private;
|
|
23
|
+
readonly refs: NodeRefs;
|
|
13
24
|
constructor(handle: AnyHandle);
|
|
14
25
|
protected abstract createReader(wire: Uint8Array): TReader;
|
|
15
|
-
protected abstract materializeNode(reader: TReader, nodeId: number): TNode;
|
|
26
|
+
protected abstract materializeNode(reader: TReader, nodeId: number, refs: NodeRefs): TNode;
|
|
16
27
|
protected abstract readParentId(reader: TReader, nodeId: number): number;
|
|
17
28
|
protected abstract readChildIds(reader: TReader, nodeId: number): number[];
|
|
18
|
-
/**
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* outright, even when no mutation landed — match-time ids cannot be trusted
|
|
27
|
-
* once the plugin no longer controls the tree. */
|
|
28
|
-
assertUnsealed(): void;
|
|
29
|
-
/** Materialize one node for a child stub's first real-field read. */
|
|
29
|
+
/** Kind-specific `(handle → cache)` slot, supplied as a module-level WeakMap
|
|
30
|
+
* by each subclass so MDAST and HAST snapshots never collide. */
|
|
31
|
+
protected abstract cacheSlot(): WeakMap<AnyHandle, EpochCache<TReader>>;
|
|
32
|
+
/** Whether this pass's snapshot already exists; never takes one itself. */
|
|
33
|
+
hasHotSnapshot(): boolean;
|
|
34
|
+
/** Materialize one node for a child stub's first real-field read. The
|
|
35
|
+
* materializers memoize per `(reader, id)`, so overlapping subtrees and
|
|
36
|
+
* later passes share the same materialized objects. */
|
|
30
37
|
materializeOne(nodeId: number): TNode;
|
|
31
38
|
/** Arena id of `nodeId`'s parent in the pass snapshot, or undefined at the root. */
|
|
32
39
|
parentIdOf(nodeId: number): number | undefined;
|
|
@@ -2,10 +2,30 @@ import { serializeHandle } from "#binding";
|
|
|
2
2
|
/** Rebuild count per handle: bumped whenever a command buffer lands and
|
|
3
3
|
* renumbers the arena, invalidating ids captured before it. */
|
|
4
4
|
const HANDLE_EPOCHS = new WeakMap();
|
|
5
|
+
/** Per-flavor `(handle → cache)` slots, registered so a mutation can evict a snapshot the handle would otherwise pin. */
|
|
6
|
+
const EPOCH_CACHE_SLOTS = [];
|
|
7
|
+
export function registerEpochCacheSlot(slot) {
|
|
8
|
+
EPOCH_CACHE_SLOTS.push(slot);
|
|
9
|
+
return slot;
|
|
10
|
+
}
|
|
11
|
+
/** Per handle, not per epoch: an id stale within its own handle is the epoch machinery's to drop with a warning. */
|
|
12
|
+
const HANDLE_NODE_REFS = new WeakMap();
|
|
13
|
+
function nodeRefsOfHandle(handle) {
|
|
14
|
+
let refs = HANDLE_NODE_REFS.get(handle);
|
|
15
|
+
if (refs === undefined) {
|
|
16
|
+
refs = new WeakMap();
|
|
17
|
+
HANDLE_NODE_REFS.set(handle, refs);
|
|
18
|
+
}
|
|
19
|
+
return refs;
|
|
20
|
+
}
|
|
5
21
|
/** Record that `handle`'s arena was rebuilt. Resolvers created before the bump
|
|
6
22
|
* refuse to take a fresh snapshot afterwards (their ids are stale). */
|
|
7
23
|
export function markHandleMutated(handle) {
|
|
8
24
|
HANDLE_EPOCHS.set(handle, (HANDLE_EPOCHS.get(handle) ?? 0) + 1);
|
|
25
|
+
// Safe to evict: a stale-epoch entry can never be adopted, only pinned by existing resolvers
|
|
26
|
+
for (const slot of EPOCH_CACHE_SLOTS) {
|
|
27
|
+
slot.delete(handle);
|
|
28
|
+
}
|
|
9
29
|
}
|
|
10
30
|
/** Arena sentinel in the node struct's parent field: the node has no parent. */
|
|
11
31
|
const NO_PARENT = 0xffffffff;
|
|
@@ -17,57 +37,66 @@ const NO_PARENT = 0xffffffff;
|
|
|
17
37
|
*/
|
|
18
38
|
export class LazyChildResolver {
|
|
19
39
|
#handle;
|
|
20
|
-
#reader = null;
|
|
21
|
-
#sealed = false;
|
|
22
40
|
#epoch;
|
|
41
|
+
refs;
|
|
42
|
+
/** Strong pin: retained nodes keep their pass snapshot alive after later epochs evict the slot. */
|
|
43
|
+
#cache;
|
|
23
44
|
constructor(handle) {
|
|
24
45
|
this.#handle = handle;
|
|
25
46
|
this.#epoch = HANDLE_EPOCHS.get(handle) ?? 0;
|
|
47
|
+
this.refs = nodeRefsOfHandle(handle);
|
|
26
48
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
#ensureCache() {
|
|
50
|
+
let cache = this.#cache;
|
|
51
|
+
if (cache !== undefined)
|
|
52
|
+
return cache;
|
|
53
|
+
const slot = this.cacheSlot();
|
|
54
|
+
cache = slot.get(this.#handle);
|
|
55
|
+
if (cache !== undefined && cache.epoch === this.#epoch) {
|
|
56
|
+
this.#cache = cache;
|
|
57
|
+
return cache;
|
|
58
|
+
}
|
|
59
|
+
// A node id proves the tree was read in-pass, so a deferred snapshot is
|
|
60
|
+
// still faithful as long as no command buffer mutated the arena since
|
|
61
|
+
// match time. An existing same-epoch cache is always safe: the snapshot
|
|
62
|
+
// is an immutable copy.
|
|
63
|
+
if ((HANDLE_EPOCHS.get(this.#handle) ?? 0) !== this.#epoch) {
|
|
41
64
|
throw new Error("Cannot read node content: this node was retained past its visitor pass " +
|
|
42
|
-
"and the tree
|
|
65
|
+
"and the tree has changed since. Reading a child node's field (or calling " +
|
|
66
|
+
"ctx.parent()) during the pass pins the pass snapshot; eager fields like " +
|
|
67
|
+
"tagName or properties do not. Or copy the data you need before the pass ends.");
|
|
43
68
|
}
|
|
69
|
+
// The serialized buffer already carries each node's `data` blob (read
|
|
70
|
+
// eagerly by the materializer), and the arena isn't mutated mid-visit,
|
|
71
|
+
// so no separate lazy NAPI fetch is needed. This also keeps walk-path
|
|
72
|
+
// children consistent with the fully materialized tree (no `data` key
|
|
73
|
+
// when a node has none).
|
|
74
|
+
cache = {
|
|
75
|
+
epoch: this.#epoch,
|
|
76
|
+
reader: this.createReader(serializeHandle(this.#handle)),
|
|
77
|
+
};
|
|
78
|
+
this.#cache = cache;
|
|
79
|
+
slot.set(this.#handle, cache);
|
|
80
|
+
return cache;
|
|
44
81
|
}
|
|
45
82
|
#ensureReader() {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
// The serialized buffer already carries each node's `data` blob (read
|
|
56
|
-
// eagerly by the materializer), and the arena isn't mutated mid-visit —
|
|
57
|
-
// so no separate lazy NAPI fetch is needed. This also keeps walk-path
|
|
58
|
-
// children consistent with the fully materialized tree (no `data` key
|
|
59
|
-
// when a node has none).
|
|
60
|
-
this.#reader = this.createReader(serializeHandle(this.#handle));
|
|
61
|
-
}
|
|
62
|
-
return this.#reader;
|
|
83
|
+
return this.#ensureCache().reader;
|
|
84
|
+
}
|
|
85
|
+
/** Whether this pass's snapshot already exists; never takes one itself. */
|
|
86
|
+
hasHotSnapshot() {
|
|
87
|
+
if (this.#cache !== undefined)
|
|
88
|
+
return true;
|
|
89
|
+
const cache = this.cacheSlot().get(this.#handle);
|
|
90
|
+
return cache !== undefined && cache.epoch === this.#epoch;
|
|
63
91
|
}
|
|
64
|
-
/** Materialize one node for a child stub's first real-field read.
|
|
92
|
+
/** Materialize one node for a child stub's first real-field read. The
|
|
93
|
+
* materializers memoize per `(reader, id)`, so overlapping subtrees and
|
|
94
|
+
* later passes share the same materialized objects. */
|
|
65
95
|
materializeOne(nodeId) {
|
|
66
|
-
return this.materializeNode(this.#
|
|
96
|
+
return this.materializeNode(this.#ensureCache().reader, nodeId, this.refs);
|
|
67
97
|
}
|
|
68
98
|
/** Arena id of `nodeId`'s parent in the pass snapshot, or undefined at the root. */
|
|
69
99
|
parentIdOf(nodeId) {
|
|
70
|
-
this.assertUnsealed();
|
|
71
100
|
const parentId = this.readParentId(this.#ensureReader(), nodeId);
|
|
72
101
|
return parentId === NO_PARENT ? undefined : parentId;
|
|
73
102
|
}
|
|
@@ -77,7 +106,6 @@ export class LazyChildResolver {
|
|
|
77
106
|
/** Index of `nodeId` within its parent's children in the pass snapshot,
|
|
78
107
|
* or undefined at the root. */
|
|
79
108
|
indexInParent(nodeId) {
|
|
80
|
-
this.assertUnsealed();
|
|
81
109
|
const reader = this.#ensureReader();
|
|
82
110
|
const parentId = this.readParentId(reader, nodeId);
|
|
83
111
|
if (parentId === NO_PARENT)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared materializer machinery for the HAST and MDAST flavors: per-reader
|
|
3
|
+
* node memo, lazy `children` descriptors, and the frozen-mode (plugin walk
|
|
4
|
+
* path) freeze rules.
|
|
5
|
+
*/
|
|
6
|
+
import type { Position } from "unist";
|
|
7
|
+
import type { NodeRefs } from "./visitor-shared.js";
|
|
8
|
+
/** The reader surface the shared machinery needs; both `HastReader` and `MdastReader` satisfy it. */
|
|
9
|
+
export interface MaterializerReader {
|
|
10
|
+
getNodeType(nodeId: number): number;
|
|
11
|
+
getChildIds(nodeId: number): number[];
|
|
12
|
+
getPosition(nodeId: number): Position | undefined;
|
|
13
|
+
getNodeData(nodeId: number): string | null;
|
|
14
|
+
}
|
|
15
|
+
export interface MaterializerSpec<TReader extends MaterializerReader, TNode extends object> {
|
|
16
|
+
/** Function name used in error/warning messages (e.g. "materializeHastNode"). */
|
|
17
|
+
label: string;
|
|
18
|
+
/** Node-type tag -> canonical AST name (the generated `TYPE_NAMES`). */
|
|
19
|
+
typeNames: Readonly<Record<number, string>>;
|
|
20
|
+
/** Whether `node` carries `children`. Takes the built node because mdast
|
|
21
|
+
* `custom` decides leafness per node rather than per type. */
|
|
22
|
+
hasChildren(nodeType: number, node: TNode, reader: TReader, nodeId: number): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Install the type-specific eager fields on `node`. Must not install
|
|
25
|
+
* `children`, `position`, `data`, or `_nodeId`, and must not freeze:
|
|
26
|
+
* the shared machinery owns all of those.
|
|
27
|
+
*/
|
|
28
|
+
populate(node: TNode, reader: TReader, nodeId: number, nodeType: number): void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Build a memoizing materializer, memoized per `(reader, id)`.
|
|
32
|
+
*
|
|
33
|
+
* `node` materializes one node with lazy `children`; `frozen` (the plugin walk
|
|
34
|
+
* path) deep-freezes every node at construction so plugins cannot corrupt the
|
|
35
|
+
* shared cache. `tree` materializes a whole tree eagerly, which is what the
|
|
36
|
+
* step-by-step API wants: it asked for the tree, so laziness would only add
|
|
37
|
+
* per-node accessor overhead.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createMaterializer<TReader extends MaterializerReader, TNode extends object>(spec: MaterializerSpec<TReader, TNode>): {
|
|
40
|
+
node: (reader: TReader, nodeId: number, frozen?: boolean, refs?: NodeRefs) => TNode;
|
|
41
|
+
tree: (reader: TReader, rootId: number) => TNode;
|
|
42
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared materializer machinery for the HAST and MDAST flavors: per-reader
|
|
3
|
+
* node memo, lazy `children` descriptors, and the frozen-mode (plugin walk
|
|
4
|
+
* path) freeze rules.
|
|
5
|
+
*/
|
|
6
|
+
import { deepFreeze } from "./freeze.js";
|
|
7
|
+
/**
|
|
8
|
+
* Build a memoizing materializer, memoized per `(reader, id)`.
|
|
9
|
+
*
|
|
10
|
+
* `node` materializes one node with lazy `children`; `frozen` (the plugin walk
|
|
11
|
+
* path) deep-freezes every node at construction so plugins cannot corrupt the
|
|
12
|
+
* shared cache. `tree` materializes a whole tree eagerly, which is what the
|
|
13
|
+
* step-by-step API wants: it asked for the tree, so laziness would only add
|
|
14
|
+
* per-node accessor overhead.
|
|
15
|
+
*/
|
|
16
|
+
export function createMaterializer(spec) {
|
|
17
|
+
const readerCaches = new WeakMap();
|
|
18
|
+
function materialize(reader, nodeId, frozen = false, refs) {
|
|
19
|
+
const cache = readerCache(reader, frozen, refs);
|
|
20
|
+
let node = cache.nodes.get(nodeId);
|
|
21
|
+
if (node === undefined) {
|
|
22
|
+
node = buildNode(reader, cache, nodeId, reader.getNodeType(nodeId));
|
|
23
|
+
cache.nodes.set(nodeId, node);
|
|
24
|
+
}
|
|
25
|
+
return node;
|
|
26
|
+
}
|
|
27
|
+
/** Frozen-mode `children`: memoized in `cache.childLists` because the node
|
|
28
|
+
* is frozen, so the accessor cannot self-replace with a data property. */
|
|
29
|
+
function frozenChildrenDescriptor(reader, cache) {
|
|
30
|
+
return {
|
|
31
|
+
get() {
|
|
32
|
+
const nodeId = this._nodeId;
|
|
33
|
+
let value = cache.childLists.get(nodeId);
|
|
34
|
+
if (value === undefined) {
|
|
35
|
+
const ids = reader.getChildIds(nodeId);
|
|
36
|
+
const built = new Array(ids.length);
|
|
37
|
+
let i = 0;
|
|
38
|
+
for (const childId of ids)
|
|
39
|
+
built[i++] = materialize(reader, childId, true);
|
|
40
|
+
value = Object.freeze(built);
|
|
41
|
+
cache.childLists.set(nodeId, value);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
},
|
|
45
|
+
configurable: true,
|
|
46
|
+
enumerable: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Mutable-mode `children`: self-replacing with a plain writable array on
|
|
50
|
+
* first read. The id is captured rather than stored on the node, so a
|
|
51
|
+
* materialized tree carries no marker for `toEqual` or a spread to find. */
|
|
52
|
+
function mutableChildrenDescriptor(reader, nodeId) {
|
|
53
|
+
return {
|
|
54
|
+
get() {
|
|
55
|
+
const ids = reader.getChildIds(nodeId);
|
|
56
|
+
const value = new Array(ids.length);
|
|
57
|
+
let i = 0;
|
|
58
|
+
for (const childId of ids)
|
|
59
|
+
value[i++] = materialize(reader, childId);
|
|
60
|
+
Object.defineProperty(this, "children", {
|
|
61
|
+
value,
|
|
62
|
+
writable: true,
|
|
63
|
+
configurable: true,
|
|
64
|
+
enumerable: true,
|
|
65
|
+
});
|
|
66
|
+
return value;
|
|
67
|
+
},
|
|
68
|
+
configurable: true,
|
|
69
|
+
enumerable: true,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function readerCache(reader, frozen, refs) {
|
|
73
|
+
let cache = readerCaches.get(reader);
|
|
74
|
+
if (cache === undefined) {
|
|
75
|
+
cache = {
|
|
76
|
+
nodes: new Map(),
|
|
77
|
+
childLists: new Map(),
|
|
78
|
+
children: undefined,
|
|
79
|
+
frozen,
|
|
80
|
+
refs,
|
|
81
|
+
};
|
|
82
|
+
if (frozen)
|
|
83
|
+
cache.children = frozenChildrenDescriptor(reader, cache);
|
|
84
|
+
readerCaches.set(reader, cache);
|
|
85
|
+
}
|
|
86
|
+
if (cache.frozen !== frozen) {
|
|
87
|
+
throw new Error(`${spec.label}: a reader cannot mix frozen and mutable materialization`);
|
|
88
|
+
}
|
|
89
|
+
return cache;
|
|
90
|
+
}
|
|
91
|
+
function buildNode(reader, cache, nodeId, nodeType, eager = false) {
|
|
92
|
+
const typeName = spec.typeNames[nodeType] ?? `unknown(${nodeType})`;
|
|
93
|
+
// Plain object, not a class: unified's `assertNode` rejects any other prototype.
|
|
94
|
+
const node = { type: typeName };
|
|
95
|
+
const position = reader.getPosition(nodeId);
|
|
96
|
+
if (position !== undefined) {
|
|
97
|
+
node.position = position;
|
|
98
|
+
}
|
|
99
|
+
if (cache.frozen) {
|
|
100
|
+
// Non-enumerable so `nid()` never trusts an id that a spread copied.
|
|
101
|
+
cache.refs?.set(node, nodeId);
|
|
102
|
+
Object.defineProperty(node, "_nodeId", {
|
|
103
|
+
value: nodeId,
|
|
104
|
+
writable: false,
|
|
105
|
+
configurable: true,
|
|
106
|
+
enumerable: false,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
spec.populate(node, reader, nodeId, nodeType);
|
|
110
|
+
// Plugins can set `data` on any node type, so rehydrate generically
|
|
111
|
+
// (see website/content/docs/divergences.md for the code-block case).
|
|
112
|
+
const rawData = reader.getNodeData(nodeId);
|
|
113
|
+
if (rawData !== null) {
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(rawData);
|
|
116
|
+
if (parsed && typeof parsed === "object" && Object.keys(parsed).length > 0) {
|
|
117
|
+
Object.defineProperty(node, "data", {
|
|
118
|
+
value: parsed,
|
|
119
|
+
writable: true,
|
|
120
|
+
configurable: true,
|
|
121
|
+
enumerable: true,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
if (process.env.NODE_ENV !== "production") {
|
|
127
|
+
console.warn(`${spec.label}: malformed node_data for nodeId=${nodeId}`, err);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (!eager && spec.hasChildren(nodeType, node, reader, nodeId)) {
|
|
132
|
+
Object.defineProperty(node, "children", cache.children ?? mutableChildrenDescriptor(reader, nodeId));
|
|
133
|
+
}
|
|
134
|
+
if (cache.frozen) {
|
|
135
|
+
// Deep-freeze the eager own values but not the lazy `children` accessor;
|
|
136
|
+
// freeze eagerly even for containers so nothing is writable while cached.
|
|
137
|
+
const descriptors = Object.getOwnPropertyDescriptors(node);
|
|
138
|
+
for (const key of Object.keys(descriptors)) {
|
|
139
|
+
const desc = descriptors[key];
|
|
140
|
+
if (desc !== undefined && "value" in desc) {
|
|
141
|
+
deepFreeze(desc.value);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
Object.freeze(node);
|
|
145
|
+
}
|
|
146
|
+
return node;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Iterative on purpose: recursing to full document depth overflows on deeply
|
|
150
|
+
* nested input, and nothing else here descends more than one level.
|
|
151
|
+
*
|
|
152
|
+
* Breadth-first over the parents still to fill, so every node is built and
|
|
153
|
+
* every child list decoded exactly once, and a child is wired into its
|
|
154
|
+
* parent's array as it is built rather than in a second pass.
|
|
155
|
+
*/
|
|
156
|
+
function fillTree(reader, cache, rootId) {
|
|
157
|
+
const rootType = reader.getNodeType(rootId);
|
|
158
|
+
const root = buildNode(reader, cache, rootId, rootType, true);
|
|
159
|
+
if (!spec.hasChildren(rootType, root, reader, rootId))
|
|
160
|
+
return root;
|
|
161
|
+
const parents = [root];
|
|
162
|
+
const parentIds = [rootId];
|
|
163
|
+
for (let p = 0; p < parentIds.length; p++) {
|
|
164
|
+
const parent = parents[p];
|
|
165
|
+
const parentId = parentIds[p];
|
|
166
|
+
if (parent === undefined || parentId === undefined)
|
|
167
|
+
continue;
|
|
168
|
+
const ids = reader.getChildIds(parentId);
|
|
169
|
+
const kids = new Array(ids.length);
|
|
170
|
+
for (let i = 0; i < ids.length; i++) {
|
|
171
|
+
const childId = ids[i];
|
|
172
|
+
if (childId === undefined)
|
|
173
|
+
continue;
|
|
174
|
+
const childType = reader.getNodeType(childId);
|
|
175
|
+
const child = buildNode(reader, cache, childId, childType, true);
|
|
176
|
+
kids[i] = child;
|
|
177
|
+
if (spec.hasChildren(childType, child, reader, childId)) {
|
|
178
|
+
parents.push(child);
|
|
179
|
+
parentIds.push(childId);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Assignment beats defineProperty here; `eager` left `children` uninstalled.
|
|
183
|
+
parent.children = kids;
|
|
184
|
+
}
|
|
185
|
+
return root;
|
|
186
|
+
}
|
|
187
|
+
/** Whole tree at once: the caller will walk it, so lazy accessors cost more than they defer. */
|
|
188
|
+
function materializeTree(reader, rootId) {
|
|
189
|
+
return fillTree(reader, readerCache(reader, false, undefined), rootId);
|
|
190
|
+
}
|
|
191
|
+
return { node: materialize, tree: materializeTree };
|
|
192
|
+
}
|
|
@@ -5,9 +5,12 @@ type MdastResolver = LazyChildResolver<MdastReader, MdastNode>;
|
|
|
5
5
|
/**
|
|
6
6
|
* Walk-path child stub: arena id + `type` eagerly, every other field a lazy
|
|
7
7
|
* forward to the materialized node (first read snapshots the arena via
|
|
8
|
-
* `materializeOne`, which enforces the handle epoch
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* `materializeOne`, which enforces the handle epoch). Spread/identity rules
|
|
9
|
+
* are enforced by `nid()` (authoritative doc in hast-visitor.ts).
|
|
10
|
+
*
|
|
11
|
+
* A user-defined node's `type` is the one exception: it lives in the arena,
|
|
12
|
+
* not the tag, so it joins the lazy fields rather than making every sibling
|
|
13
|
+
* list snapshot the arena up front.
|
|
11
14
|
*/
|
|
12
15
|
export declare class MdastChildStub {
|
|
13
16
|
_resolver: MdastResolver;
|
package/dist/mdast/child-stub.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { flatByTag, stubDescriptors } from "../child-stub.js";
|
|
1
|
+
import { flatByTag, installStubDescriptors, stubDescriptors, } from "../child-stub.js";
|
|
2
2
|
import { MDAST_LAYOUT_KEYS } from "./generated/layout.js";
|
|
3
3
|
import { NAME_TO_TYPE, TYPE_NAMES } from "./generated/node-types.js";
|
|
4
4
|
import { LEAF_TYPES } from "./mdast-materializer.js";
|
|
@@ -8,6 +8,7 @@ const N = NAME_TO_TYPE;
|
|
|
8
8
|
const HAND_WRITTEN_FIELDS = {
|
|
9
9
|
[N.list]: ["ordered", "start", "spread"],
|
|
10
10
|
[N.listItem]: ["spread", "checked"],
|
|
11
|
+
[N.descriptionDetails]: ["spread"],
|
|
11
12
|
[N.table]: ["align"],
|
|
12
13
|
[N.containerDirective]: ["name", "attributes"],
|
|
13
14
|
[N.leafDirective]: ["name", "attributes"],
|
|
@@ -16,11 +17,18 @@ const HAND_WRITTEN_FIELDS = {
|
|
|
16
17
|
[N.mdxJsxTextElement]: ["name", "attributes"],
|
|
17
18
|
};
|
|
18
19
|
const TYPE_NAME_BY_TAG = flatByTag(TYPE_NAMES);
|
|
20
|
+
/** Internal tag for user-defined nodes; its stored `name` field is folded into
|
|
21
|
+
* the open `node.type`, so the stub exposes `value` (leaf content) but no
|
|
22
|
+
* separate `name`. */
|
|
23
|
+
const MDAST_CUSTOM = NAME_TO_TYPE.custom;
|
|
19
24
|
const MDAST_STUB_DESCRIPTORS = [];
|
|
20
25
|
for (const tag of Object.keys(TYPE_NAMES)) {
|
|
21
26
|
const nodeType = Number(tag);
|
|
22
|
-
const fields =
|
|
23
|
-
|
|
27
|
+
const fields = nodeType === MDAST_CUSTOM
|
|
28
|
+
? ["value"]
|
|
29
|
+
: [...(MDAST_LAYOUT_KEYS[nodeType] ?? HAND_WRITTEN_FIELDS[nodeType] ?? [])];
|
|
30
|
+
// `custom` gets its own `children` getter: leafness is per node, not per type.
|
|
31
|
+
if (nodeType !== MDAST_CUSTOM && !LEAF_TYPES.has(nodeType))
|
|
24
32
|
fields.push("children");
|
|
25
33
|
MDAST_STUB_DESCRIPTORS[nodeType] = stubDescriptors(fields);
|
|
26
34
|
}
|
|
@@ -29,9 +37,12 @@ const FALLBACK_DESCRIPTORS = stubDescriptors([]);
|
|
|
29
37
|
/**
|
|
30
38
|
* Walk-path child stub: arena id + `type` eagerly, every other field a lazy
|
|
31
39
|
* forward to the materialized node (first read snapshots the arena via
|
|
32
|
-
* `materializeOne`, which enforces the handle epoch
|
|
33
|
-
*
|
|
34
|
-
*
|
|
40
|
+
* `materializeOne`, which enforces the handle epoch). Spread/identity rules
|
|
41
|
+
* are enforced by `nid()` (authoritative doc in hast-visitor.ts).
|
|
42
|
+
*
|
|
43
|
+
* A user-defined node's `type` is the one exception: it lives in the arena,
|
|
44
|
+
* not the tag, so it joins the lazy fields rather than making every sibling
|
|
45
|
+
* list snapshot the arena up front.
|
|
35
46
|
*/
|
|
36
47
|
export class MdastChildStub {
|
|
37
48
|
_resolver;
|
|
@@ -40,7 +51,59 @@ export class MdastChildStub {
|
|
|
40
51
|
constructor(resolver, id, nodeType) {
|
|
41
52
|
this._resolver = resolver;
|
|
42
53
|
this._id = id;
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
if (nodeType === MDAST_CUSTOM) {
|
|
55
|
+
installLazyCustomType(this);
|
|
56
|
+
installLazyCustomChildren(this);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
this.type = TYPE_NAME_BY_TAG[nodeType] ?? `unknown(${nodeType})`;
|
|
60
|
+
}
|
|
61
|
+
installStubDescriptors(this, MDAST_STUB_DESCRIPTORS[nodeType] ?? FALLBACK_DESCRIPTORS);
|
|
45
62
|
}
|
|
63
|
+
/** @internal */
|
|
64
|
+
get _refs() {
|
|
65
|
+
return this._resolver.refs;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** `children` as a self-replacing accessor, and self-*removing* for a leaf: the
|
|
69
|
+
* stub only knows the tag, so it cannot tell a leaf from a parent until the
|
|
70
|
+
* arena snapshot says which shape this node has. */
|
|
71
|
+
function installLazyCustomChildren(stub) {
|
|
72
|
+
Object.defineProperty(stub, "children", {
|
|
73
|
+
get() {
|
|
74
|
+
const real = this._resolver.materializeOne(this._id);
|
|
75
|
+
const value = real.children;
|
|
76
|
+
if (value === undefined) {
|
|
77
|
+
delete this.children;
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
Object.defineProperty(this, "children", {
|
|
81
|
+
value,
|
|
82
|
+
writable: false,
|
|
83
|
+
enumerable: true,
|
|
84
|
+
configurable: false,
|
|
85
|
+
});
|
|
86
|
+
return value;
|
|
87
|
+
},
|
|
88
|
+
enumerable: true,
|
|
89
|
+
configurable: true,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/** `type` as a self-replacing accessor: the materialized node folds the stored
|
|
93
|
+
* name into `type`. Enumerable so a spread copy still carries it. */
|
|
94
|
+
function installLazyCustomType(stub) {
|
|
95
|
+
Object.defineProperty(stub, "type", {
|
|
96
|
+
get() {
|
|
97
|
+
const value = this._resolver.materializeOne(this._id).type;
|
|
98
|
+
Object.defineProperty(this, "type", {
|
|
99
|
+
value,
|
|
100
|
+
writable: true,
|
|
101
|
+
enumerable: true,
|
|
102
|
+
configurable: true,
|
|
103
|
+
});
|
|
104
|
+
return value;
|
|
105
|
+
},
|
|
106
|
+
enumerable: true,
|
|
107
|
+
configurable: true,
|
|
108
|
+
});
|
|
46
109
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { MdastReader } from "../mdast-reader.js";
|
|
2
2
|
/** Materialized property names per tag (the non-skip `js` names above),
|
|
3
|
-
*
|
|
4
|
-
* Exported for the child-stub field tables. */
|
|
3
|
+
* exported for the child-stub field tables. */
|
|
5
4
|
export declare const MDAST_LAYOUT_KEYS: Readonly<Record<number, readonly string[]>>;
|
|
6
5
|
/**
|
|
7
6
|
* Decode a node's type-specific `type_data` from the walk buffer onto `node`,
|
|
@@ -14,6 +13,6 @@ export declare function decodeMdastTypeData(view: DataView, buf: Uint8Array, sta
|
|
|
14
13
|
* Attach a node's fixed `type_data` fields, materialized from the arena
|
|
15
14
|
* snapshot, driven by `MDAST_LAYOUTS`. Returns `false` for tags with no entry,
|
|
16
15
|
* so the materializer falls through to its hand-written cases (list, table,
|
|
17
|
-
* directives, MDX JSX). Fields
|
|
16
|
+
* directives, MDX JSX). Fields are eager plain stores.
|
|
18
17
|
*/
|
|
19
18
|
export declare function materializeMdastFields(reader: MdastReader, node: object, nodeId: number, nodeType: number): boolean;
|