rusteerio 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +73 -0
- package/Cargo.lock +1051 -0
- package/Cargo.toml +36 -0
- package/LICENSE +21 -0
- package/README.md +252 -2
- package/RELEASING.md +114 -0
- package/THIRD_PARTY_NOTICES.md +7443 -0
- package/build.rs +3 -0
- package/dist/chunk-DKULX6VO.mjs +3185 -0
- package/dist/chunk-DKULX6VO.mjs.map +1 -0
- package/dist/chunk-OHPGEY3E.mjs +32 -0
- package/dist/chunk-OHPGEY3E.mjs.map +1 -0
- package/dist/index.cjs +3230 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +113 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.mjs +38 -0
- package/dist/index.mjs.map +1 -0
- package/dist/slim.cjs +2441 -0
- package/dist/slim.cjs.map +1 -0
- package/dist/slim.d.mts +11 -0
- package/dist/slim.d.ts +11 -0
- package/dist/slim.mjs +19 -0
- package/dist/slim.mjs.map +1 -0
- package/dist/types-BP6TpxE5.d.mts +532 -0
- package/dist/types-BP6TpxE5.d.ts +532 -0
- package/dist/utils.cjs +61 -0
- package/dist/utils.cjs.map +1 -0
- package/dist/utils.d.mts +13 -0
- package/dist/utils.d.ts +13 -0
- package/dist/utils.mjs +17 -0
- package/dist/utils.mjs.map +1 -0
- package/native/src/arena.rs +1781 -0
- package/native/src/attributes.rs +923 -0
- package/native/src/codec.rs +172 -0
- package/native/src/diagnostics.rs +1249 -0
- package/native/src/extraction.rs +226 -0
- package/native/src/gc.rs +166 -0
- package/native/src/hook_sink.rs +727 -0
- package/native/src/hook_stream.rs +513 -0
- package/native/src/lib.rs +2095 -0
- package/native/src/location.rs +392 -0
- package/native/src/runtime.rs +189 -0
- package/native/src/selector.rs +1287 -0
- package/native/src/sink.rs +261 -0
- package/native/src/stream.rs +334 -0
- package/native/src/tokenizer.rs +355 -0
- package/native/src/tracker.rs +232 -0
- package/native/vendor/README.md +25 -0
- package/native/vendor/html5ever-0.35.0/.cargo_vcs_info.json +6 -0
- package/native/vendor/html5ever-0.35.0/Cargo.lock +828 -0
- package/native/vendor/html5ever-0.35.0/Cargo.toml +85 -0
- package/native/vendor/html5ever-0.35.0/LICENSE-APACHE +201 -0
- package/native/vendor/html5ever-0.35.0/LICENSE-MIT +25 -0
- package/native/vendor/html5ever-0.35.0/README.md +60 -0
- package/native/vendor/html5ever-0.35.0/benches/html5ever.rs +86 -0
- package/native/vendor/html5ever-0.35.0/data/bench/lipsum-zh.html +19 -0
- package/native/vendor/html5ever-0.35.0/data/bench/lipsum.html +40 -0
- package/native/vendor/html5ever-0.35.0/data/bench/medium-fragment.html +24 -0
- package/native/vendor/html5ever-0.35.0/data/bench/small-fragment.html +7 -0
- package/native/vendor/html5ever-0.35.0/data/bench/strong.html +1 -0
- package/native/vendor/html5ever-0.35.0/data/bench/tiny-fragment.html +1 -0
- package/native/vendor/html5ever-0.35.0/examples/arena.rs +356 -0
- package/native/vendor/html5ever-0.35.0/examples/capi/tokenize.c +74 -0
- package/native/vendor/html5ever-0.35.0/examples/noop-tokenize.rs +47 -0
- package/native/vendor/html5ever-0.35.0/examples/noop-tree-builder.rs +132 -0
- package/native/vendor/html5ever-0.35.0/examples/print-tree-actions.rs +185 -0
- package/native/vendor/html5ever-0.35.0/examples/tokenize.rs +111 -0
- package/native/vendor/html5ever-0.35.0/src/driver.rs +148 -0
- package/native/vendor/html5ever-0.35.0/src/lib.rs +30 -0
- package/native/vendor/html5ever-0.35.0/src/macros.rs +34 -0
- package/native/vendor/html5ever-0.35.0/src/serialize/mod.rs +255 -0
- package/native/vendor/html5ever-0.35.0/src/tokenizer/char_ref/mod.rs +451 -0
- package/native/vendor/html5ever-0.35.0/src/tokenizer/interface.rs +99 -0
- package/native/vendor/html5ever-0.35.0/src/tokenizer/mod.rs +2305 -0
- package/native/vendor/html5ever-0.35.0/src/tokenizer/states.rs +97 -0
- package/native/vendor/html5ever-0.35.0/src/tree_builder/data.rs +170 -0
- package/native/vendor/html5ever-0.35.0/src/tree_builder/mod.rs +1848 -0
- package/native/vendor/html5ever-0.35.0/src/tree_builder/rules.rs +1494 -0
- package/native/vendor/html5ever-0.35.0/src/tree_builder/tag_sets.rs +114 -0
- package/native/vendor/html5ever-0.35.0/src/tree_builder/types.rs +91 -0
- package/native/vendor/html5ever-0.35.0/src/util/str.rs +52 -0
- package/native/vendor/html5gum-0.8.4/Cargo.lock +1006 -0
- package/native/vendor/html5gum-0.8.4/Cargo.toml +109 -0
- package/native/vendor/html5gum-0.8.4/LICENSE +19 -0
- package/native/vendor/html5gum-0.8.4/README.md +110 -0
- package/native/vendor/html5gum-0.8.4/benches/files/0050.html +2131 -0
- package/native/vendor/html5gum-0.8.4/benches/files/6D30.html +1004 -0
- package/native/vendor/html5gum-0.8.4/benches/files.rs +32 -0
- package/native/vendor/html5gum-0.8.4/benches/patterns.rs +31 -0
- package/native/vendor/html5gum-0.8.4/src/arrayvec.rs +29 -0
- package/native/vendor/html5gum-0.8.4/src/char_validator.rs +134 -0
- package/native/vendor/html5gum-0.8.4/src/emitters/callback.rs +676 -0
- package/native/vendor/html5gum-0.8.4/src/emitters/default.rs +207 -0
- package/native/vendor/html5gum-0.8.4/src/emitters/emitter.rs +344 -0
- package/native/vendor/html5gum-0.8.4/src/emitters/html5ever.rs +306 -0
- package/native/vendor/html5gum-0.8.4/src/emitters/mod.rs +25 -0
- package/native/vendor/html5gum-0.8.4/src/entities.rs +3342 -0
- package/native/vendor/html5gum-0.8.4/src/error.rs +96 -0
- package/native/vendor/html5gum-0.8.4/src/htmlstring.rs +130 -0
- package/native/vendor/html5gum-0.8.4/src/lib.rs +66 -0
- package/native/vendor/html5gum-0.8.4/src/machine.rs +2136 -0
- package/native/vendor/html5gum-0.8.4/src/machine_helper.rs +277 -0
- package/native/vendor/html5gum-0.8.4/src/read_helper.rs +244 -0
- package/native/vendor/html5gum-0.8.4/src/reader.rs +440 -0
- package/native/vendor/html5gum-0.8.4/src/span.rs +98 -0
- package/native/vendor/html5gum-0.8.4/src/state.rs +16 -0
- package/native/vendor/html5gum-0.8.4/src/testutils.rs +71 -0
- package/native/vendor/html5gum-0.8.4/src/tokenizer.rs +189 -0
- package/native/vendor/html5gum-0.8.4/src/utils.rs +88 -0
- package/native/vendor/markup5ever-0.35.0/.cargo_vcs_info.json +6 -0
- package/native/vendor/markup5ever-0.35.0/Cargo.toml +45 -0
- package/native/vendor/markup5ever-0.35.0/LICENSE-APACHE +201 -0
- package/native/vendor/markup5ever-0.35.0/LICENSE-MIT +25 -0
- package/native/vendor/markup5ever-0.35.0/interface/mod.rs +422 -0
- package/native/vendor/markup5ever-0.35.0/interface/tree_builder.rs +280 -0
- package/native/vendor/markup5ever-0.35.0/lib.rs +59 -0
- package/native/vendor/markup5ever-0.35.0/serialize.rs +77 -0
- package/native/vendor/markup5ever-0.35.0/util/buffer_queue.rs +333 -0
- package/native/vendor/markup5ever-0.35.0/util/smallcharset.rs +88 -0
- package/package.json +137 -4
- 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/check-native.mjs +15 -0
- package/scripts/check-release.mjs +235 -0
- package/scripts/fix-declarations.mjs +27 -0
- package/scripts/generate-notices.mjs +176 -0
- package/scripts/notices/README.md +13 -0
- package/scripts/notices/cheerio-MIT.txt +21 -0
- package/scripts/notices/html5ever-AUTHORS.txt +20 -0
- package/scripts/notices/html5ever-COPYRIGHT.txt +8 -0
- package/scripts/notices/html5ever-LICENSE-APACHE.txt +201 -0
- package/scripts/notices/html5ever-LICENSE-MIT.txt +25 -0
- package/scripts/notices/napi-rs-MIT.txt +43 -0
- package/scripts/notices/neon-MIT.txt +19 -0
- package/scripts/notices/runtime-01c266bced4a434da005.txt +3 -0
- package/scripts/notices/runtime-0a54e647fe54104658b5.txt +14 -0
- package/scripts/notices/runtime-0b74dfa0bcee5c420c6b.txt +25 -0
- package/scripts/notices/runtime-0d542e0c8804e39aa7f3.txt +177 -0
- package/scripts/notices/runtime-0f96a83840e146e43c0e.txt +21 -0
- package/scripts/notices/runtime-123a331b5dbf04c30097.txt +25 -0
- package/scripts/notices/runtime-172020dbfd5b53a226df.txt +34 -0
- package/scripts/notices/runtime-209fbbe0ad52d9235e37.txt +26 -0
- package/scripts/notices/runtime-23f18e03dc49df91622f.txt +23 -0
- package/scripts/notices/runtime-268872b9816f90fd8e85.txt +220 -0
- package/scripts/notices/runtime-35242e7a83f69875e6ed.txt +176 -0
- package/scripts/notices/runtime-378f5840b258e2779c39.txt +25 -0
- package/scripts/notices/runtime-4108245a1f2df9d4e94d.txt +25 -0
- package/scripts/notices/runtime-4455bf75a91154108304.txt +21 -0
- package/scripts/notices/runtime-553fffcd9b1cb158bc3e.txt +13 -0
- package/scripts/notices/runtime-62c7a1e35f56406896d7.txt +176 -0
- package/scripts/notices/runtime-6485b8ed310d3f0340bf.txt +25 -0
- package/scripts/notices/runtime-69fef7b0f322a6555415.txt +201 -0
- package/scripts/notices/runtime-6df43f6f4b5d4587f3d8.txt +187 -0
- package/scripts/notices/runtime-799e9ca9d179295ef372.txt +24 -0
- package/scripts/notices/runtime-7b63ecd5f1902af1b637.txt +25 -0
- package/scripts/notices/runtime-7bc7d5de2a0b793d4e1a.txt +21 -0
- package/scripts/notices/runtime-7e12e5df4bae12cb2158.txt +24 -0
- package/scripts/notices/runtime-861399f8c21c042b1105.txt +12 -0
- package/scripts/notices/runtime-8ada45cd9f843acf64e4.txt +201 -0
- package/scripts/notices/runtime-90eb64f0279b0d9432ac.txt +12 -0
- package/scripts/notices/runtime-a60eea817514531668d7.txt +201 -0
- package/scripts/notices/runtime-a7c936ff1ed8fa340172.txt +25 -0
- package/scripts/notices/runtime-ab6eec6caf0fa5775e41.txt +275 -0
- package/scripts/notices/runtime-b1181a40b2a7b25cf66f.txt +19 -0
- package/scripts/notices/runtime-b71bd43a069ca0641a9e.txt +25 -0
- package/scripts/notices/runtime-c16f8dcf1a368b83be78.txt +201 -0
- package/scripts/notices/runtime-c2cfccb812fe482101a8.txt +21 -0
- package/scripts/notices/runtime-e99d88d232bf57d70f0f.txt +25 -0
- package/scripts/notices/runtime-f32fb3b417a194167cfa.txt +9 -0
- package/scripts/notices/runtime-f5062c9a188d81dfe66b.txt +39 -0
- package/scripts/notices/runtime-f9070a832833fa7488fd.txt +4 -0
- package/scripts/notices/runtime-fab3dd6bdab226f1c086.txt +373 -0
- package/scripts/notices/runtime-fba3318d0c600380177e.txt +62 -0
- package/scripts/notices/runtime-ff8f68cb076caf8cefe7.txt +25 -0
- package/scripts/notices/runtime-ff92bed461f50338dd70.txt +74 -0
- package/scripts/notices/runtime.json +1632 -0
- package/scripts/notices/sources.json +555 -0
- package/scripts/notices/xray-MIT.txt +21 -0
- package/src/attributes.ts +271 -0
- package/src/codec.ts +74 -0
- package/src/dom.ts +87 -0
- package/src/extract-plan.ts +97 -0
- package/src/extract.ts +130 -0
- package/src/hooks.ts +220 -0
- package/src/index.ts +223 -0
- package/src/io.ts +491 -0
- package/src/native.ts +135 -0
- package/src/nodes.ts +290 -0
- package/src/parse.ts +53 -0
- package/src/parser-types.ts +157 -0
- package/src/router.ts +327 -0
- package/src/selection.ts +566 -0
- package/src/slim.ts +12 -0
- package/src/transport.ts +104 -0
- package/src/types.ts +186 -0
- package/src/utils.ts +29 -0
- package/src/workers.ts +21 -0
- package/tsconfig.json +15 -0
- package/tsup.config.ts +16 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import type { AnyNode, Element, SerializedField } from './types.js';
|
|
2
|
+
import type { Cheerio } from './selection.js';
|
|
3
|
+
import type { NativeDocument } from './native.js';
|
|
4
|
+
import { getNode, hydrateLocation, importNodes, nodeRecord } from './nodes.js';
|
|
5
|
+
|
|
6
|
+
export interface StyleProp {
|
|
7
|
+
length: number;
|
|
8
|
+
[key: string]: string | number;
|
|
9
|
+
[index: number]: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface AttributeMethods<T = AnyNode> {
|
|
13
|
+
attr<U extends AnyNode>(this: Cheerio<U>, name: string): string | undefined;
|
|
14
|
+
attr<U extends AnyNode>(this: Cheerio<U>): Record<string, string> | undefined;
|
|
15
|
+
attr<U extends AnyNode>(this: Cheerio<U>, name: string, value?: string | null | ((this: Element, index: number, value: string) => string | null)): Cheerio<U>;
|
|
16
|
+
attr<U extends AnyNode>(this: Cheerio<U>, values: Record<string, string | null>): Cheerio<U>;
|
|
17
|
+
prop<U extends AnyNode>(this: Cheerio<U>, name: 'tagName' | 'nodeName'): string | undefined;
|
|
18
|
+
prop<U extends AnyNode>(this: Cheerio<U>, name: 'innerHTML' | 'outerHTML' | 'innerText' | 'textContent'): string | null;
|
|
19
|
+
prop<U extends AnyNode>(this: Cheerio<U>, name: 'style'): StyleProp | undefined;
|
|
20
|
+
prop<U extends AnyNode>(this: Cheerio<U>, name: 'href' | 'src'): string | undefined;
|
|
21
|
+
prop<U extends AnyNode, K extends keyof Element>(this: Cheerio<U>, name: K): Element[K];
|
|
22
|
+
prop<U extends AnyNode, K extends keyof Element>(this: Cheerio<U>, name: K, value: Element[K] | ((this: Element, index: number, property: K) => Element[keyof Element])): Cheerio<U>;
|
|
23
|
+
prop<U extends AnyNode>(this: Cheerio<U>, values: Record<string, string | Element[keyof Element] | boolean>): Cheerio<U>;
|
|
24
|
+
prop<U extends AnyNode>(this: Cheerio<U>, name: string, value: string | boolean | null | ((this: Element, index: number, property: string) => string | boolean)): Cheerio<U>;
|
|
25
|
+
prop<U extends AnyNode>(this: Cheerio<U>, name: string): string;
|
|
26
|
+
data<U extends AnyNode>(this: Cheerio<U>, name: string): unknown;
|
|
27
|
+
data<U extends AnyNode>(this: Cheerio<U>): Record<string, unknown>;
|
|
28
|
+
data<U extends AnyNode>(this: Cheerio<U>, name: string, value: unknown): Cheerio<U>;
|
|
29
|
+
data<U extends AnyNode>(this: Cheerio<U>, values: Record<string, unknown>): Cheerio<U>;
|
|
30
|
+
val<U extends AnyNode>(this: Cheerio<U>): string | string[] | undefined;
|
|
31
|
+
val<U extends AnyNode>(this: Cheerio<U>, value: string | string[]): Cheerio<U>;
|
|
32
|
+
removeAttr<U extends AnyNode>(this: Cheerio<U>, name: string): Cheerio<U>;
|
|
33
|
+
hasClass<U extends AnyNode>(this: Cheerio<U>, name: string): boolean;
|
|
34
|
+
addClass<U extends AnyNode, R extends ArrayLike<U>>(this: R, name?: string | ((this: Element, index: number, current: string) => string | undefined)): R;
|
|
35
|
+
removeClass<U extends AnyNode, R extends ArrayLike<U>>(this: R, name?: string | ((this: Element, index: number, current: string) => string | undefined)): R;
|
|
36
|
+
toggleClass<U extends AnyNode, R extends ArrayLike<U>>(this: R, name?: string | ((this: Element, index: number, current: string, state?: boolean) => string), state?: boolean): R;
|
|
37
|
+
css<U extends AnyNode>(this: Cheerio<U>, names?: string[]): Record<string, string> | undefined;
|
|
38
|
+
css<U extends AnyNode>(this: Cheerio<U>, name: string): string | undefined;
|
|
39
|
+
css<U extends AnyNode>(this: Cheerio<U>, name: string, value: string | ((this: Element, index: number, current: string) => string | undefined)): Cheerio<U>;
|
|
40
|
+
css<U extends AnyNode>(this: Cheerio<U>, values: Record<string, string>): Cheerio<U>;
|
|
41
|
+
serializeArray<U extends AnyNode>(this: Cheerio<U>): SerializedField[];
|
|
42
|
+
serialize<U extends AnyNode>(this: Cheerio<U>): string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type Selection = Cheerio<AnyNode>;
|
|
46
|
+
type DataRecord = Record<string, unknown>;
|
|
47
|
+
type DataCache = Map<number, WeakRef<DataRecord>>;
|
|
48
|
+
type DataDocument = NativeDocument & {
|
|
49
|
+
nodeSpace?: object;
|
|
50
|
+
retainNodeData?: (id: number, data: DataRecord) => void;
|
|
51
|
+
nodeData?: (id: number) => DataRecord | undefined;
|
|
52
|
+
nodeDataOperation?: (id: number) => (command: string, args: unknown) => unknown;
|
|
53
|
+
};
|
|
54
|
+
const dataObjects = new WeakMap<object, DataCache>();
|
|
55
|
+
const dataFinalizer = new FinalizationRegistry<{ cache: WeakRef<DataCache>; id: number; reference: WeakRef<DataRecord> }>(({ cache, id, reference }) => {
|
|
56
|
+
const records = cache.deref();
|
|
57
|
+
if (records?.get(id) === reference) records.delete(id);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function isElement(selection: Selection, id: number): boolean {
|
|
61
|
+
return ['tag', 'script', 'style'].includes(nodeRecord(selection._document, id)?.type);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function nativeValue(value: unknown): unknown {
|
|
65
|
+
const seen = new WeakSet<object>();
|
|
66
|
+
const json = JSON.stringify(value, (_key, value: unknown) => {
|
|
67
|
+
if (typeof value === 'function' || typeof value === 'symbol' || typeof value === 'bigint' || value === undefined) return { __opaqueJavaScriptValue: true };
|
|
68
|
+
if (typeof value === 'number' && !Number.isFinite(value)) return { __nativeNumber: String(value) };
|
|
69
|
+
if (value && typeof value === 'object') {
|
|
70
|
+
if (seen.has(value)) return { __opaqueJavaScriptValue: true };
|
|
71
|
+
seen.add(value);
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
});
|
|
75
|
+
return json === undefined ? { __opaqueJavaScriptValue: true } : JSON.parse(json);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function hydrateValue(value: any): any {
|
|
79
|
+
if (value && typeof value === 'object') {
|
|
80
|
+
if (Object.keys(value).length === 1 && typeof value.__nativeNumber === 'string') return Number(value.__nativeNumber);
|
|
81
|
+
for (const key of Object.keys(value)) value[key] = hydrateValue(value[key]);
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function dataObject(selection: Selection, id: number): Record<string, unknown> {
|
|
87
|
+
// Routed documents share a stable handle space. Moving a native node between
|
|
88
|
+
// documents therefore preserves data identities and callback references.
|
|
89
|
+
const document = selection._document as DataDocument;
|
|
90
|
+
const cacheKey = document.nodeSpace ?? document;
|
|
91
|
+
let documents = dataObjects.get(cacheKey);
|
|
92
|
+
if (!documents) dataObjects.set(cacheKey, documents = new Map());
|
|
93
|
+
const existingReference = documents.get(id);
|
|
94
|
+
let data = existingReference?.deref() ?? document.nodeData?.(id);
|
|
95
|
+
if (!data) {
|
|
96
|
+
const documentReference = new WeakRef(document);
|
|
97
|
+
const dispatch = document.nodeDataOperation?.(id) ?? ((command: string, args: unknown) => {
|
|
98
|
+
const document = documentReference.deref();
|
|
99
|
+
return document ? JSON.parse(document.op(command, JSON.stringify(args))) : undefined;
|
|
100
|
+
});
|
|
101
|
+
const target: Record<string, unknown> = {};
|
|
102
|
+
data = new Proxy(target, {
|
|
103
|
+
set(object, name, value) {
|
|
104
|
+
Reflect.set(object, name, value);
|
|
105
|
+
if (typeof name === 'string') dispatch('data', { ids: [id], name, value: nativeValue(value) });
|
|
106
|
+
return true;
|
|
107
|
+
},
|
|
108
|
+
defineProperty(object, name, descriptor) {
|
|
109
|
+
Reflect.defineProperty(object, name, descriptor);
|
|
110
|
+
if (typeof name === 'string' && 'value' in descriptor) dispatch('data', { ids: [id], name, value: nativeValue(descriptor.value) });
|
|
111
|
+
return true;
|
|
112
|
+
},
|
|
113
|
+
deleteProperty(object, name) {
|
|
114
|
+
Reflect.deleteProperty(object, name);
|
|
115
|
+
if (typeof name === 'string') dispatch('removeData', { ids: [id], name });
|
|
116
|
+
return true;
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
const node = getNode(selection._document, id);
|
|
120
|
+
// JS data can contain callbacks, cycles, and object identities. The DOM and
|
|
121
|
+
// parsed data values live in Rust; this handle preserves those JS values.
|
|
122
|
+
Object.defineProperty(node, 'data', { configurable: true, enumerable: true, get: () => data, set: (value: Record<string, unknown>) => { for (const key of Object.keys(data!)) delete data![key]; for (const key of Object.keys(value ?? {})) data![key] = value[key]; } });
|
|
123
|
+
}
|
|
124
|
+
if (existingReference?.deref() !== data) {
|
|
125
|
+
const reference = new WeakRef(data);
|
|
126
|
+
documents.set(id, reference);
|
|
127
|
+
dataFinalizer.register(data, { cache: new WeakRef(documents), id, reference });
|
|
128
|
+
}
|
|
129
|
+
document.retainNodeData?.(id, data);
|
|
130
|
+
return data;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function property(selection: Selection, id: number, name: string, raw = false): any {
|
|
134
|
+
const result = selection.op('prop', { ids: [id], name, ...(raw ? { raw: true } : {}) });
|
|
135
|
+
if (name === 'sourceCodeLocation') return hydrateLocation(result);
|
|
136
|
+
return result?.__nativeNodeProperty ? (getNode(selection._document, id) as any)[result.__nativeNodeProperty] : result === null ? undefined : result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function propertyValue(selection: Selection, name: string, value: any): any {
|
|
140
|
+
if (name === 'children' || name === 'childNodes') return importNodes(selection._document, value);
|
|
141
|
+
if (['parent', 'parentNode', 'prev', 'previousSibling', 'next', 'nextSibling'].includes(name)) return value == null ? null : importNodes(selection._document, [value])[0] ?? null;
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function attr(this: Selection, name?: any, value?: any): any {
|
|
146
|
+
if (typeof name === 'object' || value !== undefined) {
|
|
147
|
+
if (typeof value === 'function') {
|
|
148
|
+
if (typeof name !== 'string') throw new Error('Bad combination of arguments.');
|
|
149
|
+
this.ids.forEach((id, index) => {
|
|
150
|
+
if (!isElement(this, id)) return;
|
|
151
|
+
const node = getNode(this._document, id) as Element;
|
|
152
|
+
const current = node.attribs[name];
|
|
153
|
+
const next = value.call(node, index, current);
|
|
154
|
+
this.op('attr', { ids: [id], name, value: next === null ? null : String(next) });
|
|
155
|
+
});
|
|
156
|
+
} else if (typeof name === 'object') {
|
|
157
|
+
for (const key of Object.keys(name)) this.op('attr', { ids: this.ids, name: key, value: name[key] === null ? null : String(name[key]) });
|
|
158
|
+
} else {
|
|
159
|
+
this.op('attr', { ids: this.ids, name, value: value === null ? null : String(value) });
|
|
160
|
+
}
|
|
161
|
+
return this;
|
|
162
|
+
}
|
|
163
|
+
if (arguments.length > 1) return this;
|
|
164
|
+
const result = this.op('attr', { ids: this.ids, ...(typeof name === 'string' ? { name } : {}) });
|
|
165
|
+
if (result === null) return undefined;
|
|
166
|
+
return !name && this.ids.length ? (getNode(this._document, this.ids[0]) as Element).attribs : result;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function prop(this: Selection, name: any, value?: any): any {
|
|
170
|
+
if (typeof name === 'string' && value === undefined) return this.ids.length ? property(this, this.ids[0], name) : undefined;
|
|
171
|
+
if (typeof name === 'object' || value !== undefined) {
|
|
172
|
+
if (typeof value === 'function') {
|
|
173
|
+
if (typeof name === 'object') throw new TypeError('Bad combination of arguments.');
|
|
174
|
+
this.ids.forEach((id, index) => {
|
|
175
|
+
if (!isElement(this, id)) return;
|
|
176
|
+
const next = value.call(getNode(this._document, id), index, property(this, id, name, true));
|
|
177
|
+
this.op('prop', { ids: [id], name, value: propertyValue(this, name, next) });
|
|
178
|
+
});
|
|
179
|
+
} else if (typeof name === 'object') {
|
|
180
|
+
for (const key of Object.keys(name)) this.op('prop', { ids: this.ids, name: key, value: propertyValue(this, key, name[key]) });
|
|
181
|
+
} else {
|
|
182
|
+
this.op('prop', { ids: this.ids, name, value: propertyValue(this, name, value) });
|
|
183
|
+
}
|
|
184
|
+
return this;
|
|
185
|
+
}
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function data(this: Selection, name?: any, value?: unknown): any {
|
|
190
|
+
const first = this.ids[0];
|
|
191
|
+
if (first === undefined || !isElement(this, first)) return undefined;
|
|
192
|
+
if (name != null && (typeof name === 'object' || value !== undefined)) {
|
|
193
|
+
for (const id of this.ids) {
|
|
194
|
+
if (!isElement(this, id)) continue;
|
|
195
|
+
const record = dataObject(this, id);
|
|
196
|
+
if (typeof name === 'object') for (const key of Object.keys(name)) record[key] = name[key];
|
|
197
|
+
else if (typeof name === 'string') record[name] = value;
|
|
198
|
+
}
|
|
199
|
+
return this;
|
|
200
|
+
}
|
|
201
|
+
const record = dataObject(this, first);
|
|
202
|
+
if (typeof name === 'string' && Object.hasOwn(record, name)) return record[name];
|
|
203
|
+
const found = this.op<{ found: boolean; value?: any }>('data', { ids: [first], ...(name == null ? {} : { name }) });
|
|
204
|
+
if (!found.found) return undefined;
|
|
205
|
+
if (name == null) {
|
|
206
|
+
for (const key of Object.keys(found.value)) if (!Object.hasOwn(record, key)) record[key] = hydrateValue(found.value[key]);
|
|
207
|
+
return record;
|
|
208
|
+
}
|
|
209
|
+
record[name] = hydrateValue(found.value);
|
|
210
|
+
return record[name];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function css(this: Selection, name?: any, value?: any): any {
|
|
214
|
+
if (name != null && value != null || typeof name === 'object' && !Array.isArray(name)) {
|
|
215
|
+
if (typeof name === 'object') {
|
|
216
|
+
for (const [propertyIndex, key] of Object.keys(name).entries()) {
|
|
217
|
+
if (typeof name[key] === 'function') {
|
|
218
|
+
for (const id of this.ids) {
|
|
219
|
+
if (!isElement(this, id)) continue;
|
|
220
|
+
const current = this.op('css', { ids: [id], name: key }) ?? undefined;
|
|
221
|
+
const next = name[key].call(getNode(this._document, id), propertyIndex, current);
|
|
222
|
+
this.op('css', { ids: [id], name: key, value: next == null ? null : String(next) });
|
|
223
|
+
}
|
|
224
|
+
} else this.op('css', { ids: this.ids, name: key, value: name[key] == null ? null : String(name[key]) });
|
|
225
|
+
}
|
|
226
|
+
} else if (typeof name === 'string' && typeof value === 'function') {
|
|
227
|
+
this.ids.forEach((id, index) => {
|
|
228
|
+
if (!isElement(this, id)) return;
|
|
229
|
+
const current = this.op('css', { ids: [id], name }) ?? undefined;
|
|
230
|
+
const next = value.call(getNode(this._document, id), index, current);
|
|
231
|
+
this.op('css', { ids: [id], name, value: next == null ? null : String(next) });
|
|
232
|
+
});
|
|
233
|
+
} else if (typeof name === 'string') this.op('css', { ids: this.ids, name, value: value == null ? null : String(value) });
|
|
234
|
+
return this;
|
|
235
|
+
}
|
|
236
|
+
return this.op('css', { ids: this.ids, ...(name === undefined ? {} : { name }) }) ?? undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function classMethod(command: 'addClass' | 'removeClass' | 'toggleClass') {
|
|
240
|
+
return function(this: Selection, name?: any, state?: boolean): Selection {
|
|
241
|
+
if (typeof name === 'function') {
|
|
242
|
+
this.ids.forEach((id, index) => {
|
|
243
|
+
if (!isElement(this, id)) return;
|
|
244
|
+
const node = getNode(this._document, id) as Element;
|
|
245
|
+
const next = command === 'toggleClass'
|
|
246
|
+
? name.call(node, index, node.attribs.class ?? '', state)
|
|
247
|
+
: name.call(node, index, node.attribs.class ?? '');
|
|
248
|
+
if (typeof next === 'string') this.op(command, { ids: [id], name: next, ...(typeof state === 'boolean' ? { state } : {}) });
|
|
249
|
+
});
|
|
250
|
+
} else if (typeof name === 'string' || command === 'removeClass' && arguments.length === 0) {
|
|
251
|
+
this.op(command, { ids: this.ids, ...(typeof name === 'string' ? { name } : { all: true }), ...(typeof state === 'boolean' ? { state } : {}) });
|
|
252
|
+
}
|
|
253
|
+
return this;
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function installAttributes(CheerioClass: { prototype: object }): void {
|
|
258
|
+
Object.assign(CheerioClass.prototype, {
|
|
259
|
+
attr, prop, data, css,
|
|
260
|
+
removeAttr(this: Selection, name: string) { this.op('removeAttr', { ids: this.ids, name }); return this; },
|
|
261
|
+
hasClass(this: Selection, name: string) { return this.op<boolean>('hasClass', { ids: this.ids, name }); },
|
|
262
|
+
addClass: classMethod('addClass'), removeClass: classMethod('removeClass'), toggleClass: classMethod('toggleClass'),
|
|
263
|
+
val(this: Selection, value?: unknown) {
|
|
264
|
+
if (arguments.length === 0) return this.op('val', { ids: this.ids }) ?? undefined;
|
|
265
|
+
const result = this.op('val', { ids: this.ids, value: value === undefined ? { __undefinedJavaScriptValue: true } : value });
|
|
266
|
+
return result?.chain === false ? result.returnValue : result === false ? undefined : this;
|
|
267
|
+
},
|
|
268
|
+
serializeArray(this: Selection) { return this.op<SerializedField[]>('serializeArray', { ids: this.ids }); },
|
|
269
|
+
serialize(this: Selection) { return this.op<string>('serialize', { ids: this.ids }); },
|
|
270
|
+
});
|
|
271
|
+
}
|
package/src/codec.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node strings use UTF-16, including unpaired surrogates. Rust strings use
|
|
3
|
+
* Unicode scalar values. Reserve a small private-use alphabet on this private
|
|
4
|
+
* boundary so neither representation loses a code unit.
|
|
5
|
+
*/
|
|
6
|
+
const reserved = /[\ud800-\ue800]/;
|
|
7
|
+
const encoded = /[\ue000-\ue800]/;
|
|
8
|
+
const escapeUnit = 0xe800;
|
|
9
|
+
const isWellFormed = (String.prototype as String & { isWellFormed?(): boolean }).isWellFormed;
|
|
10
|
+
|
|
11
|
+
export function encodeString(value: string): string {
|
|
12
|
+
if (!encoded.test(value) && (isWellFormed ? isWellFormed.call(value) : !reserved.test(value))) return value;
|
|
13
|
+
const parts: string[] = [];
|
|
14
|
+
let start = 0;
|
|
15
|
+
for (let index = 0; index < value.length; index++) {
|
|
16
|
+
const unit = value.charCodeAt(index);
|
|
17
|
+
if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
18
|
+
const next = value.charCodeAt(index + 1);
|
|
19
|
+
if (next >= 0xdc00 && next <= 0xdfff) { index++; continue; }
|
|
20
|
+
}
|
|
21
|
+
let replacement: string | undefined;
|
|
22
|
+
if (unit >= 0xd800 && unit <= 0xdfff) replacement = String.fromCharCode(0xe000 + unit - 0xd800);
|
|
23
|
+
else if (unit >= 0xe000 && unit <= escapeUnit) replacement = '\ue800' + value[index];
|
|
24
|
+
if (replacement !== undefined) {
|
|
25
|
+
parts.push(value.slice(start, index), replacement);
|
|
26
|
+
start = index + 1;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return start === 0 ? value : parts.join('') + value.slice(start);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function decodeString(value: string): string {
|
|
33
|
+
if (!encoded.test(value)) return value;
|
|
34
|
+
const parts: string[] = [];
|
|
35
|
+
let start = 0;
|
|
36
|
+
for (let index = 0; index < value.length; index++) {
|
|
37
|
+
const unit = value.charCodeAt(index);
|
|
38
|
+
if (unit < 0xe000 || unit > escapeUnit) continue;
|
|
39
|
+
const next = value.charCodeAt(index + 1);
|
|
40
|
+
if (unit === escapeUnit) {
|
|
41
|
+
if (next < 0xe000 || next > escapeUnit || Number.isNaN(next)) continue;
|
|
42
|
+
parts.push(value.slice(start, index), value[++index]);
|
|
43
|
+
} else parts.push(value.slice(start, index), String.fromCharCode(0xd800 + unit - 0xe000));
|
|
44
|
+
start = index + 1;
|
|
45
|
+
}
|
|
46
|
+
return start === 0 ? value : parts.join('') + value.slice(start);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Transform JSON string tokens rather than the whole JSON text: JSON.stringify
|
|
50
|
+
// escapes lone surrogates, while a literal backslash-u sequence is plain data.
|
|
51
|
+
const stringToken = /"(?:[^"\\]|\\[\s\S])*"/g;
|
|
52
|
+
const encodeJsonCandidate = /[\ud800-\ue800]|\\u(?:d[89a-f][\da-f]{2}|e[0-8][\da-f]{2})/i;
|
|
53
|
+
const escapedJsonCandidate = /\\u(?:d[89a-f][\da-f]{2}|e[0-8][\da-f]{2})/i;
|
|
54
|
+
const decodeJsonCandidate = /[\ue000-\ue800]|\\ue[0-8][\da-f]{2}/i;
|
|
55
|
+
|
|
56
|
+
export function encodeJson(value: string): string {
|
|
57
|
+
if (!encodeJsonCandidate.test(value)) return value;
|
|
58
|
+
if (isWellFormed?.call(value) && !encoded.test(value) && !escapedJsonCandidate.test(value)) return value;
|
|
59
|
+
return value.replace(stringToken, (token) => JSON.stringify(encodeString(JSON.parse(token))));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function decodeJson(value: string): string {
|
|
63
|
+
return decodeJsonCandidate.test(value) ? value.replace(stringToken, (token) => JSON.stringify(decodeString(JSON.parse(token)))) : value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Private parser protocol flag, appended without deserializing large options. */
|
|
67
|
+
export function encodeOptions(value?: string): string {
|
|
68
|
+
if (value === undefined) return '{"__utf16Codec":true}';
|
|
69
|
+
const json = encodeJson(value);
|
|
70
|
+
if (!json.trimStart().startsWith('{')) return '{"__utf16Codec":true}';
|
|
71
|
+
const end = json.lastIndexOf('}');
|
|
72
|
+
const prefix = json.slice(0, end);
|
|
73
|
+
return prefix + (prefix.trimEnd().endsWith('{') ? '' : ',') + '"__utf16Codec":true' + json.slice(end);
|
|
74
|
+
}
|
package/src/dom.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { requireNative } from './native.js';
|
|
2
|
+
import { getNode, importNodes, nodeBinding, operation, cloneNode } from './nodes.js';
|
|
3
|
+
import { routeDocument } from './router.js';
|
|
4
|
+
import type * as Shape from './types.js';
|
|
5
|
+
|
|
6
|
+
export const ElementType = Object.freeze({ Root: 'root', Text: 'text', Directive: 'directive', Comment: 'comment', Script: 'script', Style: 'style', Tag: 'tag', CDATA: 'cdata', Doctype: 'doctype' } as typeof Shape.ElementType);
|
|
7
|
+
|
|
8
|
+
function initialize<T extends Node>(node: T, type: string, fields: Record<string, unknown>, children: readonly Shape.AnyNode[] = []): T {
|
|
9
|
+
const document = routeDocument(new (requireNative().NativeDocument)('', false));
|
|
10
|
+
const id = operation<number>(document, 'createNode', { type, ...fields });
|
|
11
|
+
if (children.length) operation(document, 'setNode', { id, field: 'children', value: importNodes(document, children) });
|
|
12
|
+
return getNode(document, id, node) as unknown as T;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** DOM constructors backed by native storage, also shared across ESM/CJS views. */
|
|
16
|
+
export abstract class Node {
|
|
17
|
+
static [Symbol.hasInstance](value: unknown): boolean {
|
|
18
|
+
const binding = nodeBinding(value);
|
|
19
|
+
if (!binding) return Function.prototype[Symbol.hasInstance].call(this, value);
|
|
20
|
+
const type = (value as Shape.AnyNode).type;
|
|
21
|
+
switch (this.name) {
|
|
22
|
+
case 'Node': return true;
|
|
23
|
+
case 'DataNode': return ['text', 'comment', 'directive'].includes(type);
|
|
24
|
+
case 'NodeWithChildren': return ['root', 'tag', 'script', 'style', 'cdata'].includes(type);
|
|
25
|
+
case 'Element': return ['tag', 'script', 'style'].includes(type);
|
|
26
|
+
case 'Document': return type === 'root';
|
|
27
|
+
case 'CDATA': return type === 'cdata';
|
|
28
|
+
case 'Text': return type === 'text';
|
|
29
|
+
case 'Comment': return type === 'comment';
|
|
30
|
+
case 'ProcessingInstruction': return type === 'directive';
|
|
31
|
+
default: return Function.prototype[Symbol.hasInstance].call(this, value);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export interface Node extends Shape.Node {}
|
|
36
|
+
export abstract class DataNode extends Node {}
|
|
37
|
+
export interface DataNode extends Shape.DataNode {}
|
|
38
|
+
export abstract class NodeWithChildren extends Node {}
|
|
39
|
+
export interface NodeWithChildren extends Shape.NodeWithChildren {}
|
|
40
|
+
export class Text extends DataNode { declare readonly nodeType: 3; declare type: Shape.Text['type']; constructor(data: string) { super(); return initialize(this, 'text', { data }); } }
|
|
41
|
+
export interface Text extends Shape.Text {}
|
|
42
|
+
export class Comment extends DataNode { declare readonly nodeType: 8; declare type: Shape.Comment['type']; constructor(data: string) { super(); return initialize(this, 'comment', { data }); } }
|
|
43
|
+
export interface Comment extends Shape.Comment {}
|
|
44
|
+
export class ProcessingInstruction extends DataNode { declare readonly nodeType: 1; declare type: Shape.ProcessingInstruction['type']; constructor(name: string, data: string) { super(); return initialize(this, 'directive', { name, data }); } }
|
|
45
|
+
export interface ProcessingInstruction extends Shape.ProcessingInstruction {}
|
|
46
|
+
export class Document extends NodeWithChildren { declare readonly nodeType: 9; declare type: Shape.Document['type']; constructor(children: Shape.ChildNode[]) { super(); return initialize(this, 'root', {}, children); } }
|
|
47
|
+
export interface Document extends Shape.Document {}
|
|
48
|
+
export class CDATA extends NodeWithChildren { declare readonly nodeType: 4; declare type: Shape.CDATA['type']; constructor(children: Shape.ChildNode[]) { super(); return initialize(this, 'cdata', {}, children); } }
|
|
49
|
+
export interface CDATA extends Shape.CDATA {}
|
|
50
|
+
export class Element extends NodeWithChildren { declare readonly nodeType: 1; declare type: Shape.Element['type']; declare sourceCodeLocation?: Shape.TagSourceCodeLocation | null;
|
|
51
|
+
constructor(name: string, attribs: Record<string, string>, children: Shape.ChildNode[] = [], type?: Shape.Element['type']) {
|
|
52
|
+
super(); return initialize(this, type ?? (name === 'script' || name === 'style' ? name : 'tag'), { name, attribs }, children);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export interface Element extends Shape.Element {}
|
|
56
|
+
|
|
57
|
+
/** @internal */
|
|
58
|
+
export function nodePrototype(type: string): object {
|
|
59
|
+
return ({ root: Document, text: Text, comment: Comment, directive: ProcessingInstruction, cdata: CDATA, tag: Element, script: Element, style: Element }[type] ?? Node).prototype;
|
|
60
|
+
}
|
|
61
|
+
export const isTag = (node: Shape.Node): node is Shape.Element => ['tag', 'script', 'style'].includes(node.type);
|
|
62
|
+
export const isText = (node: Shape.Node): node is Shape.Text => node.type === 'text';
|
|
63
|
+
export const isComment = (node: Shape.Node): node is Shape.Comment => node.type === 'comment';
|
|
64
|
+
export const isDirective = (node: Shape.Node): node is Shape.ProcessingInstruction => node.type === 'directive';
|
|
65
|
+
export const isDocument = (node: Shape.Node): node is Shape.Document => node.type === 'root';
|
|
66
|
+
export const isCDATA = (node: Shape.Node): node is Shape.CDATA => node.type === 'cdata';
|
|
67
|
+
export const hasChildren = (node: Shape.Node): node is Shape.NodeWithChildren => 'children' in node;
|
|
68
|
+
|
|
69
|
+
// DOM aliases live on their class prototypes, as they do in domhandler. Native
|
|
70
|
+
// views intercept the underlying fields; external supplied nodes keep theirs.
|
|
71
|
+
Object.defineProperties(Node.prototype, {
|
|
72
|
+
cloneNode: { configurable: true, writable: true, value: cloneNode },
|
|
73
|
+
parentNode: { configurable: true, get(this: Shape.Node) { return this.parent; }, set(this: Shape.Node, value: Shape.ParentNode | null) { this.parent = value; } },
|
|
74
|
+
previousSibling: { configurable: true, get(this: Shape.Node) { return this.prev; }, set(this: Shape.Node, value: Shape.ChildNode | null) { this.prev = value; } },
|
|
75
|
+
nextSibling: { configurable: true, get(this: Shape.Node) { return this.next; }, set(this: Shape.Node, value: Shape.ChildNode | null) { this.next = value; } },
|
|
76
|
+
nodeType: { configurable: true, get(this: Shape.Node) { return ({ root: 9, text: 3, comment: 8, cdata: 4 } as Record<string, number>)[this.type] ?? 1; } },
|
|
77
|
+
});
|
|
78
|
+
Object.defineProperty(DataNode.prototype, 'nodeValue', { configurable: true, get(this: Shape.DataNode) { return this.data; }, set(this: Shape.DataNode, value: string) { this.data = value; } });
|
|
79
|
+
Object.defineProperties(NodeWithChildren.prototype, {
|
|
80
|
+
childNodes: { configurable: true, get(this: Shape.NodeWithChildren) { return this.children; }, set(this: Shape.NodeWithChildren, value: Shape.ChildNode[]) { this.children = value; } },
|
|
81
|
+
firstChild: { configurable: true, get(this: Shape.NodeWithChildren) { return this.children[0] ?? null; } },
|
|
82
|
+
lastChild: { configurable: true, get(this: Shape.NodeWithChildren) { return this.children[this.children.length - 1] ?? null; } },
|
|
83
|
+
});
|
|
84
|
+
Object.defineProperties(Element.prototype, {
|
|
85
|
+
tagName: { configurable: true, get(this: Shape.Element) { return this.name; }, set(this: Shape.Element, value: string) { this.name = value; } },
|
|
86
|
+
attributes: { configurable: true, get(this: Shape.Element) { return Object.keys(this.attribs).map((name) => ({ name, value: this.attribs[name], namespace: this['x-attribsNamespace']?.[name], prefix: this['x-attribsPrefix']?.[name] })); } },
|
|
87
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { CheerioOptions, ExtractMap } from './types.js';
|
|
2
|
+
import { types } from 'node:util';
|
|
3
|
+
|
|
4
|
+
export interface ExtractPlanEntry {
|
|
5
|
+
key: string;
|
|
6
|
+
selector: string;
|
|
7
|
+
all: boolean;
|
|
8
|
+
value: { kind: 'property'; name: string } | { kind: 'map'; entries: ExtractPlanEntry[] };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ExtractPlan { version: 1; entries: ExtractPlanEntry[] }
|
|
12
|
+
|
|
13
|
+
// Properties that can be returned as ordinary values. Properties returning DOM
|
|
14
|
+
// views stay on the regular API so their identities and document leases survive.
|
|
15
|
+
const scalarProperties = new Set([
|
|
16
|
+
'textContent', 'innerText', 'innerHTML', 'outerHTML', 'tagName', 'nodeName',
|
|
17
|
+
'nodeType', 'id', 'class', 'className', 'title', 'href', 'src', 'alt', 'name',
|
|
18
|
+
'value', 'type', 'checked', 'selected', 'disabled', 'multiple', 'readOnly',
|
|
19
|
+
'required', 'hidden', 'open', 'autofocus', 'controls', 'loop', 'muted',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function dataValue(object: object, key: string): { value: unknown } | undefined {
|
|
23
|
+
for (let current: object | null = object; current; current = Object.getPrototypeOf(current)) {
|
|
24
|
+
if (types.isProxy(current)) return undefined;
|
|
25
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
26
|
+
if (descriptor) return 'value' in descriptor ? { value: descriptor.value } : undefined;
|
|
27
|
+
}
|
|
28
|
+
return { value: undefined };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Compile pure extraction maps. User callbacks and accessors keep JS semantics. */
|
|
32
|
+
export function compileExtractPlan(map: ExtractMap, options: CheerioOptions): ExtractPlan | undefined {
|
|
33
|
+
if (options.pseudos || options.treeAdapter || options.Tokenizer || options.xml && typeof options.xml === 'object' && options.xml.Tokenizer) return undefined;
|
|
34
|
+
const ancestors = new Set<object>();
|
|
35
|
+
const compile = (input: unknown): ExtractPlanEntry[] | undefined => {
|
|
36
|
+
if (!input || typeof input !== 'object' || types.isProxy(input) || ancestors.has(input) || ancestors.size >= 64) return undefined;
|
|
37
|
+
for (let current = Object.getPrototypeOf(input); current; current = Object.getPrototypeOf(current)) {
|
|
38
|
+
if (types.isProxy(current)) return undefined;
|
|
39
|
+
}
|
|
40
|
+
ancestors.add(input);
|
|
41
|
+
try {
|
|
42
|
+
const entries: ExtractPlanEntry[] = [];
|
|
43
|
+
for (const key in input) {
|
|
44
|
+
// Cheerio uses an ordinary result object. Its legacy __proto__ setter
|
|
45
|
+
// semantics must execute through the JavaScript path.
|
|
46
|
+
if (key === '__proto__') return undefined;
|
|
47
|
+
const inherited = Object.getOwnPropertyDescriptor(Object.prototype, key);
|
|
48
|
+
if (inherited && (!('value' in inherited) || !inherited.writable)) return undefined;
|
|
49
|
+
const slot = dataValue(input, key);
|
|
50
|
+
if (!slot) return undefined;
|
|
51
|
+
const all = Array.isArray(slot.value);
|
|
52
|
+
if (slot.value && typeof slot.value === 'object' && types.isProxy(slot.value)) return undefined;
|
|
53
|
+
const first = all ? dataValue(slot.value as object, '0') : slot;
|
|
54
|
+
if (!first) return undefined;
|
|
55
|
+
const descriptor = first.value;
|
|
56
|
+
let selector: unknown;
|
|
57
|
+
let value: unknown;
|
|
58
|
+
if (typeof descriptor === 'string') { selector = descriptor; value = 'textContent'; }
|
|
59
|
+
else if (descriptor && typeof descriptor === 'object') {
|
|
60
|
+
if (types.isProxy(descriptor)) return undefined;
|
|
61
|
+
const selectorSlot = dataValue(descriptor, 'selector');
|
|
62
|
+
const valueSlot = dataValue(descriptor, 'value');
|
|
63
|
+
if (!selectorSlot || !valueSlot) return undefined;
|
|
64
|
+
selector = selectorSlot.value;
|
|
65
|
+
value = valueSlot.value ?? 'textContent';
|
|
66
|
+
} else return undefined;
|
|
67
|
+
if (typeof selector !== 'string') return undefined;
|
|
68
|
+
if (typeof value === 'string') {
|
|
69
|
+
if (!scalarProperties.has(value) && !/^(?:data-|aria-).+/.test(value)) return undefined;
|
|
70
|
+
entries.push({ key, selector, all, value: { kind: 'property', name: value } });
|
|
71
|
+
} else {
|
|
72
|
+
const nested = compile(value);
|
|
73
|
+
if (!nested) return undefined;
|
|
74
|
+
entries.push({ key, selector, all, value: { kind: 'map', entries: nested } });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return entries;
|
|
78
|
+
} finally { ancestors.delete(input); }
|
|
79
|
+
};
|
|
80
|
+
const entries = compile(map);
|
|
81
|
+
return entries && { version: 1, entries };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** JSON omits undefined fields; restore them recursively without changing order. */
|
|
85
|
+
export function hydrateExtractResult(result: Record<string, unknown>, entries: ExtractPlanEntry[]): void {
|
|
86
|
+
const values = entries.map((entry) => Object.hasOwn(result, entry.key) ? result[entry.key] : entry.all ? [] : undefined);
|
|
87
|
+
for (const entry of entries) delete result[entry.key];
|
|
88
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
89
|
+
const entry = entries[index];
|
|
90
|
+
Object.defineProperty(result, entry.key, { value: values[index], enumerable: true, configurable: true, writable: true });
|
|
91
|
+
if (entry.value.kind !== 'map') continue;
|
|
92
|
+
const nested = result[entry.key];
|
|
93
|
+
for (const value of entry.all && Array.isArray(nested) ? nested : [nested]) {
|
|
94
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) hydrateExtractResult(value as Record<string, unknown>, entry.value.entries);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|