rusteerio 1.0.0 → 1.1.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/Cargo.lock +42 -1
- package/Cargo.toml +8 -1
- package/README.md +90 -51
- package/THIRD_PARTY_NOTICES.md +138 -2
- package/dist/{chunk-DMRD3FGC.mjs → chunk-FPRP6UIX.mjs} +2233 -751
- package/dist/chunk-FPRP6UIX.mjs.map +1 -0
- package/dist/index.cjs +2232 -750
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/slim.cjs +2226 -745
- package/dist/slim.cjs.map +1 -1
- package/dist/slim.d.mts +2 -2
- package/dist/slim.d.ts +2 -2
- package/dist/slim.mjs +1 -1
- package/dist/{types-BP6TpxE5.d.mts → types-Cs9UP1Vv.d.mts} +3 -3
- package/dist/{types-BP6TpxE5.d.ts → types-Cs9UP1Vv.d.ts} +3 -3
- package/dist/utils.d.mts +1 -1
- package/dist/utils.d.ts +1 -1
- package/native/src/arena.rs +231 -602
- package/native/src/attributes.rs +11 -5
- package/native/src/codec.rs +62 -18
- package/native/src/document_order.rs +711 -0
- package/native/src/extraction.rs +15 -3
- package/native/src/gc.rs +12 -3
- package/native/src/hook_sink.rs +77 -3
- package/native/src/id_hash.rs +42 -0
- package/native/src/immutable.rs +256 -0
- package/native/src/lib.rs +1097 -172
- package/native/src/markup_tokenizer.rs +668 -0
- package/native/src/runtime.rs +189 -33
- package/native/src/selector.rs +346 -44
- package/native/src/sink.rs +14 -1
- package/native/src/snapshot.rs +509 -0
- package/native/src/stream.rs +2 -2
- package/native/src/tokenizer.rs +377 -204
- package/native/vendor/README.md +3 -2
- package/native/vendor/html5ever-0.35.0/src/tokenizer/char_ref/mod.rs +79 -0
- package/native/vendor/html5ever-0.35.0/src/tokenizer/mod.rs +23 -13
- package/package.json +3 -1
- package/rusteerio.darwin-arm64.node +0 -0
- package/rusteerio.darwin-x64.node +0 -0
- package/rusteerio.linux-arm64-gnu.node +0 -0
- package/rusteerio.linux-arm64-musl.node +0 -0
- package/rusteerio.linux-x64-gnu.node +0 -0
- package/rusteerio.linux-x64-musl.node +0 -0
- package/rusteerio.win32-arm64-msvc.node +0 -0
- package/rusteerio.win32-x64-msvc.node +0 -0
- package/scripts/generate-notices.mjs +11 -0
- package/scripts/notices/htmlparser2-MIT.txt +18 -0
- package/scripts/notices/sources.json +6 -0
- package/src/attributes.ts +78 -44
- package/src/codec.ts +22 -3
- package/src/extract.ts +86 -2
- package/src/hooks.ts +148 -2
- package/src/index.ts +51 -40
- package/src/match.ts +207 -0
- package/src/native.ts +9 -1
- package/src/nodes.ts +531 -183
- package/src/router.ts +792 -229
- package/src/selection.ts +239 -125
- package/src/snapshot.ts +255 -0
- package/src/transport.ts +51 -42
- 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 {
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
|
28
|
-
return
|
|
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
|
|
48
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
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) {
|
|
56
|
-
|
|
57
|
-
|
|
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(
|
|
61
|
-
defineProperty(_target, key, descriptor) {
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const state =
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (
|
|
229
|
-
|
|
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
|
-
|
|
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 =
|
|
255
|
-
if (existing !== undefined) {
|
|
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 =
|
|
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,
|
|
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 &&
|
|
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;
|