rusteerio 1.0.1 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/Cargo.lock +42 -1
  2. package/Cargo.toml +8 -1
  3. package/README.md +90 -51
  4. package/THIRD_PARTY_NOTICES.md +107 -2
  5. package/dist/{chunk-DMRD3FGC.mjs → chunk-FPRP6UIX.mjs} +2233 -751
  6. package/dist/chunk-FPRP6UIX.mjs.map +1 -0
  7. package/dist/index.cjs +2232 -750
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.mts +2 -2
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.mjs +1 -1
  12. package/dist/slim.cjs +2226 -745
  13. package/dist/slim.cjs.map +1 -1
  14. package/dist/slim.d.mts +2 -2
  15. package/dist/slim.d.ts +2 -2
  16. package/dist/slim.mjs +1 -1
  17. package/dist/{types-BP6TpxE5.d.mts → types-Cs9UP1Vv.d.mts} +3 -3
  18. package/dist/{types-BP6TpxE5.d.ts → types-Cs9UP1Vv.d.ts} +3 -3
  19. package/dist/utils.d.mts +1 -1
  20. package/dist/utils.d.ts +1 -1
  21. package/native/src/arena.rs +219 -80
  22. package/native/src/attributes.rs +11 -5
  23. package/native/src/document_order.rs +711 -0
  24. package/native/src/extraction.rs +15 -3
  25. package/native/src/gc.rs +12 -3
  26. package/native/src/id_hash.rs +42 -0
  27. package/native/src/immutable.rs +256 -0
  28. package/native/src/lib.rs +1039 -171
  29. package/native/src/runtime.rs +189 -33
  30. package/native/src/selector.rs +346 -44
  31. package/native/src/sink.rs +14 -1
  32. package/native/src/snapshot.rs +527 -0
  33. package/native/src/stream.rs +2 -2
  34. package/native/src/tokenizer.rs +29 -12
  35. package/native/vendor/html5ever-0.35.0/src/tokenizer/char_ref/mod.rs +79 -0
  36. package/package.json +3 -1
  37. package/rusteerio.darwin-arm64.node +0 -0
  38. package/rusteerio.darwin-x64.node +0 -0
  39. package/rusteerio.linux-arm64-gnu.node +0 -0
  40. package/rusteerio.linux-arm64-musl.node +0 -0
  41. package/rusteerio.linux-x64-gnu.node +0 -0
  42. package/rusteerio.linux-x64-musl.node +0 -0
  43. package/rusteerio.win32-arm64-msvc.node +0 -0
  44. package/rusteerio.win32-x64-msvc.node +0 -0
  45. package/src/attributes.ts +78 -44
  46. package/src/codec.ts +22 -3
  47. package/src/extract.ts +86 -2
  48. package/src/hooks.ts +148 -2
  49. package/src/index.ts +51 -40
  50. package/src/match.ts +207 -0
  51. package/src/native.ts +9 -1
  52. package/src/nodes.ts +531 -183
  53. package/src/router.ts +792 -229
  54. package/src/selection.ts +239 -125
  55. package/src/snapshot.ts +255 -0
  56. package/src/transport.ts +51 -42
  57. package/dist/chunk-DMRD3FGC.mjs.map +0 -1
package/src/nodes.ts CHANGED
@@ -1,16 +1,24 @@
1
1
  import type { NativeDocument, NativeNodeRecord } from './native.js';
2
+ import type { NodeSnapshot } from './snapshot.js';
2
3
  import type { AnyNode } from './types.js';
3
4
  import { nodePrototype } from './dom.js';
4
- import { sameNodeSpace } from './router.js';
5
+ import { cachedView, cacheView, exposeTo, isLocalState, isRouted, onViewRefresh, plainSnapshot, recordAt, routedFor, stateIn, VIEW_ATTRIBUTES, VIEW_CHILD_TARGET, VIEW_LOCAL, VIEW_SELF, VIEW_STATE, type DocumentState } from './router.js';
5
6
 
6
7
  export interface NodeBinding { document: NativeDocument; id: number }
7
- interface ViewState { document: NativeDocument; id: number; lease?: object; children?: AnyNode[]; attributes?: Record<string, string>; maps?: Record<string, Record<string, string>> }
8
- interface NodeState { views: WeakMap<object, ViewState>; bindings: WeakMap<object, NodeBinding>; caches: WeakMap<object, Map<number, WeakRef<AnyNode>>>; finalizer: FinalizationRegistry<{ cache: Map<number, WeakRef<AnyNode>>; id: number }> }
9
- const nodeStateKey = Symbol.for('rusteerio.native-node-views.v2');
10
- const nodeGlobals = globalThis as typeof globalThis & { [key: symbol]: NodeState | undefined };
11
- const nodeState: NodeState = nodeGlobals[nodeStateKey] ??= { views: new WeakMap(), bindings: new WeakMap(), caches: new WeakMap(), finalizer: new FinalizationRegistry(({ cache, id }) => { if (!cache.get(id)?.deref()) cache.delete(id); }) };
12
- const { bindings, caches } = nodeState;
8
+
9
+ /** A node object: its fields read the native record for the node's current location. */
10
+ interface View { [VIEW_STATE]: DocumentState; [VIEW_LOCAL]: number; [VIEW_SELF]: View; [VIEW_ATTRIBUTES]: AttributeView | undefined; [VIEW_CHILDREN]: AnyNode[] | undefined; [VIEW_CHILD_TARGET]: AnyNode[] | undefined; [key: string]: any }
11
+ /** A plain attribute object that mirrors the record; its proxy only intercepts writes. */
12
+ /**
13
+ * The handler of a node's attribute proxy, which also keeps the proxy's plain
14
+ * target and the generation it was copied in, so each attribute object costs
15
+ * one target, one handler and one proxy.
16
+ */
17
+ interface AttributeView extends ProxyHandler<Record<string, string>> { view: View; proxy: Record<string, string>; target: Record<string, string>; generation: number; state: DocumentState }
18
+
19
+ const VIEW_CHILDREN = Symbol.for('rusteerio.node.children');
13
20
  const arrayIndex = /^(?:0|[1-9]\d*)$/;
21
+ const emptyRecord = Object.freeze({ id: -1, type: 'released', children: [] }) as unknown as NativeNodeRecord;
14
22
 
15
23
  export function operation<T = any>(document: NativeDocument, command: string, args: unknown = {}): T {
16
24
  const direct = (document as NativeDocument & { operate?(command: string, args: unknown): T }).operate;
@@ -19,13 +27,34 @@ export function operation<T = any>(document: NativeDocument, command: string, ar
19
27
  return (value === undefined || value === '' ? undefined : JSON.parse(value)) as T;
20
28
  }
21
29
 
30
+ /** The id of a native node object, without allocating a binding. */
31
+ export function viewId(node: unknown): number | undefined {
32
+ const view = node as View | null | undefined;
33
+ const state = view?.[VIEW_STATE];
34
+ return state !== undefined && view![VIEW_SELF] === view && isLocalState(state) ? state.base + view![VIEW_LOCAL] : undefined;
35
+ }
36
+
37
+ /** Whether `node` is a node object of a native document, of this or another copy of the package. */
38
+ function isNativeNode(node: unknown): boolean {
39
+ const view = node as View | null | undefined;
40
+ return view?.[VIEW_STATE] !== undefined && view[VIEW_SELF] === view;
41
+ }
42
+
43
+ /** The id of a native node object, made resolvable by `document`'s operations. */
44
+ export function viewIdIn(document: NativeDocument, node: unknown): number | undefined {
45
+ const id = viewId(node);
46
+ if (id !== undefined) exposeTo(document, (node as View)[VIEW_STATE]);
47
+ return id;
48
+ }
49
+
22
50
  export function nodeBinding(node: unknown): NodeBinding | undefined {
23
- return node && typeof node === 'object' ? bindings.get(node) : undefined;
51
+ const id = node && typeof node === 'object' ? viewId(node) : undefined;
52
+ return id === undefined ? undefined : { document: routedFor((node as View)[VIEW_STATE]), id };
24
53
  }
25
54
 
26
55
  export function nodeId(node: unknown, document?: NativeDocument): number | undefined {
27
- const binding = nodeBinding(node);
28
- return binding && (!document || sameNodeSpace(document, binding.document)) ? binding.id : undefined;
56
+ const id = node && typeof node === 'object' ? viewId(node) : undefined;
57
+ return id !== undefined && (!document || isRouted(document)) ? id : undefined;
29
58
  }
30
59
 
31
60
  /** Recreate parse5's null-prototype attribute maps and shared start-tag map. */
@@ -40,201 +69,515 @@ export function hydrateLocation<T>(location: T): T {
40
69
  return location;
41
70
  }
42
71
 
72
+ /** The node's record; its links are ids local to the node's document. */
43
73
  export function nodeRecord(document: NativeDocument, id: number): NativeNodeRecord {
44
- return operation(document, 'node', { id });
74
+ if (!isRouted(document)) return operation(document, 'node', { id });
75
+ const state = stateIn(document, id);
76
+ return recordAt(state, id - state.base) as NativeNodeRecord;
77
+ }
78
+
79
+ const isElementType = (type: string | undefined): boolean => type === 'tag' || type === 'script' || type === 'style';
80
+
81
+ /** Mirrors the native `traverse` operation over cached records; `undefined` defers to it. */
82
+ export function traverseRecords(document: NativeDocument, ids: number[], kind: string): number[] | undefined {
83
+ if (!isRouted(document)) return undefined;
84
+ const result: number[] = [];
85
+ for (const id of ids) {
86
+ const state = stateIn(document, id);
87
+ const { base } = state;
88
+ const node = recordAt(state, id - base);
89
+ if (!node) continue;
90
+ const isElement = (local: number): boolean => isElementType(recordAt(state, local)?.type);
91
+ if (kind === 'children') {
92
+ for (const child of node.children ?? []) if (isElement(child)) result.push(base + child);
93
+ } else if (kind === 'contents') {
94
+ for (const child of node.children ?? []) result.push(base + child);
95
+ } else if (kind === 'parent') {
96
+ if (node.parent != null && isElement(node.parent)) result.push(base + node.parent);
97
+ } else if (kind === 'parents' || kind === 'ancestors') {
98
+ const seen = new Set<number>();
99
+ for (let parent = node.parent; parent != null && !seen.has(parent);) {
100
+ seen.add(parent);
101
+ const ancestor = recordAt(state, parent);
102
+ if (!ancestor || ancestor.type === 'root') break;
103
+ if (isElementType(ancestor.type)) result.push(base + parent);
104
+ parent = ancestor.parent;
105
+ }
106
+ } else if (kind === 'next' || kind === 'prev' || kind === 'nextAll' || kind === 'prevAll') {
107
+ const isForward = kind === 'next' || kind === 'nextAll';
108
+ const isSingle = kind === 'next' || kind === 'prev';
109
+ const seen = new Set<number>();
110
+ for (let sibling = isForward ? node.next : node.prev; sibling != null && !seen.has(sibling);) {
111
+ seen.add(sibling);
112
+ const record = recordAt(state, sibling);
113
+ if (!record) break;
114
+ if (isElementType(record.type)) {
115
+ result.push(base + sibling);
116
+ if (isSingle) break;
117
+ }
118
+ sibling = isForward ? record.next : record.prev;
119
+ }
120
+ } else if (kind === 'siblings') {
121
+ const parent = node.parent == null ? null : recordAt(state, node.parent);
122
+ const local = id - base;
123
+ for (const other of parent?.children ?? []) if (other !== local && isElement(other)) result.push(base + other);
124
+ } else return undefined;
125
+ }
126
+ return result;
127
+ }
128
+
129
+ /** The node object for an arena slot, created on first use and then shared. */
130
+ export function viewAt(state: DocumentState, local: number): AnyNode | undefined {
131
+ const existing = state.views[local] ?? cachedView(state, local);
132
+ if (existing) return existing as AnyNode;
133
+ const parsed = state.adapterNodes?.[local];
134
+ if (parsed) { state.adapterNodes![local] = undefined; return adoptObject(state, local, parsed); }
135
+ const snapshot = plainSnapshot(state, local);
136
+ let constructor: ViewConstructor;
137
+ if (snapshot) constructor = viewConstructor(snapshot.typeOf(local), false);
138
+ else {
139
+ const record = recordAt(state, local);
140
+ if (!record) return undefined;
141
+ constructor = viewConstructor(record.type, record.sourceCodeLocation !== undefined);
142
+ }
143
+ const view = new constructor(state, local);
144
+ cacheView(state, local, view);
145
+ return view as unknown as AnyNode;
146
+ }
147
+
148
+ /** A document root's node object, built without reading its record: its type is known. */
149
+ export function rootNode(document: NativeDocument, id: number): AnyNode {
150
+ if (!isRouted(document)) throw new TypeError('Native nodes require a routed document');
151
+ const state = stateIn(document, id);
152
+ const local = id - state.base;
153
+ const existing = cachedView(state, local);
154
+ if (existing) return existing as AnyNode;
155
+ if (state.adapterNodes?.[local] || state.options.sourceCodeLocationInfo) return viewAt(state, local) as AnyNode;
156
+ const view = new (viewConstructor('root', false))(state, local);
157
+ cacheView(state, local, view);
158
+ return view as unknown as AnyNode;
159
+ }
160
+
161
+ /** Stable DOM node identities; each property reads or updates the Rust arena. */
162
+ export function getNode(document: NativeDocument, id: number, original?: any): AnyNode {
163
+ if (!isRouted(document)) throw new TypeError('Native nodes require a routed document');
164
+ const state = stateIn(document, id);
165
+ const local = id - state.base;
166
+ if (original === undefined) return viewAt(state, local) as AnyNode;
167
+ return adoptObject(state, local, original);
45
168
  }
46
169
 
47
- function setField(document: NativeDocument, id: number, field: string, value: unknown): void {
48
- operation(document, 'setNode', { id, field, value });
170
+ function viewRecord(view: View): NativeNodeRecord {
171
+ const state = view[VIEW_STATE];
172
+ const local = view[VIEW_LOCAL];
173
+ return state.records[local] ?? recordAt(state, local) ?? emptyRecord;
49
174
  }
50
175
 
51
- function attributes(document: NativeDocument, id: number, field = 'attribs', prototype: object | null = (document as NativeDocument & { options?: { _useHtmlParser2?: boolean } }).options?._useHtmlParser2 === false ? null : Object.prototype): Record<string, string> {
52
- const read = () => (nodeRecord(document, id) as any)?.[field] ?? {};
53
- const target = Object.create(prototype);
176
+ function linkedView(view: View, local: number | null | undefined): AnyNode | null {
177
+ return local == null ? null : viewAt(view[VIEW_STATE], local) ?? null;
178
+ }
179
+
180
+ function setField(view: View, field: string, value: unknown): void {
181
+ const state = view[VIEW_STATE];
182
+ operation(routedFor(state), 'setNode', { id: state.base + view[VIEW_LOCAL], field, value });
183
+ }
184
+
185
+ function setPointer(view: View, field: string, value: unknown): void {
186
+ setField(view, field, value == null ? null : importNodes(routedFor(view[VIEW_STATE]), [value])[0]);
187
+ }
188
+
189
+ function attributesOf(view: View): Record<string, string> {
190
+ const state = view[VIEW_STATE];
191
+ let attributes = view[VIEW_ATTRIBUTES];
192
+ if (!attributes) view[VIEW_ATTRIBUTES] = attributes = createAttributes(view, state.options._useHtmlParser2 === false ? null : Object.prototype);
193
+ if (attributes.generation !== state.generation || attributes.state !== state) syncAttributes(view, attributes);
194
+ return attributes.proxy;
195
+ }
196
+
197
+ function createAttributes(view: View, prototype: object | null): AttributeView {
198
+ const target: Record<string, string> = Object.create(prototype);
199
+ // The node lives on the proxy's own handler, so the attribute object stays plain to reflection.
200
+ const handler: AttributeView = Object.create(attributeWrites);
201
+ handler.view = view;
202
+ handler.target = target;
203
+ handler.generation = -1;
204
+ handler.state = view[VIEW_STATE];
205
+ handler.proxy = new Proxy(target, handler);
206
+ return handler;
207
+ }
208
+
209
+ /** Copies the record's attributes into the plain target, preserving key order. */
210
+ function syncAttributes(view: View, attributes: AttributeView): void {
211
+ const { target } = attributes;
212
+ const state = view[VIEW_STATE];
213
+ const local = view[VIEW_LOCAL];
214
+ if (attributes.generation === -1) {
215
+ const snapshot = plainSnapshot(state, local);
216
+ if (snapshot) {
217
+ snapshot.copyAttributes(local, target);
218
+ attributes.generation = state.generation;
219
+ return;
220
+ }
221
+ }
222
+ const values = viewRecord(view).attribs ?? {};
223
+ const keys = Object.keys(target);
224
+ const nextKeys = Object.keys(values);
225
+ let isSameOrder = keys.length === nextKeys.length;
226
+ for (let index = 0; isSameOrder && index < keys.length; index++) isSameOrder = keys[index] === nextKeys[index];
227
+ if (!isSameOrder) for (const key of keys) delete target[key];
228
+ for (const key of nextKeys) {
229
+ if (isSameOrder && target[key] === values[key]) continue;
230
+ if (key === '__proto__') Object.defineProperty(target, key, { value: values[key], writable: true, enumerable: true, configurable: true });
231
+ else target[key] = values[key];
232
+ }
233
+ attributes.generation = view[VIEW_STATE].generation;
234
+ attributes.state = view[VIEW_STATE];
235
+ }
236
+
237
+ /** Re-reads attributes for a node whose attributes a native command changed. */
238
+ function refreshAttributes(view: object): void {
239
+ const attributes = (view as View)[VIEW_ATTRIBUTES];
240
+ if (attributes) syncAttributes(view as View, attributes);
241
+ }
242
+ onViewRefresh(refreshAttributes, fillChildren);
243
+
244
+ /** Reads go straight to the plain target; writes go to Rust, which then refreshes the target. */
245
+ const attributeWrites: ProxyHandler<Record<string, string>> = {
246
+ set(this: AttributeView, target, key, value) {
247
+ if (typeof key === 'symbol') return Reflect.set(target, key, value);
248
+ const view = this.view;
249
+ setField(view, 'attribs', { ...viewRecord(view).attribs, [key]: String(value) });
250
+ syncAttributes(view, view[VIEW_ATTRIBUTES]!);
251
+ return true;
252
+ },
253
+ deleteProperty(this: AttributeView, target, key) {
254
+ if (typeof key === 'symbol') return Reflect.deleteProperty(target, key);
255
+ const view = this.view;
256
+ const values = { ...viewRecord(view).attribs };
257
+ delete values[key];
258
+ setField(view, 'attribs', values);
259
+ syncAttributes(view, view[VIEW_ATTRIBUTES]!);
260
+ return true;
261
+ },
262
+ defineProperty(this: AttributeView, target, key, descriptor) {
263
+ if (typeof key === 'symbol') return Reflect.defineProperty(target, key, descriptor);
264
+ const view = this.view;
265
+ setField(view, 'attribs', { ...viewRecord(view).attribs, [key]: String(descriptor.value) });
266
+ syncAttributes(view, view[VIEW_ATTRIBUTES]!);
267
+ return true;
268
+ },
269
+ };
270
+
271
+ /** Namespace and prefix maps are rare; they read the record on every access. */
272
+ function attributeMap(view: View, field: 'attribsNamespace' | 'attribsPrefix'): Record<string, string> {
273
+ const read = (): Record<string, string> => (viewRecord(view) as any)[field] ?? {};
274
+ const target = Object.create(view[VIEW_STATE].options._useHtmlParser2 === false ? null : Object.prototype);
275
+ const viewField = `x-${field}`;
54
276
  return new Proxy(target, {
55
- get(_target, key) { return typeof key === 'symbol' ? Reflect.get(target, key) : Object.hasOwn(read(), key) ? read()[key] : Reflect.get(target, key); },
56
- set(_target, key, value) { setField(document, id, field, { ...read(), [key]: String(value) }); return true; },
57
- deleteProperty(_target, key) { const values = { ...read() }; delete values[key]; setField(document, id, field, values); return true; },
277
+ get(_target, key) {
278
+ if (typeof key === 'symbol') return Reflect.get(target, key);
279
+ const values = read();
280
+ return Object.hasOwn(values, key) ? values[key] : Reflect.get(target, key);
281
+ },
282
+ set(_target, key, value) { setField(view, viewField, { ...read(), [key]: String(value) }); return true; },
283
+ deleteProperty(_target, key) { const values = { ...read() }; delete values[key as string]; setField(view, viewField, values); return true; },
58
284
  ownKeys() { return Reflect.ownKeys(read()); },
59
285
  has(_target, key) { return Object.hasOwn(read(), key) || key in target; },
60
- getOwnPropertyDescriptor(_target, key) { return Object.hasOwn(read(), key) ? { value: read()[key], writable: true, enumerable: true, configurable: true } : undefined; },
61
- defineProperty(_target, key, descriptor) { const values = { ...read(), [key]: String(descriptor.value) }; setField(document, id, field, values); return true; },
286
+ getOwnPropertyDescriptor(_target, key) { const values = read(); return Object.hasOwn(values, key) ? { value: values[key as string], writable: true, enumerable: true, configurable: true } : undefined; },
287
+ defineProperty(_target, key, descriptor) { setField(view, viewField, { ...read(), [key]: String(descriptor.value) }); return true; },
62
288
  });
63
289
  }
64
290
 
65
- function children(document: NativeDocument, id: number): AnyNode[] {
66
- const read = (): number[] => nodeRecord(document, id)?.children ?? [];
67
- const snapshot = () => read().map((child) => getNode(document, child));
68
- const save = (nodes: unknown[]) => setField(document, id, 'children', importNodes(document, nodes));
69
- const mutating = new Set(['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', 'copyWithin', 'fill']);
70
- const target: AnyNode[] = [];
71
- return new Proxy(target, {
72
- get(_target, key, receiver) {
73
- if (key === 'length') return read().length;
74
- if (typeof key === 'string' && arrayIndex.test(key)) { const child = read()[Number(key)]; return child === undefined ? undefined : getNode(document, child); }
75
- if (typeof key === 'string' && mutating.has(key)) return (...args: unknown[]) => {
76
- const values = snapshot();
77
- const result = Reflect.apply((Array.prototype as any)[key], values, args);
78
- save(values);
79
- return result === values ? receiver : result;
80
- };
81
- return Reflect.get(target, key, receiver);
82
- },
83
- set(_target, key, value) {
84
- const values = snapshot();
85
- if (key === 'length') values.length = value;
86
- else if (typeof key === 'string' && arrayIndex.test(key)) values[Number(key)] = value;
87
- else return Reflect.set(target, key, value);
88
- save(values); return true;
89
- },
90
- deleteProperty(_target, key) { if (typeof key === 'string' && arrayIndex.test(key)) { const values = snapshot(); values.splice(Number(key), 1); save(values); return true; } return Reflect.deleteProperty(target, key); },
91
- has(_target, key) { return typeof key === 'string' && arrayIndex.test(key) ? Number(key) < read().length : Reflect.has(target, key); },
92
- ownKeys() { return [...read().map((_child, i) => String(i)), 'length']; },
93
- getOwnPropertyDescriptor(_target, key) {
94
- if (key === 'length') return { ...Reflect.getOwnPropertyDescriptor(target, 'length')!, value: read().length };
95
- if (typeof key === 'string' && arrayIndex.test(key) && Number(key) < read().length) return { value: getNode(document, read()[Number(key)]), writable: true, enumerable: true, configurable: true };
96
- return Reflect.getOwnPropertyDescriptor(target, key);
291
+ /**
292
+ * A node's children: a real array of node objects, so reads cost what
293
+ * Cheerio's do. Its mutating methods write the whole list to Rust once, and
294
+ * the router refills the array in place whenever Rust reports the parent
295
+ * changed. Assigning an index or `length` directly changes only this array.
296
+ * `constructor` is `Array`, so arrays derived from it are plain.
297
+ */
298
+ class ChildArray extends Array<AnyNode> {
299
+ readonly #view: View;
300
+ constructor(view: View) {
301
+ super();
302
+ this.#view = view;
303
+ }
304
+ static viewOf(list: unknown): View | undefined {
305
+ return list instanceof ChildArray ? list.#view : undefined;
306
+ }
307
+ }
308
+ Object.defineProperty(ChildArray.prototype, 'constructor', { configurable: true, writable: true, value: Array });
309
+ for (const method of ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', 'copyWithin', 'fill'] as const) {
310
+ Object.defineProperty(ChildArray.prototype, method, {
311
+ configurable: true, writable: true,
312
+ value(this: ChildArray, ...args: unknown[]) {
313
+ const view = ChildArray.viewOf(this);
314
+ if (!view) return Reflect.apply((Array.prototype as any)[method], this, args);
315
+ const values = Array.from(this);
316
+ const result = Reflect.apply((Array.prototype as any)[method], values, args);
317
+ saveChildren(view, values);
318
+ return result === values ? this : result;
97
319
  },
98
320
  });
99
321
  }
100
322
 
101
- const parentAliases: Record<string, string> = { parentNode: 'parent', previousSibling: 'prev', nextSibling: 'next' };
102
- const sourceFields = new Set(['type', 'name', 'data', 'namespace', 'prefix', 'startIndex', 'endIndex', 'sourceCodeLocation', 'x-name', 'x-publicId', 'x-systemId', 'x-mode']);
103
- const tagTypes = new Set(['tag', 'script', 'style']);
104
- const parentTypes = new Set(['root', 'tag', 'script', 'style', 'cdata']);
105
- const dataTypes = new Set(['text', 'comment', 'directive']);
106
- function viewKeys(record: NativeNodeRecord): string[] {
107
- const keys = ['parent', 'prev', 'next', 'startIndex', 'endIndex'];
108
- if (parentTypes.has(record.type)) keys.push('children');
109
- if (record.data !== undefined) keys.push('data');
110
- if (record.name !== undefined) keys.push('name');
111
- if (tagTypes.has(record.type)) keys.push('attribs');
112
- keys.push('type');
113
- for (const field of sourceFields) if (!keys.includes(field) && field in record) keys.push(field);
114
- if (tagTypes.has(record.type)) {
115
- if ('attribsNamespace' in record) keys.push('x-attribsNamespace');
116
- if ('attribsPrefix' in record) keys.push('x-attribsPrefix');
117
- }
118
- return keys;
119
- }
120
- function viewValue(state: ViewState, key: string, receiver: any): any {
121
- const { document, id } = state;
122
- const record = nodeRecord(document, id);
123
- if (key === 'cloneNode') return cloneNode;
124
- if (key === 'nodeType') return ({ root: 9, text: 3, comment: 8, cdata: 4 } as Record<string, number>)[record.type] ?? 1;
125
- if (key === 'tagName' && tagTypes.has(record.type)) return record.name;
126
- if (key === 'nodeValue' && dataTypes.has(record.type)) return record.data;
127
- const pointer = parentAliases[key] ?? key;
128
- if (['parent', 'prev', 'next'].includes(pointer)) return record[pointer] == null ? null : getNode(document, record[pointer]);
129
- if (['children', 'childNodes', 'firstChild', 'lastChild'].includes(key) && parentTypes.has(record.type)) {
130
- const list = state.children ??= children(document, id);
131
- return key === 'firstChild' ? list[0] ?? null : key === 'lastChild' ? list[list.length - 1] ?? null : list;
323
+ function saveChildren(view: View, values: unknown[]): void {
324
+ setField(view, 'children', importNodes(routedFor(view[VIEW_STATE]), values));
325
+ }
326
+
327
+ function childList(view: View): AnyNode[] {
328
+ const existing = view[VIEW_CHILDREN];
329
+ if (existing) return existing;
330
+ const state = view[VIEW_STATE];
331
+ if (!state.hasChildLists) { state.hasChildLists = true; state.raw.takeTouched?.(); }
332
+ const list = new ChildArray(view);
333
+ view[VIEW_CHILD_TARGET] = list;
334
+ view[VIEW_CHILDREN] = list;
335
+ fillChildren(view);
336
+ return list;
337
+ }
338
+
339
+ /** Rebuilds a materialized child array from the node's current record. */
340
+ function fillChildren(view: object): void {
341
+ const target = (view as View)[VIEW_CHILD_TARGET];
342
+ if (!target) return;
343
+ const state = (view as View)[VIEW_STATE];
344
+ const local = (view as View)[VIEW_LOCAL];
345
+ const snapshot = plainSnapshot(state, local);
346
+ if (snapshot && state.stale.size === 0) {
347
+ fillFromSnapshot(state, snapshot, local, target);
348
+ return;
132
349
  }
133
- if (key === 'attribs' && tagTypes.has(record.type)) return state.attributes ??= attributes(document, id);
134
- if ((key === 'x-attribsNamespace' && 'attribsNamespace' in record || key === 'x-attribsPrefix' && 'attribsPrefix' in record) && tagTypes.has(record.type)) {
135
- state.maps ??= {};
136
- return state.maps[key] ??= attributes(document, id, key === 'x-attribsNamespace' ? 'attribsNamespace' : 'attribsPrefix');
350
+ const locals = viewRecord(view as View).children ?? [];
351
+ target.length = locals.length;
352
+ for (let index = 0; index < locals.length; index++) target[index] = viewAt(state, locals[index])!;
353
+ }
354
+
355
+ /**
356
+ * The common case of a walk: a node's children read straight from the
357
+ * snapshot arrays and cached strongly, with each type's constructor resolved
358
+ * once per snapshot instead of once per node.
359
+ */
360
+ function fillFromSnapshot(state: DocumentState, snapshot: NodeSnapshot, local: number, target: AnyNode[]): void {
361
+ const count = snapshot.childCount(local);
362
+ const first = snapshot.childStart(local);
363
+ const childIds = snapshot.childIds;
364
+ const views = state.views;
365
+ const isStrong = !state.adapterNodes;
366
+ const constructors = snapshotConstructors(snapshot);
367
+ target.length = count;
368
+ for (let index = 0; index < count; index++) {
369
+ const child = childIds[first + index];
370
+ let node = (views[child] ?? (state.weakViews.size ? cachedView(state, child) : undefined)) as AnyNode | undefined;
371
+ if (node === undefined) {
372
+ const constructor = isStrong && child < state.baseline && snapshot.isPlain(child) ? constructors(snapshot.typeReference(child)) : undefined;
373
+ if (constructor) views[child] = node = new constructor(state, child) as unknown as AnyNode;
374
+ else node = viewAt(state, child)!;
375
+ }
376
+ target[index] = node;
137
377
  }
138
- if (key === 'attributes' && tagTypes.has(record.type)) {
139
- const attrs = state.attributes ??= attributes(document, id);
140
- return Object.keys(attrs).map((name) => ({ name, value: attrs[name], namespace: record.attribsNamespace?.[name], prefix: record.attribsPrefix?.[name] }));
378
+ }
379
+
380
+ function snapshotConstructors(snapshot: NodeSnapshot): (reference: number) => ViewConstructor {
381
+ let lookup = snapshot.viewConstructors as ((reference: number) => ViewConstructor) | undefined;
382
+ if (!lookup) {
383
+ const byReference: ViewConstructor[] = [];
384
+ lookup = (reference) => byReference[reference] ??= viewConstructor(snapshot.name(reference), false);
385
+ snapshot.viewConstructors = lookup;
141
386
  }
142
- if (key === 'sourceCodeLocation') return hydrateLocation(record[key]);
143
- if (sourceFields.has(key)) return record[key] ?? (key === 'startIndex' || key === 'endIndex' ? null : undefined);
144
- return undefined;
145
- }
146
- function writeView(state: ViewState, key: string, value: any): boolean {
147
- const { document, id } = state;
148
- const record = nodeRecord(document, id);
149
- if (key === 'tagName' && tagTypes.has(record.type)) key = 'name';
150
- else if (key === 'nodeValue' && dataTypes.has(record.type)) key = 'data';
151
- else if (key === 'childNodes') key = 'children';
152
- else key = parentAliases[key] ?? key;
153
- if (sourceFields.has(key) || ['attribs', 'x-attribsNamespace', 'x-attribsPrefix'].includes(key)) { setField(document, id, key, value); return true; }
154
- if (key === 'children' && parentTypes.has(record.type)) { setField(document, id, key, importNodes(document, value)); return true; }
155
- if (['parent', 'prev', 'next'].includes(key)) { setField(document, id, key, value == null ? null : importNodes(document, [value])[0]); return true; }
156
- return false;
157
- }
158
- const nodeHandler: ProxyHandler<object> = {
159
- get(target, key, receiver) {
160
- if (Reflect.getOwnPropertyDescriptor(target, key)) return Reflect.get(target, key, receiver);
161
- if (typeof key === 'string') {
162
- const state = nodeState.views.get(target)!;
163
- if (key === 'cloneNode' || key === 'nodeType' || sourceFields.has(key) || ['tagName', 'nodeValue', 'parent', 'prev', 'next', 'parentNode', 'previousSibling', 'nextSibling', 'children', 'childNodes', 'firstChild', 'lastChild', 'attribs', 'attributes', 'x-attribsNamespace', 'x-attribsPrefix'].includes(key)) return viewValue(state, key, receiver);
164
- }
165
- return Reflect.get(target, key, receiver);
166
- },
167
- set(target, key, value, receiver) {
168
- if (Reflect.getOwnPropertyDescriptor(target, key)) return Reflect.set(target, key, value, receiver);
169
- if (typeof key === 'string' && writeView(nodeState.views.get(target)!, key, value)) return true;
170
- return Reflect.set(target, key, value, receiver);
387
+ return lookup;
388
+ }
389
+
390
+ type Accessor = { get(this: View): unknown; set(this: View, value: any): void };
391
+ const recordField = (field: string, fallback?: null): Accessor => ({
392
+ get() { return (viewRecord(this) as any)[field] ?? fallback; },
393
+ set(value) { setField(this, field, value); },
394
+ });
395
+ /** Fields read straight from the snapshot when it holds the node, without building a record. */
396
+ const snapshotField = (field: 'type' | 'name' | 'data'): Accessor => ({
397
+ get() {
398
+ const state = this[VIEW_STATE];
399
+ const local = this[VIEW_LOCAL];
400
+ const snapshot = plainSnapshot(state, local);
401
+ if (snapshot) return field === 'type' ? snapshot.typeOf(local) : field === 'name' ? snapshot.nameOf(local) : snapshot.dataOf(local);
402
+ return (viewRecord(this) as any)[field];
171
403
  },
172
- has(target, key) { return Reflect.has(target, key) || typeof key === 'string' && (viewKeys(nodeRecord(nodeState.views.get(target)!.document, nodeState.views.get(target)!.id)).includes(key) || ['cloneNode', 'nodeType', 'parentNode', 'previousSibling', 'nextSibling'].includes(key)); },
173
- ownKeys(target) { const state = nodeState.views.get(target)!; return [...new Set([...viewKeys(nodeRecord(state.document, state.id)), ...Reflect.ownKeys(target)])]; },
174
- getOwnPropertyDescriptor(target, key) {
175
- const existing = Reflect.getOwnPropertyDescriptor(target, key);
176
- if (existing) return existing;
177
- const state = nodeState.views.get(target)!;
178
- if (typeof key === 'string' && viewKeys(nodeRecord(state.document, state.id)).includes(key)) return { value: viewValue(state, key, undefined), configurable: true, enumerable: true, writable: true };
179
- return undefined;
404
+ set(value) { setField(this, field, value); },
405
+ });
406
+ const linkOffsets = { parent: 1, prev: 2, next: 3 } as const;
407
+ const pointerField = (field: 'parent' | 'prev' | 'next'): Accessor => ({
408
+ get() {
409
+ const state = this[VIEW_STATE];
410
+ const local = this[VIEW_LOCAL];
411
+ const snapshot = plainSnapshot(state, local);
412
+ return linkedView(this, snapshot ? snapshot.linkOf(local, linkOffsets[field]) : viewRecord(this)[field]);
180
413
  },
414
+ set(value) { setPointer(this, field, value); },
415
+ });
416
+ const nodeAccessors: Record<string, Accessor> = {
417
+ type: snapshotField('type'),
418
+ parent: pointerField('parent'),
419
+ prev: pointerField('prev'),
420
+ next: pointerField('next'),
421
+ startIndex: recordField('startIndex', null),
422
+ endIndex: recordField('endIndex', null),
423
+ };
424
+ /** Present only on nodes parsed with location info, as in Cheerio. */
425
+ const locationAccessor: Accessor = { get() { return hydrateLocation(viewRecord(this).sourceCodeLocation); }, set(value) { setField(this, 'sourceCodeLocation', value); } };
426
+ const childAccessors: Record<string, Accessor> = {
427
+ children: { get() { return childList(this); }, set(value) { setField(this, 'children', importNodes(routedFor(this[VIEW_STATE]), value)); } },
181
428
  };
429
+ const elementAccessors: Record<string, Accessor> = {
430
+ name: snapshotField('name'),
431
+ attribs: { get() { return attributesOf(this); }, set(value) { setField(this, 'attribs', value); } },
432
+ namespace: recordField('namespace'),
433
+ prefix: recordField('prefix'),
434
+ 'x-attribsNamespace': { get() { return viewRecord(this).attribsNamespace !== undefined ? attributeMap(this, 'attribsNamespace') : undefined; }, set(value) { setField(this, 'x-attribsNamespace', value); } },
435
+ 'x-attribsPrefix': { get() { return viewRecord(this).attribsPrefix !== undefined ? attributeMap(this, 'attribsPrefix') : undefined; }, set(value) { setField(this, 'x-attribsPrefix', value); } },
436
+ };
437
+ const dataAccessors: Record<string, Accessor> = { data: snapshotField('data') };
438
+ const directiveAccessors: Record<string, Accessor> = { name: recordField('name'), 'x-name': recordField('x-name'), 'x-publicId': recordField('x-publicId'), 'x-systemId': recordField('x-systemId') };
439
+ const documentAccessors: Record<string, Accessor> = { 'x-mode': recordField('x-mode') };
182
440
 
183
- /** Stable DOM node identities; each property reads or updates the Rust arena. */
184
- export function getNode(document: NativeDocument, id: number, original?: any): AnyNode {
185
- const cacheKey = document.nodeSpace ?? document;
186
- let cache = caches.get(cacheKey);
187
- if (!cache) caches.set(cacheKey, cache = new Map());
188
- const existing = cache.get(id)?.deref();
189
- if (existing) return existing;
190
- document = (document as NativeDocument & { nodeDocument?(id: number): NativeDocument }).nodeDocument?.(id) ?? document;
191
- const lease = (document as NativeDocument & { retainNodes?(ids: number[]): object }).retainNodes?.([id]);
192
- const record = nodeRecord(document, id);
193
- if (!record) return undefined as unknown as AnyNode;
194
- if (original === undefined) {
195
- const target = Object.create(nodePrototype(record.type));
196
- const node = new Proxy(target, nodeHandler) as AnyNode;
197
- nodeState.views.set(target, { document, id, lease });
198
- cache.set(id, new WeakRef(node));
199
- nodeState.finalizer.register(node, { cache, id });
200
- bindings.set(node, { document, id });
201
- return node;
441
+ function accessorsFor(type: string): Record<string, Accessor> {
442
+ switch (type) {
443
+ case 'tag': case 'script': case 'style': return { ...nodeAccessors, ...childAccessors, ...elementAccessors };
444
+ case 'root': return { ...nodeAccessors, ...childAccessors, ...documentAccessors };
445
+ case 'cdata': return { ...nodeAccessors, ...childAccessors };
446
+ case 'directive': return { ...nodeAccessors, ...dataAccessors, ...directiveAccessors };
447
+ case 'text': case 'comment': return { ...nodeAccessors, ...dataAccessors };
448
+ default: return { ...nodeAccessors, ...childAccessors, ...dataAccessors, name: recordField('name') };
202
449
  }
203
- const node: any = original;
204
- cache.set(id, new WeakRef(node));
205
- nodeState.finalizer.register(node, { cache, id });
206
- bindings.set(node, { document, id });
207
- const read = (): any => { void lease; return nodeRecord(document, id); };
208
- const define = (key: string, get: () => unknown, set?: (value: any) => void, enumerable = true) => {
209
- try { Object.defineProperty(node, key, { configurable: true, enumerable, get, ...(set ? { set } : {}) }); } catch { /* Foreign user properties may be non-configurable. */ }
210
- };
211
- for (const field of ['type', 'name', 'data', 'namespace', 'prefix', 'startIndex', 'endIndex', 'sourceCodeLocation', 'x-name', 'x-publicId', 'x-systemId', 'x-mode']) {
212
- if (field in record || ['type', 'startIndex', 'endIndex'].includes(field)) define(field, () => (field === 'sourceCodeLocation' ? hydrateLocation(read()[field]) : read()[field]) ?? (field === 'startIndex' || field === 'endIndex' ? null : undefined), (value) => setField(document, id, field, value));
450
+ }
451
+
452
+ type ViewConstructor = new (state: DocumentState, local: number) => View;
453
+ const viewConstructors = new Map<string, ViewConstructor>();
454
+ const plainConstructors: Record<string, ViewConstructor | undefined> = Object.create(null);
455
+
456
+ /** One constructor per node shape so node objects share a hidden class and prototype. */
457
+ function viewConstructor(type: string, hasLocation: boolean): ViewConstructor {
458
+ if (!hasLocation) { const plain = plainConstructors[type]; if (plain) return plain; }
459
+ const key = hasLocation ? `${type}:location` : type;
460
+ let constructor = viewConstructors.get(key);
461
+ if (constructor) return constructor;
462
+ const prototype = Object.create(nodePrototype(type));
463
+ const accessors = hasLocation ? { ...accessorsFor(type), sourceCodeLocation: locationAccessor } : accessorsFor(type);
464
+ for (const [field, accessor] of Object.entries(accessors)) Object.defineProperty(prototype, field, { ...accessor, configurable: true, enumerable: false });
465
+ constructor = function (this: View, state: DocumentState, local: number) {
466
+ this[VIEW_STATE] = state;
467
+ this[VIEW_LOCAL] = local;
468
+ this[VIEW_SELF] = this;
469
+ this[VIEW_ATTRIBUTES] = undefined;
470
+ this[VIEW_CHILDREN] = undefined;
471
+ this[VIEW_CHILD_TARGET] = undefined;
472
+ } as unknown as ViewConstructor;
473
+ Object.defineProperty(prototype, inspectSymbol, { configurable: true, writable: true, value: inspectNode });
474
+ constructor.prototype = prototype;
475
+ viewConstructors.set(key, constructor);
476
+ if (!hasLocation) plainConstructors[type] = constructor;
477
+ return constructor;
478
+ }
479
+
480
+ const inspectSymbol = Symbol.for('nodejs.util.inspect.custom');
481
+ const inspectedFields = ['type', 'name', 'data', 'attribs', 'namespace', 'startIndex', 'endIndex'];
482
+
483
+ /** Console output shows the node's fields instead of its native bookkeeping. */
484
+ function inspectNode(this: View, depth: number, options: object, inspect: (value: unknown, options: object) => string): string {
485
+ const fields: Record<string, unknown> = {};
486
+ for (const field of inspectedFields) if (field in this && this[field] != null) fields[field] = field === 'attribs' ? { ...this.attribs } : this[field];
487
+ if ('children' in this) fields.children = `[${this.children.length} nodes]`;
488
+ const name = Object.getPrototypeOf(Object.getPrototypeOf(this))?.constructor?.name ?? 'Node';
489
+ return depth < 0 ? `[${name}]` : `${name} ${inspect(fields, { ...options, depth: 1 })}`;
490
+ }
491
+
492
+ const adoptedAliases: PropertyDescriptorMap = {
493
+ parentNode: { configurable: true, enumerable: false, get(this: View) { return this.parent; }, set(this: View, value) { this.parent = value; } },
494
+ previousSibling: { configurable: true, enumerable: false, get(this: View) { return this.prev; }, set(this: View, value) { this.prev = value; } },
495
+ nextSibling: { configurable: true, enumerable: false, get(this: View) { return this.next; }, set(this: View, value) { this.next = value; } },
496
+ nodeType: { configurable: true, enumerable: false, get(this: View) { return ({ root: 9, text: 3, comment: 8, cdata: 4 } as Record<string, number>)[viewRecord(this).type] ?? 1; } },
497
+ cloneNode: { configurable: true, enumerable: false, writable: true, value: cloneNode },
498
+ };
499
+ const adoptedChildAliases: PropertyDescriptorMap = {
500
+ childNodes: { configurable: true, enumerable: false, get(this: View) { return this.children; }, set(this: View, value) { this.children = value; } },
501
+ firstChild: { configurable: true, enumerable: false, get(this: View) { return this.children[0] ?? null; } },
502
+ lastChild: { configurable: true, enumerable: false, get(this: View) { return this.children[this.children.length - 1] ?? null; } },
503
+ };
504
+ const adoptedElementAliases: PropertyDescriptorMap = {
505
+ tagName: { configurable: true, enumerable: false, get(this: View) { return this.name; }, set(this: View, value) { this.name = value; } },
506
+ attributes: { configurable: true, enumerable: false, get(this: View) { const attributes = this.attribs; const record = viewRecord(this); return Object.keys(attributes).map((name) => ({ name, value: attributes[name], namespace: record.attribsNamespace?.[name], prefix: record.attribsPrefix?.[name] })); } },
507
+ };
508
+ const adoptedDataAliases: PropertyDescriptorMap = {
509
+ nodeValue: { configurable: true, enumerable: false, get(this: View) { return this.data; }, set(this: View, value) { this.data = value; } },
510
+ };
511
+ const adoptedFields = new Map<string, PropertyDescriptorMap>();
512
+ const optionalAdoptedKeys = ['name', 'data', 'namespace', 'prefix', 'sourceCodeLocation', 'x-name', 'x-publicId', 'x-systemId', 'x-mode', 'x-attribsNamespace', 'x-attribsPrefix'];
513
+ const optionalAdoptedFields = new Set(optionalAdoptedKeys);
514
+ const adoptedShapes = new Map<string, PropertyDescriptorMap>();
515
+
516
+ /**
517
+ * Binds an object created outside the arena (a DOM constructor or a parser
518
+ * hook's node) to its native node, keeping the caller's object identity.
519
+ */
520
+ function adoptObject(state: DocumentState, local: number, original: any): AnyNode {
521
+ const record = recordAt(state, local);
522
+ if (!record) return undefined as unknown as AnyNode;
523
+ const view = original as View;
524
+ const attributesPrototype = original.attribs && typeof original.attribs === 'object' ? Object.getPrototypeOf(original.attribs) : undefined;
525
+ view[VIEW_STATE] = state;
526
+ view[VIEW_LOCAL] = local;
527
+ view[VIEW_SELF] = view;
528
+ view[VIEW_ATTRIBUTES] = undefined;
529
+ view[VIEW_CHILDREN] = undefined;
530
+ view[VIEW_CHILD_TARGET] = undefined;
531
+ let fields = adoptedFields.get(record.type);
532
+ if (!fields) {
533
+ fields = {};
534
+ for (const [key, accessor] of Object.entries({ ...accessorsFor(record.type), sourceCodeLocation: locationAccessor })) fields[key] = { ...accessor, configurable: true, enumerable: true };
535
+ adoptedFields.set(record.type, fields);
213
536
  }
214
- for (const field of ['parent', 'prev', 'next']) define(field, () => { const value = read()[field]; return value === null || value === undefined ? null : getNode(document, value); }, (value) => setField(document, id, field, value == null ? null : importNodes(document, [value])[0]));
215
- for (const [alias, field] of [['parentNode', 'parent'], ['previousSibling', 'prev'], ['nextSibling', 'next']]) define(alias, () => node[field], (value) => { node[field] = value; }, false);
216
- if ('children' in record && ['root', 'tag', 'script', 'style', 'cdata'].includes(record.type)) {
217
- const list = children(document, id);
218
- define('children', () => list, (value) => setField(document, id, 'children', importNodes(document, value)));
219
- define('childNodes', () => list, (value) => { node.children = value; }, false);
220
- define('firstChild', () => list[0] ?? null, undefined, false);
221
- define('lastChild', () => list[list.length - 1] ?? null, undefined, false);
537
+ // Which optional fields a node has varies; each combination's full descriptor map is built once.
538
+ let shape = record.type;
539
+ for (const key of optionalAdoptedKeys) {
540
+ if (!(key in fields)) continue;
541
+ const field = key === 'x-attribsNamespace' ? 'attribsNamespace' : key === 'x-attribsPrefix' ? 'attribsPrefix' : key;
542
+ shape += (record as any)[field] !== undefined || Object.hasOwn(original, key) ? '1' : '0';
222
543
  }
223
- if (['tag', 'script', 'style'].includes(record.type)) {
224
- const attrs = attributes(document, id, 'attribs', Object.getPrototypeOf(original.attribs ?? {}));
225
- define('attribs', () => attrs, (value) => setField(document, id, 'attribs', value));
226
- define('tagName', () => node.name, (value) => { node.name = value; }, false);
227
- for (const field of ['x-attribsNamespace', 'x-attribsPrefix']) {
228
- if (!((field === 'x-attribsNamespace' ? 'attribsNamespace' : 'attribsPrefix') in record) && !Object.hasOwn(original, field)) continue;
229
- const values = attributes(document, id, field === 'x-attribsNamespace' ? 'attribsNamespace' : 'attribsPrefix');
230
- define(field, () => values, (value) => setField(document, id, field, value));
544
+ let descriptors = adoptedShapes.get(shape);
545
+ if (!descriptors) {
546
+ const present: PropertyDescriptorMap = {};
547
+ for (const key in fields) {
548
+ const field = key === 'x-attribsNamespace' ? 'attribsNamespace' : key === 'x-attribsPrefix' ? 'attribsPrefix' : key;
549
+ if (optionalAdoptedFields.has(key) && (record as any)[field] === undefined && !Object.hasOwn(original, key)) continue;
550
+ present[key] = fields[key];
231
551
  }
232
- define('attributes', () => Object.keys(attrs).map((name) => ({ name, value: attrs[name], namespace: read().attribsNamespace?.[name], prefix: read().attribsPrefix?.[name] })), undefined, false);
552
+ const isParent = 'children' in present;
553
+ const isData = record.type === 'text' || record.type === 'comment' || record.type === 'directive';
554
+ descriptors = { ...present, ...adoptedAliases, ...(isParent ? adoptedChildAliases : {}), ...(isElementType(record.type) ? adoptedElementAliases : {}), ...(isData ? adoptedDataAliases : {}) };
555
+ adoptedShapes.set(shape, descriptors);
556
+ }
557
+ const isElement = isElementType(record.type);
558
+ try {
559
+ Object.defineProperties(view, descriptors);
560
+ } catch {
561
+ // Foreign user properties may be non-configurable; those keep their values.
562
+ }
563
+ if (isElement && attributesPrototype !== undefined) view[VIEW_ATTRIBUTES] = createAttributes(view, attributesPrototype);
564
+ cacheView(state, local, view);
565
+ return view as unknown as AnyNode;
566
+ }
567
+
568
+ /**
569
+ * Registers a tree adapter's nodes for the document's slots. Each node can be
570
+ * passed back to the API at once; its fields become live when first returned.
571
+ */
572
+ export function attachParsedNodes(state: DocumentState, nodes: (object | undefined)[]): void {
573
+ state.adapterNodes = nodes;
574
+ for (let local = 0; local < nodes.length; local++) {
575
+ const node = nodes[local] as View | undefined;
576
+ if (!node) continue;
577
+ node[VIEW_STATE] = state;
578
+ node[VIEW_LOCAL] = local;
579
+ node[VIEW_SELF] = node;
233
580
  }
234
- if (['text', 'comment', 'directive'].includes(record.type)) define('nodeValue', () => node.data, (value) => { node.data = value; }, false);
235
- define('nodeType', () => ({ root: 9, text: 3, comment: 8, cdata: 4 } as Record<string, number>)[read().type] ?? 1, undefined, false);
236
- Object.defineProperty(node, 'cloneNode', { configurable: true, enumerable: false, writable: true, value: cloneNode });
237
- return node;
238
581
  }
239
582
 
240
583
  /** Clone through the receiver's current native owner, including borrowed calls. */
@@ -244,6 +587,11 @@ export function cloneNode<T extends AnyNode>(this: T, recursive = false): T {
244
587
  return getNode(binding.document, operation<number[]>(binding.document, 'clone', { ids: [binding.id], deep: recursive })[0]) as T;
245
588
  }
246
589
 
590
+ function childIds(document: NativeDocument, id: number): number[] {
591
+ const state = stateIn(document, id);
592
+ return (recordAt(state, id - state.base)?.children ?? []).map((child) => state.base + child);
593
+ }
594
+
247
595
  /** Import external DOM nodes once and keep their object identities as handles. */
248
596
  export function importNodes(document: NativeDocument, input: unknown): number[] {
249
597
  if (input == null || input === false) return [];
@@ -251,8 +599,8 @@ export function importNodes(document: NativeDocument, input: unknown): number[]
251
599
  const result: number[] = [];
252
600
  for (const node of values) {
253
601
  if (node == null) continue;
254
- const existing = nodeId(node, document);
255
- if (existing !== undefined) { document.retainNode?.(existing); result.push(existing); continue; }
602
+ const existing = node && typeof node === 'object' ? viewIdIn(document, node) : undefined;
603
+ if (existing !== undefined) { result.push(existing); continue; }
256
604
  if (typeof node !== 'object' || typeof node.type !== 'string') continue;
257
605
  const records: any[] = [];
258
606
  const originals = new Map<number, any>();
@@ -273,10 +621,10 @@ export function importNodes(document: NativeDocument, input: unknown): number[]
273
621
  const adopt = (oldId: number, newId: number): void => {
274
622
  const source = originals.get(oldId);
275
623
  const oldChildren = records[oldId].children;
276
- const newChildren = nodeRecord(document, newId).children ?? [];
624
+ const newChildren = childIds(document, newId);
277
625
  // A node already belonging to another Rust document must retain its
278
626
  // original document handle. Imported clones receive their own handles.
279
- getNode(document, newId, nodeBinding(source) ? undefined : source);
627
+ getNode(document, newId, isNativeNode(source) ? undefined : source);
280
628
  for (let i = 0; i < oldChildren.length; i++) if (newChildren[i] !== undefined) adopt(oldChildren[i], newChildren[i]);
281
629
  };
282
630
  adopt(root, imported[0]);
@@ -285,6 +633,6 @@ export function importNodes(document: NativeDocument, input: unknown): number[]
285
633
  return result;
286
634
  }
287
635
 
288
- export const isTag = (node: any): boolean => !!node && ['tag', 'script', 'style'].includes(node.type);
636
+ export const isTag = (node: any): boolean => !!node && isElementType(node.type);
289
637
  export const isDocument = (node: any): boolean => node?.type === 'root';
290
638
  export const hasChildren = (node: any): boolean => !!node && 'children' in node;