rusteerio 1.1.1 → 1.1.3

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 (46) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/README.md +37 -37
  4. package/THIRD_PARTY_NOTICES.md +1 -1
  5. package/dist/{chunk-FPRP6UIX.mjs → chunk-HN63P3U7.mjs} +625 -401
  6. package/dist/chunk-HN63P3U7.mjs.map +1 -0
  7. package/dist/index.cjs +624 -400
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.mts +3 -2
  10. package/dist/index.d.ts +3 -2
  11. package/dist/index.mjs +1 -1
  12. package/dist/slim.cjs +555 -407
  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-Cs9UP1Vv.d.mts → types-CJsc0PH-.d.mts} +12 -0
  18. package/dist/{types-Cs9UP1Vv.d.ts → types-CJsc0PH-.d.ts} +12 -0
  19. package/dist/utils.d.mts +1 -1
  20. package/dist/utils.d.ts +1 -1
  21. package/native/src/arena.rs +163 -7
  22. package/native/src/attributes.rs +18 -1
  23. package/native/src/codec.rs +79 -0
  24. package/native/src/lib.rs +119 -30
  25. package/native/src/runtime.rs +23 -0
  26. package/native/src/snapshot.rs +117 -80
  27. package/native/src/tokenizer.rs +34 -20
  28. package/package.json +1 -1
  29. package/rusteerio.darwin-arm64.node +0 -0
  30. package/rusteerio.darwin-x64.node +0 -0
  31. package/rusteerio.linux-arm64-gnu.node +0 -0
  32. package/rusteerio.linux-arm64-musl.node +0 -0
  33. package/rusteerio.linux-x64-gnu.node +0 -0
  34. package/rusteerio.linux-x64-musl.node +0 -0
  35. package/rusteerio.win32-arm64-msvc.node +0 -0
  36. package/rusteerio.win32-x64-msvc.node +0 -0
  37. package/src/extract.ts +117 -24
  38. package/src/index.ts +27 -6
  39. package/src/match.ts +4 -4
  40. package/src/native.ts +4 -0
  41. package/src/nodes.ts +70 -10
  42. package/src/router.ts +80 -4
  43. package/src/selection.ts +18 -17
  44. package/src/snapshot.ts +37 -0
  45. package/src/transport.ts +1 -1
  46. package/dist/chunk-FPRP6UIX.mjs.map +0 -1
package/src/router.ts CHANGED
@@ -78,6 +78,8 @@ export interface DocumentState {
78
78
  collectionRetries: number;
79
79
  /** Whether a collection was due but the document left the pending list; its next use schedules it again. */
80
80
  isCollectionEvicted: boolean;
81
+ /** Whether the native document holds a snapshot built while parsing, valid until the first write. */
82
+ hasPreparedSnapshot: boolean;
81
83
  leases: Set<Lease>;
82
84
  /** Lease count at which released leases are swept. */
83
85
  leaseSweepSize: number;
@@ -218,7 +220,7 @@ export function stateFor(raw: NativeDocument, options: CheerioOptions = {}): Doc
218
220
  raw: raw as ManagedNativeDocument, base: index * STRIDE, space: registry.space, options, rootLocal: undefined, routed: undefined,
219
221
  // Sized up front: node objects are cached out of order, and regrowing a holey array is costly.
220
222
  generation: 0, records: [], bulk: 0, snapshotSize: 0, stale: new Set(), views: new Array(baseline), looseViews: new Map(), looseViewLimit: minimumLooseViewLimit, weakViews: new Map(), baseline,
221
- leases: new Set(), leaseSweepSize: 64, data: new Map(), isExposed: false, hasChildLists: false, collectionRetries: 0, isCollectionEvicted: false,
223
+ leases: new Set(), leaseSweepSize: 64, data: new Map(), isExposed: false, hasChildLists: false, collectionRetries: 0, isCollectionEvicted: false, hasPreparedSnapshot: false,
222
224
  };
223
225
  holder[stateKey] = state;
224
226
  return state;
@@ -382,7 +384,7 @@ function readRecord(state: DocumentState, local: number): NativeNodeRecord | nul
382
384
  // An unchanged small document is read throughout; after a write, often only a few nodes are.
383
385
  const firstSnapshotRead = state.generation === 0 ? size / 100 : 1 + size / 100;
384
386
  if (reads >= Math.min(minimumReadsBeforeSnapshot, firstSnapshotRead)) {
385
- const snapshot = new NodeSnapshot(state.raw.snapshot());
387
+ const snapshot = new NodeSnapshot(takePreparedSnapshot(state, true) ?? state.raw.snapshot());
386
388
  state.snapshotSize = snapshot.size;
387
389
  state.bulk = snapshot;
388
390
  const record = snapshot.record(local);
@@ -392,12 +394,69 @@ function readRecord(state: DocumentState, local: number): NativeNodeRecord | nul
392
394
  return rawOperation(state.raw, 'node', { id: local });
393
395
  }
394
396
 
397
+ /**
398
+ * Lets a routed document nothing has read or changed yet use the snapshot
399
+ * its native document built while parsing, when its reads first need one.
400
+ */
401
+ export function usePreparedSnapshot(document: NativeDocument): void {
402
+ if (!(document instanceof RoutedDocument)) return;
403
+ const state = document.state;
404
+ state.hasPreparedSnapshot = state.generation === 0 && state.bulk === 0;
405
+ }
406
+
407
+ function takePreparedSnapshot(state: DocumentState, isWanted: boolean): NativeSnapshot | undefined {
408
+ if (!state.hasPreparedSnapshot) return undefined;
409
+ state.hasPreparedSnapshot = false;
410
+ return state.raw.takePreparedSnapshot?.(isWanted) ?? undefined;
411
+ }
412
+
395
413
  /** The current snapshot when it holds `local` exactly, so its fields can be read without a record. */
396
414
  export function plainSnapshot(state: DocumentState, local: number): NodeSnapshot | undefined {
397
415
  const bulk = state.bulk;
398
416
  return bulk instanceof NodeSnapshot && !(state.stale.size && state.stale.has(local)) && bulk.isPlain(local) ? bulk : undefined;
399
417
  }
400
418
 
419
+ /**
420
+ * The current snapshot when it holds `local`'s type, name, links and
421
+ * children. Those are kept for every node, also one whose large text or
422
+ * attributes must be read individually.
423
+ */
424
+ export function structureSnapshot(state: DocumentState, local: number): NodeSnapshot | undefined {
425
+ const bulk = state.bulk;
426
+ return bulk instanceof NodeSnapshot && !(state.stale.size && state.stale.has(local)) && bulk.isPresent(local) ? bulk : undefined;
427
+ }
428
+
429
+ /** Nodes a JavaScript `text()` reads before leaving larger subtrees to the native engine. */
430
+ const snapshotTextBudget = 256;
431
+
432
+ /**
433
+ * The text of a small subtree read from the snapshot, as the native `text`
434
+ * concatenates it, or `undefined` to ask the native engine. Text too large
435
+ * for the snapshot is read once per snapshot, so repeated reads do not copy
436
+ * it out of the native tree again.
437
+ */
438
+ function textFromSnapshot(state: DocumentState, local: number): string | undefined {
439
+ let text = '';
440
+ const stack = [local];
441
+ for (let visited = 0; stack.length; visited++) {
442
+ const node = stack.pop()!;
443
+ const snapshot = structureSnapshot(state, node);
444
+ if (!snapshot || visited === snapshotTextBudget) return undefined;
445
+ if (snapshot.isTextAt(node)) {
446
+ text += snapshot.dataOf(node) ?? largeText(state, snapshot, node);
447
+ continue;
448
+ }
449
+ for (let index = snapshot.childCount(node) - 1; index >= 0; index--) stack.push(snapshot.childAt(node, index));
450
+ }
451
+ return text;
452
+ }
453
+
454
+ function largeText(state: DocumentState, snapshot: NodeSnapshot, local: number): string {
455
+ let text = snapshot.largeTexts.get(local);
456
+ if (text === undefined) snapshot.largeTexts.set(local, text = state.raw.text([local]));
457
+ return text;
458
+ }
459
+
401
460
  /** A string attribute of an element, read like the native `attr` lookup. */
402
461
  export function attributeAt(state: DocumentState, local: number, name: string): string | undefined {
403
462
  const snapshot = plainSnapshot(state, local);
@@ -409,6 +468,7 @@ export function attributeAt(state: DocumentState, local: number, name: string):
409
468
 
410
469
  /** Starts a new generation: cached records may no longer match the arena. */
411
470
  export function invalidate(state: DocumentState): void {
471
+ takePreparedSnapshot(state, false);
412
472
  state.generation++;
413
473
  state.records = [];
414
474
  state.bulk = 0;
@@ -417,9 +477,13 @@ export function invalidate(state: DocumentState): void {
417
477
 
418
478
  /** Discards the records of nodes a write changed without touching the tree's shape. */
419
479
  function invalidateNodes(state: DocumentState, locals: number[]): void {
480
+ takePreparedSnapshot(state, false);
420
481
  const isSnapshot = state.bulk instanceof NodeSnapshot;
482
+ const { records } = state;
421
483
  for (const local of locals) {
422
- state.records[local] = undefined;
484
+ // Writing past the end would grow the cache with holes, which V8 turns
485
+ // into a slow dictionary for far indexes.
486
+ if (records[local] !== undefined) records[local] = undefined;
423
487
  if (isSnapshot) state.stale.add(local);
424
488
  }
425
489
  }
@@ -652,7 +716,14 @@ export class RoutedDocument implements NativeDocument {
652
716
  const found = group.state === this.state ? group.state.raw.select(selector, group.locals) : this.call<number[]>(group.state, 'select', { selector, contexts: group.locals });
653
717
  return this.globals(group.state, found);
654
718
  }
655
- text(ids: number[]): string { return this.groups(ids).map((group) => group.state.raw.text(group.locals)).join(''); }
719
+ text(ids: number[]): string {
720
+ if (ids.length === 1) {
721
+ const state = this.owner(ids[0]);
722
+ const text = textFromSnapshot(state, ids[0] - state.base);
723
+ if (text !== undefined) return text;
724
+ }
725
+ return this.groups(ids).map((group) => group.state.raw.text(group.locals)).join('');
726
+ }
656
727
  innerHtml(id: number): string {
657
728
  const state = this.owner(id);
658
729
  const local = id - state.base;
@@ -717,9 +788,14 @@ export class RoutedDocument implements NativeDocument {
717
788
  return this.globals(state, inserted);
718
789
  }
719
790
  const result: number[] = [];
791
+ const isBesideTarget = args.position === 'before' || args.position === 'after';
720
792
  for (let index = 0; index < targets.length; index++) {
721
793
  const target = this.owner(targets[index]);
722
794
  const targetLocal = targets[index] - target.base;
795
+ // As the native insert, skip targets that cannot receive nodes before
796
+ // moving any there: the nodes would leave their document for nothing.
797
+ const record = recordAt(target, targetLocal);
798
+ if (!record || !parentTypes.has(record.type) || (isBesideTarget && record.parent == null)) continue;
723
799
  const local = this.moveInto(target, nodes, index + 1 < targets.length);
724
800
  const inserted = this.call<number[]>(target, command, { ...args, targetIds: [targetLocal], nodeIds: local });
725
801
  result.push(...this.globals(target, inserted));
package/src/selection.ts CHANGED
@@ -483,26 +483,27 @@ export class Cheerio<T = AnyNode> implements ArrayLike<T>, Iterable<T> {
483
483
  [Symbol.iterator](): Iterator<T>;
484
484
  [Symbol.iterator](): IterableIterator<T> { return this._items()[Symbol.iterator](); }
485
485
  /** @internal */ _insert(position: string, content: any[]): this {
486
+ if (typeof content[0] !== 'function') {
487
+ // The native insert skips targets that cannot hold children, and
488
+ // only the last listed target receives the nodes themselves.
489
+ const ids = this.ids;
490
+ if (ids.length) this.op('insert', { targetIds: ids, nodeIds: this._domIds(content), position });
491
+ return this;
492
+ }
486
493
  const targets = this.ids.filter((id) => ['root', 'tag', 'script', 'style', 'cdata'].includes(nodeRecord(this[DOCUMENT], id).type));
487
494
  if (!targets.length)
488
495
  return this;
489
- if (typeof content[0] === 'function')
490
- return (this as Cheerio<any>).each((index, node: any) => {
491
- if (!targets.includes(this.ids[index]))
492
- return;
493
- if ((position === 'before' || position === 'after') && nodeRecord(this[DOCUMENT], this.ids[index]).parent == null)
494
- return;
495
- const value = content[0].call(node, index, this[DOCUMENT].innerHtml(this.ids[index]));
496
- let nodes = this._domIds(value);
497
- if (index < this.length - 1)
498
- nodes = this.op('clone', { ids: nodes, deep: true });
499
- this.op('insert', { targetIds: [this.ids[index]], nodeIds: nodes, position });
500
- }) as this;
501
- let nodes = this._domIds(content);
502
- if (targets[targets.length - 1] !== this.ids[this.ids.length - 1])
503
- nodes = this.op('clone', { ids: nodes, deep: true });
504
- this.op('insert', { targetIds: targets, nodeIds: nodes, position });
505
- return this;
496
+ return (this as Cheerio<any>).each((index, node: any) => {
497
+ if (!targets.includes(this.ids[index]))
498
+ return;
499
+ if ((position === 'before' || position === 'after') && nodeRecord(this[DOCUMENT], this.ids[index]).parent == null)
500
+ return;
501
+ const value = content[0].call(node, index, this[DOCUMENT].innerHtml(this.ids[index]));
502
+ let nodes = this._domIds(value);
503
+ if (index < this.length - 1)
504
+ nodes = this.op('clone', { ids: nodes, deep: true });
505
+ this.op('insert', { targetIds: [this.ids[index]], nodeIds: nodes, position });
506
+ }) as this;
506
507
  }
507
508
  append<T extends AnyNode>(this: Cheerio<T>, ...elems: [
508
509
  (this: AnyNode, i: number, html: string) => BasicAcceptedElems<AnyNode>
package/src/snapshot.ts CHANGED
@@ -87,8 +87,12 @@ export class NodeSnapshot {
87
87
  private readonly text: string;
88
88
  private readonly isEncoded: boolean;
89
89
  private readonly names: (string | undefined)[] = [];
90
+ /** Per interned type: 1 for element types, 2 for root, 3 for text, 0 otherwise. */
91
+ private readonly typeKinds: (number | undefined)[] = [];
90
92
  /** Node object constructors by interned type, resolved once per snapshot. */
91
93
  viewConstructors: unknown = undefined;
94
+ /** Text nodes too large for the arrays, read individually while this snapshot is current. */
95
+ readonly largeTexts = new Map<number, string>();
92
96
 
93
97
  constructor(raw: NativeSnapshot) {
94
98
  const { data } = raw;
@@ -189,6 +193,39 @@ export class NodeSnapshot {
189
193
  return this.nodes[local * STRIDE + 4];
190
194
  }
191
195
 
196
+ /** Whether a node is an element: a tag, script or style. */
197
+ isElementAt(local: number): boolean {
198
+ return this.typeKind(local) === 1;
199
+ }
200
+
201
+ isRootAt(local: number): boolean {
202
+ return this.typeKind(local) === 2;
203
+ }
204
+
205
+ isTextAt(local: number): boolean {
206
+ return this.typeKind(local) === 3;
207
+ }
208
+
209
+ /** Whether a node's record adds fields stored as JSON, such as source locations. */
210
+ hasExtraFields(local: number): boolean {
211
+ return this.nodes[local * STRIDE + 14] !== 0;
212
+ }
213
+
214
+ /** Whether the slot holds a node, rather than one a collection released. */
215
+ isPresent(local: number): boolean {
216
+ return local < this.size && (this.nodes[local * STRIDE + 10] & RELEASED) === 0;
217
+ }
218
+
219
+ private typeKind(local: number): number {
220
+ const reference = this.nodes[local * STRIDE];
221
+ let kind = this.typeKinds[reference];
222
+ if (kind === undefined) {
223
+ const type = this.name(reference);
224
+ kind = this.typeKinds[reference] = type === 'tag' || type === 'script' || type === 'style' ? 1 : type === 'root' ? 2 : type === 'text' ? 3 : 0;
225
+ }
226
+ return kind;
227
+ }
228
+
192
229
  /** The interned reference of a node's type, comparable across nodes of this snapshot. */
193
230
  typeReference(local: number): number {
194
231
  return this.nodes[local * STRIDE];
package/src/transport.ts CHANGED
@@ -106,7 +106,7 @@ export function wrapNativeBinding(binding: NativeBinding): NativeBinding {
106
106
  if (raw.NativeHookStream) wrapped.NativeHookStream = wrapConstructor(raw.NativeHookStream, ([isDocument, options, bridge]) => [isDocument, encodeOptions(options), wrapBridge(bridge)], wrapStream);
107
107
  if (raw.parseWithTreeAdapter) wrapped.parseWithTreeAdapter = (source: string, isDocument: boolean, options: string, bridge: (request: string) => string) => invoke(() => raw.parseWithTreeAdapter(encodeString(source), isDocument, encodeOptions(options), wrapBridge(bridge)));
108
108
  if (raw.parseAsync) wrapped.parseAsync = (source, isDocument, options) => invoke(() => raw.parseAsync!(encodeString(source), isDocument, encodeOptions(options)));
109
- if (raw.parseManyAsync) wrapped.parseManyAsync = (sources, isDocument, threads, options) => invoke(() => raw.parseManyAsync!(sources.map(encodeString), isDocument, threads, encodeOptions(options)));
109
+ if (raw.parseManyAsync) wrapped.parseManyAsync = (sources, isDocument, threads, options, plans) => invoke(() => raw.parseManyAsync!(sources.map(encodeString), isDocument, threads, encodeOptions(options), plans === undefined ? undefined : encodeJson(plans)));
110
110
  if (raw.extractPlanManyAsync) wrapped.extractPlanManyAsync = async (sources, plan, threads, options) => decodeJson(await invoke(() => raw.extractPlanManyAsync!(sources.map(encodeString), encodeJson(plan), threads, encodeOptions(options))));
111
111
  if (raw.extractManyAsync) wrapped.extractManyAsync = async (sources, queries, threads, isDocument, options) => decodeJson(await invoke(() => raw.extractManyAsync!(sources.map(encodeString), queries.map((query) => ({ ...query, key: encodeString(query.key), selector: encodeString(query.selector), ...(query.attr === undefined ? {} : { attr: encodeString(query.attr) }) })), threads, isDocument, encodeOptions(options))));
112
112
  return wrapped;