crawlforge-extractors 1.9.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -10
- package/index.d.ts +35 -3
- package/index.js +2 -0
- package/package.json +4 -2
- package/src/blockedPage.js +22 -0
- package/src/carriers.js +374 -0
- package/src/embeddedState.js +161 -98
- package/src/flight.js +412 -0
- package/src/frameworkData.js +323 -0
- package/src/jsLiteral.js +266 -0
- package/src/jsonFind.js +150 -0
package/src/jsonFind.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jsonFind.js — find where a key lives in a parsed payload.
|
|
3
|
+
*
|
|
4
|
+
* The companion to jsonPath.js: a caller who knows a field's name but not its
|
|
5
|
+
* position in a multi-megabyte state blob asks for the key, gets back every
|
|
6
|
+
* path that holds it plus a short preview of each value, and then selects the
|
|
7
|
+
* one it wants with `path`. The paths are written in exactly the syntax
|
|
8
|
+
* selectJsonPath reads, so a match round-trips.
|
|
9
|
+
*
|
|
10
|
+
* The payload is page content, so the walk is iterative (depth is free),
|
|
11
|
+
* visits each object once (cycles and shared subtrees cannot loop or fan out),
|
|
12
|
+
* and stops after a fixed number of nodes.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const MAX_NODES = 5_000_000;
|
|
16
|
+
|
|
17
|
+
const scalarJson = (value) => {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.stringify(value) ?? '';
|
|
20
|
+
} catch {
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const skipped = (value) => value === undefined || typeof value === 'function' || typeof value === 'symbol';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The first `max` characters of JSON.stringify(value), without serializing
|
|
29
|
+
* the rest — the value may be a large subtree.
|
|
30
|
+
* @param {unknown} value
|
|
31
|
+
* @param {number} max
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
function previewJson(value, max) {
|
|
35
|
+
let out = '';
|
|
36
|
+
const frames = [];
|
|
37
|
+
const emit = (item) => {
|
|
38
|
+
if (item === null || typeof item !== 'object') {
|
|
39
|
+
// Each character of a string serializes to at least one, so the first
|
|
40
|
+
// max - out.length characters are enough for the visible prefix.
|
|
41
|
+
out += scalarJson(typeof item === 'string' ? item.slice(0, Math.max(0, max - out.length)) : item);
|
|
42
|
+
} else if (Array.isArray(item)) {
|
|
43
|
+
out += '[';
|
|
44
|
+
frames.push({ value: item, keys: null, next: 0, written: 0 });
|
|
45
|
+
} else {
|
|
46
|
+
out += '{';
|
|
47
|
+
frames.push({ value: item, keys: Object.keys(item), next: 0, written: 0 });
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
emit(value);
|
|
52
|
+
while (frames.length > 0 && out.length < max) {
|
|
53
|
+
const frame = frames[frames.length - 1];
|
|
54
|
+
if (frame.keys === null) {
|
|
55
|
+
if (frame.next >= frame.value.length) {
|
|
56
|
+
out += ']';
|
|
57
|
+
frames.pop();
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (frame.next > 0) out += ',';
|
|
61
|
+
const item = frame.value[frame.next++];
|
|
62
|
+
emit(skipped(item) ? null : item);
|
|
63
|
+
} else {
|
|
64
|
+
if (frame.next >= frame.keys.length) {
|
|
65
|
+
out += '}';
|
|
66
|
+
frames.pop();
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const key = frame.keys[frame.next++];
|
|
70
|
+
const item = frame.value[key];
|
|
71
|
+
if (skipped(item)) continue;
|
|
72
|
+
if (frame.written++ > 0) out += ',';
|
|
73
|
+
out += `${JSON.stringify(key)}:`;
|
|
74
|
+
emit(item);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out.slice(0, max);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// A segment selectJsonPath can read back in dot form: "." splits a path and
|
|
81
|
+
// "[" opens a bracket, and an empty segment is dropped.
|
|
82
|
+
const addressableKey = (key) => key !== '' && !key.includes('.') && !key.includes('[');
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Every property named `key` (case-insensitively), anywhere under `root`, in
|
|
86
|
+
* document order.
|
|
87
|
+
*
|
|
88
|
+
* `path` is dotted (`next_f.10.1.2.3.providerModel`) and resolves with
|
|
89
|
+
* selectJsonPath. A match whose path runs through a key that syntax cannot
|
|
90
|
+
* spell (one containing "." or "[", or an empty key) is still reported, with
|
|
91
|
+
* `addressable: false`. An object reached twice (a shared or cyclic value) is
|
|
92
|
+
* walked once, at its first path.
|
|
93
|
+
*
|
|
94
|
+
* @param {unknown} root
|
|
95
|
+
* @param {string} key
|
|
96
|
+
* @param {{ limit?: number, previewChars?: number, maxNodes?: number }} [options]
|
|
97
|
+
* @returns {{ matches: Array<{ path: string, preview: string, addressable?: false }>, total: number, truncated: boolean }}
|
|
98
|
+
*/
|
|
99
|
+
export function findJsonPaths(root, key, { limit = 50, previewChars = 200, maxNodes = MAX_NODES } = {}) {
|
|
100
|
+
const matches = [];
|
|
101
|
+
let total = 0;
|
|
102
|
+
let truncated = false;
|
|
103
|
+
if (typeof key !== 'string' || root === null || typeof root !== 'object') {
|
|
104
|
+
return { matches, total, truncated };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const wanted = key.toLowerCase();
|
|
108
|
+
const seen = new WeakSet();
|
|
109
|
+
// Each entry is one node; `up` links to its parent entry, so a path is only
|
|
110
|
+
// spelled out for the matches that are reported.
|
|
111
|
+
const stack = [{ value: root, key: null, named: false, up: null, addressable: true }];
|
|
112
|
+
let visited = 0;
|
|
113
|
+
|
|
114
|
+
while (stack.length > 0) {
|
|
115
|
+
if (++visited > maxNodes) {
|
|
116
|
+
truncated = true;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
const entry = stack.pop();
|
|
120
|
+
|
|
121
|
+
if (entry.named && entry.key.toLowerCase() === wanted) {
|
|
122
|
+
total++;
|
|
123
|
+
if (matches.length < limit) {
|
|
124
|
+
const segments = [];
|
|
125
|
+
for (let at = entry; at.up !== null; at = at.up) segments.push(at.key);
|
|
126
|
+
const match = { path: segments.reverse().join('.'), preview: previewJson(entry.value, previewChars) };
|
|
127
|
+
if (!entry.addressable) match.addressable = false;
|
|
128
|
+
matches.push(match);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const { value } = entry;
|
|
133
|
+
if (value === null || typeof value !== 'object' || seen.has(value)) continue;
|
|
134
|
+
seen.add(value);
|
|
135
|
+
const named = !Array.isArray(value);
|
|
136
|
+
const keys = Object.keys(value);
|
|
137
|
+
for (let i = keys.length - 1; i >= 0; i--) {
|
|
138
|
+
stack.push({
|
|
139
|
+
value: value[keys[i]],
|
|
140
|
+
key: keys[i],
|
|
141
|
+
named,
|
|
142
|
+
up: entry,
|
|
143
|
+
addressable: entry.addressable && (!named || addressableKey(keys[i]))
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (total > matches.length) truncated = true;
|
|
149
|
+
return { matches, total, truncated };
|
|
150
|
+
}
|