rusteerio 1.0.1 → 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.
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 +509 -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/selection.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import type { AnyNode, Document, Element, ParentNode, CheerioAPI, CheerioOptions, AcceptedFilters, BasicAcceptedElems, AcceptedElems, ExtractMap, ExtractedMap } from './types.js';
2
2
  import { requireNative, type NativeDocument } from './native.js';
3
- import { getNode, importNodes, nodeBinding, nodeId, nodeRecord, operation, isTag } from './nodes.js';
3
+ import { getNode, importNodes, nodeBinding, nodeId, nodeRecord, operation, isTag, traverseRecords, viewId } from './nodes.js';
4
4
  import { installAttributes, type AttributeMethods } from './attributes.js';
5
5
  import { extract } from './extract.js';
6
- import { sameNodeSpace, retainNodes } from './router.js';
6
+ import { API, DOCUMENT, exposeDocumentTo, IDS, IS_PLAIN, LEASE, leaseNodes, routeDocument, sameNodeSpace, updateLease, VALUES, type Lease } from './router.js';
7
7
  import { parse } from './parse.js';
8
+ import { matchSimpleSelector, selectSimpleWithin } from './match.js';
8
9
  const numeric = /^(?:0|[1-9]\d*)$/;
9
10
  const isMarkup = (value: string) => /<[a-zA-Z!/?]/.test(value);
10
11
  function* customPseudoArguments(selector: string, name: string): Generator<string | undefined> {
@@ -75,137 +76,199 @@ function* customPseudoArguments(selector: string, name: string): Generator<strin
75
76
  index = end;
76
77
  }
77
78
  }
78
- /** An array-like selection of stable handles into the Rust document arena. */
79
+ const isIndex = (property: string | symbol): boolean => {
80
+ if (typeof property !== 'string') return false;
81
+ const first = property.charCodeAt(0);
82
+ return first >= 48 && first <= 57 && numeric.test(property);
83
+ };
84
+ /** Results up to this size are plain objects holding their nodes, like Cheerio's own selections. */
85
+ const plainSelectionLimit = 16;
86
+ const lengthOf = (target: Cheerio<any>): number => target[VALUES]?.length ?? target[IDS].length;
87
+ function nodeAt(target: Cheerio<any>, index: number): any {
88
+ if (target[VALUES]) return target[VALUES][index];
89
+ const id = target[IDS][index];
90
+ return id === undefined ? undefined : getNode(target[DOCUMENT], id);
91
+ }
92
+ function releaseIds(target: Cheerio<any>): void {
93
+ if (target[LEASE]) updateLease(target[DOCUMENT], target[LEASE], target[VALUES] ? [] : target[IDS]);
94
+ }
95
+ function writeIndex(target: Cheerio<any>, index: number, value: any): void {
96
+ if (target[VALUES]) target[VALUES][index] = value;
97
+ else {
98
+ const id = importNodes(target[DOCUMENT], [value])[0];
99
+ if (id === undefined) {
100
+ target[VALUES] = target[IDS].map((existing) => getNode(target[DOCUMENT], existing));
101
+ target[VALUES][index] = value;
102
+ }
103
+ else target[IDS][index] = id;
104
+ }
105
+ releaseIds(target);
106
+ }
107
+ /** Large selections resolve nodes only when an index is read. */
108
+ const selectionHandler: ProxyHandler<Cheerio<any>> = {
109
+ get: (target, property) => isIndex(property) ? nodeAt(target, Number(property)) : property === 'length' ? lengthOf(target) : Reflect.get(target, property),
110
+ set: (target, property, value) => {
111
+ if (isIndex(property)) { writeIndex(target, Number(property), value); return true; }
112
+ if (property === 'length') {
113
+ if (target[VALUES]) target[VALUES].length = value;
114
+ else target[IDS].length = value;
115
+ releaseIds(target);
116
+ return true;
117
+ }
118
+ return Reflect.set(target, property, value);
119
+ },
120
+ has: (target, property) => isIndex(property) ? Number(property) < lengthOf(target) : property === 'length' || Reflect.has(target, property),
121
+ ownKeys: (target) => [...Array.from({ length: lengthOf(target) }, (_, i) => String(i)), 'length', ...Reflect.ownKeys(target)],
122
+ getOwnPropertyDescriptor: (target, property) => {
123
+ if (isIndex(property) && Number(property) < lengthOf(target))
124
+ return { value: nodeAt(target, Number(property)), writable: true, configurable: true, enumerable: true };
125
+ if (property === 'length') return { value: lengthOf(target), writable: true, configurable: true, enumerable: true };
126
+ return Reflect.getOwnPropertyDescriptor(target, property);
127
+ },
128
+ deleteProperty: (target, property) => {
129
+ if (isIndex(property)) {
130
+ if (target[VALUES])
131
+ delete target[VALUES][Number(property)];
132
+ else
133
+ delete target[IDS][Number(property)];
134
+ releaseIds(target);
135
+ return true;
136
+ }
137
+ return Reflect.deleteProperty(target, property);
138
+ },
139
+ };
140
+ /** An array-like selection of native nodes. */
79
141
  export class Cheerio<T = AnyNode> implements ArrayLike<T>, Iterable<T> {
80
142
  [index: number]: T;
81
- /** @internal */ declare _document: NativeDocument;
82
- /** @internal */ declare _api: CheerioAPI;
83
- /** @internal */ declare _ids: number[];
84
- /** @internal */ declare _values: T[] | undefined;
85
- /** @internal */ declare _lease: object;
143
+ /** @internal */ declare [DOCUMENT]: NativeDocument;
144
+ /** @internal */ declare [API]: CheerioAPI;
145
+ /** @internal */ declare [IDS]: number[];
146
+ /** @internal */ declare [VALUES]: T[] | undefined;
147
+ /** @internal */ declare [LEASE]: Lease | undefined;
148
+ /** @internal Whether the selection holds its items as own index properties, as Cheerio's selections do. */ declare [IS_PLAIN]: boolean;
149
+ /** @internal */ get _document(): NativeDocument { return this[DOCUMENT]; }
150
+ /** @internal */ get _api(): CheerioAPI { return this[API]; }
151
+ /** @internal */ get _ids(): number[] { return this[IDS]; }
152
+ declare length: number;
86
153
  declare options: CheerioOptions;
87
154
  declare _root: Cheerio<Document> | null;
88
155
  declare prevObject: Cheerio<any> | undefined;
89
- declare length: number;
90
- cheerio = '[cheerio object]' as const;
156
+ declare cheerio: '[cheerio object]';
91
157
  constructor(api: CheerioAPI, document: NativeDocument, ids: number[] = [], root: Cheerio<Document> | null = null, options: CheerioOptions = {}, values?: T[]) {
92
- Object.defineProperties(this, {
93
- _document: { value: document, writable: true, configurable: true },
94
- _api: { value: api, writable: true, configurable: true },
95
- _ids: { value: ids, writable: true, configurable: true },
96
- _values: { value: values, writable: true, configurable: true },
97
- _lease: { value: retainNodes(document, ids), writable: true, configurable: true },
98
- options: { value: options, writable: true, configurable: true, enumerable: true },
99
- _root: { value: root, writable: true, configurable: true, enumerable: true },
100
- length: { configurable: true, enumerable: true, get: () => this._values?.length ?? this._ids.length, set: (value: number) => { if (this._values)
101
- this._values.length = value;
102
- else
103
- this._ids.length = value; this._lease = retainNodes(document, this._values ? [] : this._ids); } },
104
- });
105
- delete (this as any).cheerio;
106
- return new Proxy(this, {
107
- get: (target, property, receiver) => typeof property === 'string' && numeric.test(property)
108
- ? (target._values ? target._values[Number(property)] : target._ids[Number(property)] === undefined ? undefined : getNode(document, target._ids[Number(property)]))
109
- : Reflect.get(target, property, receiver),
110
- set: (target, property, value, receiver) => {
111
- if (typeof property === 'string' && numeric.test(property)) {
112
- if (target._values)
113
- target._values[Number(property)] = value;
114
- else {
115
- const id = importNodes(document, [value])[0];
116
- if (id === undefined) {
117
- target._values = target._ids.map((existing) => getNode(document, existing)) as T[];
118
- target._values[Number(property)] = value;
119
- }
120
- else
121
- target._ids[Number(property)] = id;
122
- }
123
- target._lease = retainNodes(document, target._values ? [] : target._ids);
124
- return true;
125
- }
126
- return Reflect.set(target, property, value, receiver);
127
- },
128
- has: (target, property) => typeof property === 'string' && numeric.test(property) ? Number(property) < target.length : Reflect.has(target, property),
129
- ownKeys: (target) => [...Array.from({ length: target.length }, (_, i) => String(i)), ...Reflect.ownKeys(target)],
130
- getOwnPropertyDescriptor: (target, property) => typeof property === 'string' && numeric.test(property) && Number(property) < target.length
131
- ? { value: target._values ? target._values[Number(property)] : getNode(document, target._ids[Number(property)]), writable: true, configurable: true, enumerable: true }
132
- : Reflect.getOwnPropertyDescriptor(target, property),
133
- deleteProperty: (target, property) => {
134
- if (typeof property === 'string' && numeric.test(property)) {
135
- if (target._values)
136
- delete target._values[Number(property)];
137
- else
138
- delete target._ids[Number(property)];
139
- target._lease = retainNodes(document, target._values ? [] : target._ids);
140
- return true;
141
- }
142
- return Reflect.deleteProperty(target, property);
143
- },
144
- });
158
+ this[DOCUMENT] = document;
159
+ this[API] = api;
160
+ this[IDS] = ids;
161
+ this[VALUES] = values;
162
+ this[LEASE] = undefined;
163
+ const count = values ? values.length : ids.length;
164
+ const isPlain = count <= plainSelectionLimit;
165
+ this[IS_PLAIN] = isPlain;
166
+ // Own keys enumerate in Cheerio's order: length, options, _root.
167
+ if (isPlain) this.length = count;
168
+ this.options = options;
169
+ this._root = root;
170
+ if (isPlain) {
171
+ // Each node object keeps its own node alive, so a plain selection needs no lease.
172
+ if (values) for (let index = 0; index < count; index++) this[index] = values[index];
173
+ else for (let index = 0; index < count; index++) this[index] = getNode(document, ids[index]) as T;
174
+ return;
175
+ }
176
+ if (!values) this[LEASE] = leaseNodes(document, this, ids);
177
+ return new Proxy(this, selectionHandler);
145
178
  }
146
- /** @internal */ get ids(): number[] { return this._values ? importNodes(this._document, this._values) : this._ids; }
147
- /** @internal */ op<R = any>(command: string, args: unknown = {}): R { const values = args as Record<string, unknown>; return operation<R>(this._document, command, { ...values, options: values.options ?? this.options }); }
179
+ /** @internal Re-reads a plain selection after direct index writes or after its nodes moved documents. */
180
+ _syncIndices(): void {
181
+ const length = this.length;
182
+ const values = this[VALUES];
183
+ const ids = this[IDS];
184
+ let isChanged = (values ?? ids).length !== length;
185
+ for (let index = 0; !isChanged && index < length; index++) isChanged = values ? this[index] !== values[index] : viewId(this[index]) !== ids[index];
186
+ if (!isChanged) return;
187
+ const items: T[] = Array.from({ length }, (_unused, index) => this[index]);
188
+ const isAllNodes = items.every((value: any) => value && typeof value === 'object' && typeof value.type === 'string');
189
+ this[VALUES] = isAllNodes ? undefined : items;
190
+ this[IDS] = isAllNodes ? importNodes(this[DOCUMENT], items) : [];
191
+ }
192
+ /** @internal */ get ids(): number[] {
193
+ if (this[IS_PLAIN]) this._syncIndices();
194
+ return this[VALUES] ? importNodes(this[DOCUMENT], this[VALUES]) : this[IDS];
195
+ }
196
+ /** @internal */ op<R = any>(command: string, args: unknown = {}): R { const values = args as Record<string, unknown>; return operation<R>(this[DOCUMENT], command, { ...values, options: values.options ?? this.options }); }
148
197
  /** @internal */ _query(command: 'select' | 'matches', args: {
149
198
  selector: string;
150
199
  ids?: number[];
151
200
  contexts?: number[];
152
201
  options?: CheerioOptions;
153
202
  }): number[] {
154
- const callbacks = Object.entries(this.options.pseudos ?? {}).filter((entry): entry is [
203
+ const pseudos = this.options.pseudos;
204
+ const callbacks = pseudos ? Object.entries(pseudos).filter((entry): entry is [
155
205
  string,
156
206
  (element: Element, argument?: string | null) => boolean
157
- ] => typeof entry[1] === 'function');
158
- if (!callbacks.length)
159
- return command === 'select' && !args.options ? this._document.select(args.selector, args.contexts) : this.op(command, args);
207
+ ] => typeof entry[1] === 'function') : [];
208
+ if (!callbacks.length) {
209
+ if (command === 'matches' && args.ids) {
210
+ const matched = matchSimpleSelector(this[DOCUMENT], args.ids, args.selector, args.options ?? this.options);
211
+ if (matched) return matched;
212
+ }
213
+ if (command === 'select' && args.contexts) {
214
+ const found = selectSimpleWithin(this[DOCUMENT], args.contexts, args.selector, args.options ?? this.options);
215
+ if (found) return found;
216
+ }
217
+ return command === 'select' && !args.options ? this[DOCUMENT].select(args.selector, args.contexts) : this.op(command, args);
218
+ }
160
219
  const pseudoMatches: Record<string, number[]> = {};
161
- const candidates = [...new Set([...this._document.select('*'), ...(args.ids ?? []), ...(args.contexts ? this._document.select('*', args.contexts) : [])])];
220
+ const candidates = [...new Set([...this[DOCUMENT].select('*'), ...(args.ids ?? []), ...(args.contexts ? this[DOCUMENT].select('*', args.contexts) : [])])];
162
221
  for (const [name, callback] of callbacks) {
163
222
  for (const argument of customPseudoArguments(args.selector, name)) {
164
223
  if (argument !== undefined && callback.length < 2)
165
224
  throw new Error(`Pseudo-class :${name} doesn't have any arguments`);
166
225
  if (argument === undefined && callback.length >= 2)
167
226
  throw new Error(`Pseudo-class :${name} requires an argument`);
168
- pseudoMatches[`${name}(${argument ?? ''})`] = candidates.filter((id) => callback(getNode(this._document, id) as Element, argument ?? null));
227
+ pseudoMatches[`${name}(${argument ?? ''})`] = candidates.filter((id) => callback(getNode(this[DOCUMENT], id) as Element, argument ?? null));
169
228
  }
170
229
  }
171
230
  return this.op(command, { ...args, pseudoMatches });
172
231
  }
173
232
  /** @internal */ _fromIds<R = AnyNode>(ids: number[]): Cheerio<R> {
174
- const result = new (this._api.fn.constructor as typeof Cheerio)<R>(this._api, this._document, ids, this._root, this.options);
233
+ const result = new (this[API].fn.constructor as typeof Cheerio)<R>(this[API], this[DOCUMENT], ids, this._root, this.options);
175
234
  result.prevObject = this;
176
235
  return result;
177
236
  }
178
237
  /** @internal */ _fromValues<R>(values: R[]): Cheerio<R> {
179
238
  const allNodes = values.every((value: any) => value && typeof value === 'object' && typeof value.type === 'string');
180
- const result = new (this._api.fn.constructor as typeof Cheerio)<R>(this._api, this._document, allNodes ? importNodes(this._document, values) : [], this._root, this.options, allNodes ? undefined : values);
239
+ const result = new (this[API].fn.constructor as typeof Cheerio)<R>(this[API], this[DOCUMENT], allNodes ? importNodes(this[DOCUMENT], values) : [], this._root, this.options, allNodes ? undefined : values);
181
240
  result.prevObject = this;
182
241
  return result;
183
242
  }
184
243
  _make<R>(value: ArrayLike<R> | R | string, context?: BasicAcceptedElems<AnyNode>): Cheerio<R>;
185
244
  _make<R>(value: any, context?: any): Cheerio<R> {
186
- const result = typeof value === 'string' || value?.cheerio ? this._api(value, context) : this._fromValues<R>(value == null ? [] : typeof value === 'object' && 'length' in value && !value.type ? Array.from(value) : [value]);
245
+ const result = typeof value === 'string' || value?.cheerio ? this[API](value, context) : this._fromValues<R>(value == null ? [] : typeof value === 'object' && 'length' in value && !value.type ? Array.from(value) : [value]);
187
246
  result.prevObject = this;
188
247
  return result as Cheerio<R>;
189
248
  }
190
249
  _parse(content: any, options: CheerioOptions, isDocument: boolean, context: ParentNode | null): Document {
191
250
  return parse(content, options, isDocument, context);
192
251
  }
193
- _render(content: any): string { return this._document.outerHtml(importNodes(this._document, content)); }
252
+ _render(content: any): string { return this[DOCUMENT].outerHtml(importNodes(this[DOCUMENT], content)); }
194
253
  /** @internal */ _domIds(content: any, contextId?: number): number[] {
195
254
  if (content == null || content === false)
196
255
  return [];
197
256
  if (typeof content === 'string')
198
257
  return this.op('create', { html: content, contextId, options: this.options });
199
- if (content?.cheerio)
200
- return content._document && sameNodeSpace(this._document, content._document) ? content.ids : importNodes(this._document, content.toArray());
258
+ if (content?.cheerio) {
259
+ if (!content[DOCUMENT] || !sameNodeSpace(this[DOCUMENT], content[DOCUMENT]))
260
+ return importNodes(this[DOCUMENT], content.toArray());
261
+ exposeDocumentTo(this[DOCUMENT], content[DOCUMENT]);
262
+ return content.ids;
263
+ }
201
264
  if (typeof content === 'object' && 'length' in content && !content.type)
202
265
  return Array.from(content as ArrayLike<any>).flatMap((item) => this._domIds(item, contextId));
203
- return importNodes(this._document, [content]);
266
+ return importNodes(this[DOCUMENT], [content]);
204
267
  }
205
268
  _makeDomArray<T extends AnyNode>(this: Cheerio<T>, elem?: BasicAcceptedElems<AnyNode> | BasicAcceptedElems<AnyNode>[], clone?: boolean): AnyNode[];
206
269
  _makeDomArray(this: Cheerio<any>, content: any, clone: any = false): any {
207
270
  const ids = this._domIds(content);
208
- return (clone ? this.op<number[]>('clone', { ids, deep: true }) : ids).map((id) => getNode(this._document, id));
271
+ return (clone ? this.op<number[]>('clone', { ids, deep: true }) : ids).map((id) => getNode(this[DOCUMENT], id));
209
272
  }
210
273
  find<T extends AnyNode>(this: Cheerio<T>, selectorOrHaystack?: string | Cheerio<Element> | Element): Cheerio<Element>;
211
274
  find(this: Cheerio<any>, selector?: any): any {
@@ -213,7 +276,7 @@ export class Cheerio<T = AnyNode> implements ArrayLike<T>, Iterable<T> {
213
276
  return this._fromIds([]);
214
277
  if (typeof selector === 'string')
215
278
  return this._findBySelector(selector, Infinity);
216
- const ids = importNodes(this._document, selector);
279
+ const ids = importNodes(this[DOCUMENT], selector);
217
280
  return this._fromIds(ids.filter((id) => this.ids.some((container) => this.op<boolean>('contains', { container, contained: id }))));
218
281
  }
219
282
  _findBySelector<T extends AnyNode>(this: Cheerio<T>, selector: string, limit: number): Cheerio<Element>;
@@ -226,19 +289,19 @@ export class Cheerio<T = AnyNode> implements ArrayLike<T>, Iterable<T> {
226
289
  return ids;
227
290
  if (typeof selector === 'string')
228
291
  return this._query('matches', { ids, selector, options: this.options });
229
- return ids.filter((id, index) => this._predicate(selector, getNode(this._document, id), index));
292
+ return ids.filter((id, index) => this._predicate(selector, getNode(this[DOCUMENT], id), index));
230
293
  }
231
294
  /** @internal */ _predicate(selector: any, node: any, index: number): boolean {
232
295
  if (typeof selector === 'function')
233
296
  return !!selector.call(node, index, node);
234
297
  if (typeof selector === 'string')
235
- return this._query('matches', { ids: importNodes(this._document, [node]), selector, options: this.options }).length > 0;
298
+ return this._query('matches', { ids: importNodes(this[DOCUMENT], [node]), selector, options: this.options }).length > 0;
236
299
  if (selector?.cheerio)
237
300
  return selector.toArray().includes(node);
238
301
  return node === selector;
239
302
  }
240
303
  /** @internal */ _traverse(kind: string, selector?: any): Cheerio<Element> {
241
- let ids = this.op<number[]>('traverse', { ids: this.ids, kind });
304
+ let ids = traverseRecords(this[DOCUMENT], this.ids, kind) ?? this.op<number[]>('traverse', { ids: this.ids, kind });
242
305
  ids = this._filterIds(ids, selector);
243
306
  if (this.length > 1 && ids.length > 1) {
244
307
  if (kind === 'parents' || kind === 'siblings')
@@ -271,8 +334,8 @@ export class Cheerio<T = AnyNode> implements ArrayLike<T>, Iterable<T> {
271
334
  /** @internal */ _until(kind: string, until?: any, filter?: any): Cheerio<Element> {
272
335
  let ids: number[] = [];
273
336
  for (const id of this.ids)
274
- for (const candidate of this.op<number[]>('traverse', { ids: [id], kind })) {
275
- if (until && this._predicate(until, getNode(this._document, candidate), ids.length))
337
+ for (const candidate of traverseRecords(this[DOCUMENT], [id], kind) ?? this.op<number[]>('traverse', { ids: [id], kind })) {
338
+ if (until && this._predicate(until, getNode(this[DOCUMENT], candidate), ids.length))
276
339
  break;
277
340
  ids.push(candidate);
278
341
  }
@@ -293,9 +356,11 @@ export class Cheerio<T = AnyNode> implements ArrayLike<T>, Iterable<T> {
293
356
  return this._fromIds([]);
294
357
  const result: number[] = [];
295
358
  for (const id of this.ids) {
296
- const record = nodeRecord(this._document, id);
297
- const candidates = [...(['tag', 'script', 'style'].includes(record.type) ? [id] : []), ...this.op<number[]>('traverse', { ids: [id], kind: 'parents' })];
298
- const match = candidates.find((candidate) => this._predicate(selector, getNode(this._document, candidate), 0));
359
+ const record = nodeRecord(this[DOCUMENT], id);
360
+ const candidates = [...(['tag', 'script', 'style'].includes(record.type) ? [id] : []), ...(traverseRecords(this[DOCUMENT], [id], 'parents') ?? this.op<number[]>('traverse', { ids: [id], kind: 'parents' }))];
361
+ // One native match for all candidates replaces a round trip per ancestor.
362
+ const matched = typeof selector === 'string' ? new Set(this._filterIds(candidates, selector)) : undefined;
363
+ const match = matched ? candidates.find((candidate) => matched.has(candidate)) : candidates.find((candidate) => this._predicate(selector, getNode(this[DOCUMENT], candidate), 0));
299
364
  if (match !== undefined && !result.includes(match))
300
365
  result.push(match);
301
366
  }
@@ -303,17 +368,29 @@ export class Cheerio<T = AnyNode> implements ArrayLike<T>, Iterable<T> {
303
368
  }
304
369
  each<T>(this: Cheerio<T>, fn: (this: T, i: number, el: T) => void | boolean): Cheerio<T>;
305
370
  each(this: Cheerio<any>, callback: any): any {
306
- const length = this.length;
307
- for (let i = 0; i < length; i++)
308
- if (callback.call(this[i], i, this[i]) === false)
371
+ if (this[IS_PLAIN]) {
372
+ const length = this.length;
373
+ for (let i = 0; i < length; i++)
374
+ if (callback.call(this[i], i, this[i]) === false)
375
+ break;
376
+ return this;
377
+ }
378
+ // Reading ids directly avoids a string-keyed proxy trap per index.
379
+ const values = this[VALUES], ids = this[IDS], document = this[DOCUMENT];
380
+ const length = values ? values.length : ids.length;
381
+ for (let i = 0; i < length; i++) {
382
+ const node = values ? values[i] : getNode(document, ids[i]);
383
+ if (callback.call(node, i, node) === false)
309
384
  break;
385
+ }
310
386
  return this;
311
387
  }
312
388
  map<T, M>(this: Cheerio<T>, fn: (this: T, i: number, el: T) => M[] | M | null | undefined): Cheerio<M>;
313
389
  map<R>(this: Cheerio<any>, callback: any): any {
314
390
  const result: R[] = [];
315
- for (let i = 0; i < this.length; i++) {
316
- const value = callback.call(this[i], i, this[i]);
391
+ const items = this._items();
392
+ for (let i = 0; i < items.length; i++) {
393
+ const value = callback.call(items[i], i, items[i]);
317
394
  if (value != null)
318
395
  result.push(...(Array.isArray(value) ? value : [value]));
319
396
  }
@@ -329,7 +406,7 @@ export class Cheerio<T = AnyNode> implements ArrayLike<T>, Iterable<T> {
329
406
  const matcher: any = match;
330
407
  if (typeof match !== 'string')
331
408
  return nodes.filter((node: any, index: number) => typeof matcher === 'function' ? matcher.call(node, index, node) : matcher?.cheerio ? matcher.toArray().includes(node) : matcher === node);
332
- const document = this?._document ?? nodeBinding(root)?.document ?? nodeBinding(nodes[0])?.document ?? new (requireNative().NativeDocument)('', false);
409
+ const document = this?.[DOCUMENT] ?? nodeBinding(root)?.document ?? nodeBinding(nodes[0])?.document ?? routeDocument(new (requireNative().NativeDocument)('', false));
333
410
  const ids = importNodes(document, nodes);
334
411
  const selected = new Set(operation<number[]>(document, 'matches', { ids, selector: match, options: { xmlMode } }));
335
412
  return nodes.filter((node: any) => { const id = nodeId(node, document); return id !== undefined && selected.has(id); });
@@ -359,40 +436,63 @@ export class Cheerio<T = AnyNode> implements ArrayLike<T>, Iterable<T> {
359
436
  return this;
360
437
  if (index < 0)
361
438
  index += this.length;
362
- return this._fromValues(this[index] === undefined ? [] : [this[index]]);
439
+ const item = this._itemAt(index);
440
+ return this._fromValues(item === undefined ? [] : [item]);
363
441
  }
364
442
  get<T>(this: Cheerio<T>, i: number): T | undefined;
365
443
  get<T>(this: Cheerio<T>): T[];
366
- get(this: Cheerio<any>, index?: any): any { return index == null ? this.toArray() : this[index < 0 ? this.length + index : index]; }
444
+ get(this: Cheerio<any>, index?: any): any { return index == null ? this.toArray() : this._itemAt(index < 0 ? this.length + index : index); }
367
445
  toArray<T>(this: Cheerio<T>): T[];
368
- toArray(this: Cheerio<any>): any { return Array.from({ length: this.length }, (_unused, i) => this[i]); }
446
+ toArray(this: Cheerio<any>): any { return this._items(); }
447
+ /** @internal Every item, read without a proxy trap per index. */
448
+ _items(): T[] {
449
+ if (this[IS_PLAIN]) {
450
+ const length = this.length;
451
+ const items: T[] = new Array(length);
452
+ for (let i = 0; i < length; i++) items[i] = this[i];
453
+ return items;
454
+ }
455
+ const values = this[VALUES], ids = this[IDS], document = this[DOCUMENT];
456
+ return values ? values.slice() : ids.map((id) => getNode(document, id) as T);
457
+ }
458
+ /** @internal */ _itemAt(index: number): T | undefined {
459
+ if (this[IS_PLAIN]) return this[index];
460
+ const values = this[VALUES];
461
+ if (values) return values[index];
462
+ const id = this[IDS][index];
463
+ return id === undefined ? undefined : getNode(this[DOCUMENT], id) as T;
464
+ }
369
465
  slice<T>(this: Cheerio<T>, start?: number, end?: number): Cheerio<T>;
370
- slice(this: Cheerio<any>, start?: any, end?: any): any { return this._values ? this._fromValues(this._values.slice(start, end)) : this._fromIds(this._ids.slice(start, end)); }
466
+ slice(this: Cheerio<any>, start?: any, end?: any): any { const ids = this.ids; return this[VALUES] ? this._fromValues(this[VALUES].slice(start, end)) : this._fromIds(ids.slice(start, end)); }
371
467
  end<T>(this: Cheerio<T>): Cheerio<AnyNode>;
372
468
  end(this: Cheerio<any>): any { return this.prevObject ?? this._make([]); }
373
469
  add<S extends AnyNode, T extends AnyNode>(this: Cheerio<T>, other: string | Cheerio<S> | S | S[], context?: Cheerio<S> | string): Cheerio<S | T>;
374
- add<R extends AnyNode>(this: Cheerio<any>, other: any, context?: any): any { return this._fromIds(this.op('uniqueSort', { ids: [...this.ids, ...this._api(other, context).ids] })); }
470
+ add<R extends AnyNode>(this: Cheerio<any>, other: any, context?: any): any {
471
+ const added = this[API](other, context);
472
+ // Ids of another copy of the package belong to its own id space.
473
+ const addedIds = added[DOCUMENT] && sameNodeSpace(this[DOCUMENT], added[DOCUMENT]) ? added.ids : importNodes(this[DOCUMENT], added.toArray());
474
+ return this._fromIds(this.op('uniqueSort', { ids: [...this.ids, ...addedIds] }));
475
+ }
375
476
  addBack<T extends AnyNode>(this: Cheerio<T>, selector?: string): Cheerio<AnyNode>;
376
477
  addBack(this: Cheerio<any>, selector?: any): any { return this.prevObject ? this.add(selector ? this.prevObject.filter(selector) : this.prevObject) as any : this as any; }
377
478
  index<T extends AnyNode>(this: Cheerio<T>, selectorOrNeedle?: string | Cheerio<AnyNode> | AnyNode): number;
378
- index(this: Cheerio<any>, selector?: any): any { const haystack = selector == null ? this.parent().children() : typeof selector === 'string' ? this._api(selector) : this; const needle = selector == null || typeof selector === 'string' ? this[0] : selector?.cheerio ? selector[0] : selector; return haystack.toArray().indexOf(needle); }
479
+ index(this: Cheerio<any>, selector?: any): any { const haystack = selector == null ? this.parent().children() : typeof selector === 'string' ? this[API](selector) : this; const needle = selector == null || typeof selector === 'string' ? this[0] : selector?.cheerio ? selector[0] : selector; return haystack.toArray().indexOf(needle); }
379
480
  splice(start: number, deleteCount?: number): any[];
380
481
  splice(start: number, deleteCount: number, ...items: any[]): any[];
381
482
  splice(...args: any[]): any[] { return Reflect.apply(Array.prototype.splice, this, args); }
382
483
  [Symbol.iterator](): Iterator<T>;
383
- *[Symbol.iterator](): IterableIterator<T> { for (let i = 0; i < this.length; i++)
384
- yield this[i]; }
484
+ [Symbol.iterator](): IterableIterator<T> { return this._items()[Symbol.iterator](); }
385
485
  /** @internal */ _insert(position: string, content: any[]): this {
386
- const targets = this.ids.filter((id) => ['root', 'tag', 'script', 'style', 'cdata'].includes(nodeRecord(this._document, id).type));
486
+ const targets = this.ids.filter((id) => ['root', 'tag', 'script', 'style', 'cdata'].includes(nodeRecord(this[DOCUMENT], id).type));
387
487
  if (!targets.length)
388
488
  return this;
389
489
  if (typeof content[0] === 'function')
390
490
  return (this as Cheerio<any>).each((index, node: any) => {
391
491
  if (!targets.includes(this.ids[index]))
392
492
  return;
393
- if ((position === 'before' || position === 'after') && nodeRecord(this._document, this.ids[index]).parent == null)
493
+ if ((position === 'before' || position === 'after') && nodeRecord(this[DOCUMENT], this.ids[index]).parent == null)
394
494
  return;
395
- const value = content[0].call(node, index, this._document.innerHtml(this.ids[index]));
495
+ const value = content[0].call(node, index, this[DOCUMENT].innerHtml(this.ids[index]));
396
496
  let nodes = this._domIds(value);
397
497
  if (index < this.length - 1)
398
498
  nodes = this.op('clone', { ids: nodes, deep: true });
@@ -421,15 +521,15 @@ export class Cheerio<T = AnyNode> implements ArrayLike<T>, Iterable<T> {
421
521
  ] | BasicAcceptedElems<AnyNode>[]): Cheerio<T>;
422
522
  after(this: Cheerio<any>, ...content: any[]): any { return this._insert('after', content); }
423
523
  appendTo<T extends AnyNode>(this: Cheerio<T>, target: BasicAcceptedElems<AnyNode>): Cheerio<T>;
424
- appendTo(this: Cheerio<any>, target: any): any { this._api(target).append(this); return this; }
524
+ appendTo(this: Cheerio<any>, target: any): any { this[API](target).append(this); return this; }
425
525
  prependTo<T extends AnyNode>(this: Cheerio<T>, target: BasicAcceptedElems<AnyNode>): Cheerio<T>;
426
- prependTo(this: Cheerio<any>, target: any): any { this._api(target).prepend(this); return this; }
526
+ prependTo(this: Cheerio<any>, target: any): any { this[API](target).prepend(this); return this; }
427
527
  /** @internal */ _insertRelative(target: any, position: string): Cheerio<T> {
428
- const targets = this._api(target).ids;
528
+ const targets = this[API](target).ids;
429
529
  (this as Cheerio<any>).remove();
430
530
  const inserted: number[] = [];
431
531
  for (const id of targets) {
432
- if (nodeRecord(this._document, id).parent == null)
532
+ if (nodeRecord(this[DOCUMENT], id).parent == null)
433
533
  continue;
434
534
  const clones = this.op<number[]>('clone', { ids: this.ids, deep: true });
435
535
  this.op('insert', { targetIds: [id], nodeIds: clones, position });
@@ -446,7 +546,7 @@ export class Cheerio<T = AnyNode> implements ArrayLike<T>, Iterable<T> {
446
546
  replaceWith<T extends AnyNode>(this: Cheerio<T>, content: AcceptedElems<AnyNode>): Cheerio<T>;
447
547
  replaceWith(this: Cheerio<any>, content: any): any {
448
548
  return this.each((index, node: any) => {
449
- if (nodeRecord(this._document, this.ids[index]).parent == null)
549
+ if (nodeRecord(this[DOCUMENT], this.ids[index]).parent == null)
450
550
  return;
451
551
  const value = typeof content === 'function' ? content.call(node, index, node) : content;
452
552
  this.op('insert', { targetIds: [this.ids[index]], nodeIds: this._domIds(value), position: 'replace' });
@@ -462,13 +562,13 @@ export class Cheerio<T = AnyNode> implements ArrayLike<T>, Iterable<T> {
462
562
  html<T extends AnyNode>(this: Cheerio<T>, str: string | Cheerio<T>): Cheerio<T>;
463
563
  html(this: Cheerio<any>, content?: any): any {
464
564
  if (content === undefined)
465
- return this.length && ['root', 'tag', 'script', 'style', 'cdata'].includes(nodeRecord(this._document, this.ids[0]).type) ? this._document.innerHtml(this.ids[0]) : null;
565
+ return this.length && ['root', 'tag', 'script', 'style', 'cdata'].includes(nodeRecord(this[DOCUMENT], this.ids[0]).type) ? this[DOCUMENT].innerHtml(this.ids[0]) : null;
466
566
  if (!content?.cheerio) {
467
567
  this.op('html', { ids: this.ids, value: String(content) });
468
568
  return this;
469
569
  }
470
570
  for (const id of this.ids) {
471
- const nodes = content?.cheerio ? importNodes(this._document, content.toArray()) : this._domIds(String(content), id);
571
+ const nodes = content?.cheerio ? importNodes(this[DOCUMENT], content.toArray()) : this._domIds(String(content), id);
472
572
  this.op('setNode', { id, field: 'children', value: nodes });
473
573
  }
474
574
  return this;
@@ -477,26 +577,26 @@ export class Cheerio<T = AnyNode> implements ArrayLike<T>, Iterable<T> {
477
577
  text<T extends AnyNode>(this: Cheerio<T>, str: string | ((this: AnyNode, i: number, text: string) => string)): Cheerio<T>;
478
578
  text(this: Cheerio<any>, content?: any): any {
479
579
  if (content === undefined)
480
- return this._document.text(this.ids);
580
+ return this[DOCUMENT].text(this.ids);
481
581
  if (typeof content !== 'function') {
482
582
  this.op('text', { ids: this.ids, value: String(content) });
483
583
  return this;
484
584
  }
485
585
  for (let i = 0; i < this.ids.length; i++) {
486
586
  const id = this.ids[i];
487
- const value = typeof content === 'function' ? content.call(getNode(this._document, id), i, this._document.text([id])) : content;
587
+ const value = typeof content === 'function' ? content.call(getNode(this[DOCUMENT], id), i, this[DOCUMENT].text([id])) : content;
488
588
  this.op('text', { ids: [id], value: String(value) });
489
589
  }
490
590
  return this;
491
591
  }
492
592
  toString<T extends AnyNode>(this: Cheerio<T>): string;
493
- toString(this: Cheerio<any>): any { return this._document.outerHtml(this.ids); }
593
+ toString(this: Cheerio<any>): any { return this[DOCUMENT].outerHtml(this.ids); }
494
594
  clone<T extends AnyNode>(this: Cheerio<T>): Cheerio<T>;
495
595
  clone(this: Cheerio<any>): any { return this._fromIds(this.op('clone', { ids: this.ids, deep: true })); }
496
596
  /** @internal */ _deepest(id: number, tagsOnly = false): number {
497
597
  for (;;) {
498
598
  const children = this.op<number[]>('traverse', { ids: [id], kind: 'children' });
499
- const child = tagsOnly ? children.find((candidate) => nodeRecord(this._document, candidate).type === 'tag') : children[0];
599
+ const child = tagsOnly ? children.find((candidate) => nodeRecord(this[DOCUMENT], candidate).type === 'tag') : children[0];
500
600
  if (child === undefined)
501
601
  return id;
502
602
  id = child;
@@ -506,25 +606,25 @@ export class Cheerio<T = AnyNode> implements ArrayLike<T>, Iterable<T> {
506
606
  const lastParent = (this as Cheerio<any>).parents().last();
507
607
  for (let i = 0; i < this.length; i++) {
508
608
  const id = this.ids[i];
509
- const node = getNode(this._document, id);
609
+ const node = getNode(this[DOCUMENT], id);
510
610
  const value = typeof content === 'function' ? content.call(node, i, node) : typeof content === 'string' && !isMarkup(content) ? lastParent.find(content).clone() : content;
511
611
  let wrappers = this._domIds(value);
512
612
  if (i < this.length - 1)
513
613
  wrappers = this.op('clone', { ids: wrappers, deep: true });
514
614
  const wrapper = wrappers[0];
515
- if (wrapper === undefined || !['root', 'tag', 'script', 'style'].includes(nodeRecord(this._document, wrapper).type))
615
+ if (wrapper === undefined || !['root', 'tag', 'script', 'style'].includes(nodeRecord(this[DOCUMENT], wrapper).type))
516
616
  continue;
517
- const deepest = this._deepest(wrapper);
518
617
  if (inner) {
519
- if (!['root', 'tag', 'script', 'style', 'cdata'].includes(nodeRecord(this._document, id).type))
618
+ if (!['root', 'tag', 'script', 'style', 'cdata'].includes(nodeRecord(this[DOCUMENT], id).type))
520
619
  continue;
521
620
  const contents = this.op<number[]>('traverse', { ids: [id], kind: 'contents' });
522
- this.op('setNode', { id: deepest, field: 'children', value: contents });
621
+ this.op('setNode', { id: this._deepest(wrapper), field: 'children', value: contents });
523
622
  this.op('setNode', { id, field: 'children', value: [wrapper] });
524
623
  }
525
- else if (nodeRecord(this._document, id).parent != null) {
526
- this.op('insert', { targetIds: [id], nodeIds: [wrapper], position: 'before' });
527
- this.op('setNode', { id: deepest, field: 'children', value: [id] });
624
+ else if (nodeRecord(this[DOCUMENT], id).parent != null) {
625
+ // A wrapper from another document gets new ids when the insertion moves it here.
626
+ const [inserted = wrapper] = this.op<number[]>('insert', { targetIds: [id], nodeIds: [wrapper], position: 'before' });
627
+ this.op('setNode', { id: this._deepest(inserted), field: 'children', value: [id] });
528
628
  }
529
629
  }
530
630
  return this;
@@ -541,7 +641,7 @@ export class Cheerio<T = AnyNode> implements ArrayLike<T>, Iterable<T> {
541
641
  return this;
542
642
  const first = this[0];
543
643
  const wrappers = this._make(typeof content === 'function' ? content.call(first, 0, first) : content).insertBefore(first);
544
- const root = [...wrappers.ids].reverse().find((id) => nodeRecord(this._document, id).type === 'tag');
644
+ const root = [...wrappers.ids].reverse().find((id) => nodeRecord(this[DOCUMENT], id).type === 'tag');
545
645
  if (root !== undefined)
546
646
  this.op('insert', { targetIds: [this._deepest(root, true)], nodeIds: this.ids, position: 'append' });
547
647
  return this;
@@ -553,6 +653,20 @@ export class Cheerio<T = AnyNode> implements ArrayLike<T>, Iterable<T> {
553
653
  extract<M extends ExtractMap>(this: Cheerio<any>, map: any): any { return extract.call(this as any, map) as ExtractedMap<M>; }
554
654
  }
555
655
  Object.defineProperty(Cheerio.prototype, 'cheerio', { value: '[cheerio object]', writable: true, configurable: true });
656
+ const inspectCustom = Symbol.for('nodejs.util.inspect.custom');
657
+ // Inspection shows the selection's own string keys, as Cheerio's selections
658
+ // have, without the internals stored under symbols.
659
+ Object.defineProperty(Cheerio.prototype, inspectCustom, {
660
+ configurable: true, writable: true,
661
+ value(this: Cheerio<any>, depth: number, options: { stylize(text: string, style: string): string }, inspect: (value: unknown, options: object) => string): string {
662
+ const prototype = Object.getPrototypeOf(this);
663
+ if (depth < 0) return options.stylize(`[${prototype.constructor.name}]`, 'special');
664
+ const shown: Record<string | symbol, unknown> = Object.create(prototype);
665
+ Object.defineProperty(shown, inspectCustom, { value: undefined });
666
+ for (const key of Object.keys(this)) shown[key] = (this as any)[key];
667
+ return inspect(shown, { ...options, depth });
668
+ },
669
+ });
556
670
  export interface Cheerio<T = AnyNode> extends AttributeMethods<T> {
557
671
  }
558
672
  installAttributes(Cheerio);