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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/README.md +37 -37
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/dist/{chunk-FPRP6UIX.mjs → chunk-HN63P3U7.mjs} +625 -401
- package/dist/chunk-HN63P3U7.mjs.map +1 -0
- package/dist/index.cjs +624 -400
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.mjs +1 -1
- package/dist/slim.cjs +555 -407
- 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-Cs9UP1Vv.d.mts → types-CJsc0PH-.d.mts} +12 -0
- package/dist/{types-Cs9UP1Vv.d.ts → types-CJsc0PH-.d.ts} +12 -0
- package/dist/utils.d.mts +1 -1
- package/dist/utils.d.ts +1 -1
- package/native/src/arena.rs +163 -7
- package/native/src/attributes.rs +18 -1
- package/native/src/codec.rs +79 -0
- package/native/src/lib.rs +119 -30
- package/native/src/runtime.rs +23 -0
- package/native/src/snapshot.rs +117 -80
- package/native/src/tokenizer.rs +34 -20
- package/package.json +1 -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/src/extract.ts +117 -24
- package/src/index.ts +27 -6
- package/src/match.ts +4 -4
- package/src/native.ts +4 -0
- package/src/nodes.ts +70 -10
- package/src/router.ts +80 -4
- package/src/selection.ts +18 -17
- package/src/snapshot.ts +37 -0
- package/src/transport.ts +1 -1
- 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
|
-
|
|
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 {
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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;
|