@linxiraos/pi-utils 1.1.26 → 1.2.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/CHANGELOG.md +6 -4
- package/README.md +1 -1
- package/dist/types/ar/index.d.ts +1 -0
- package/dist/types/ar/zip-package.d.ts +21 -0
- package/dist/types/color.d.ts +2 -0
- package/dist/types/dirs.d.ts +37 -12
- package/dist/types/docx/converter.d.ts +2 -0
- package/dist/types/dom/core.d.ts +15 -4
- package/dist/types/dom/parser.d.ts +5 -3
- package/dist/types/dom/selector.d.ts +7 -1
- package/dist/types/logger/rotating-file.d.ts +4 -2
- package/dist/types/loop-phase.d.ts +12 -7
- package/dist/types/marked/core.d.ts +7 -6
- package/dist/types/marked-list.d.ts +38 -0
- package/dist/types/math-delimiters.d.ts +48 -0
- package/dist/types/procmgr.d.ts +14 -20
- package/dist/types/readability/types.d.ts +2 -0
- package/dist/types/temp.d.ts +7 -0
- package/dist/types/turndown/types.d.ts +7 -0
- package/dist/types/vterm/buffer.d.ts +16 -5
- package/dist/types/vterm/query-responder.d.ts +15 -0
- package/package.json +2 -2
- package/src/ar/index.ts +1 -0
- package/src/ar/open.ts +29 -8
- package/src/ar/zip-package.ts +57 -0
- package/src/chalk.ts +66 -23
- package/src/color.ts +6 -0
- package/src/dirs.ts +93 -24
- package/src/docx/converter.ts +26 -12
- package/src/dom/core.ts +354 -111
- package/src/dom/parser.ts +42 -19
- package/src/dom/selector.ts +328 -95
- package/src/fetch-retry.ts +2 -0
- package/src/format.ts +5 -1
- package/src/json-lexer.ts +54 -0
- package/src/json-parse.ts +21 -6
- package/src/logger/rotating-file.ts +5 -3
- package/src/logger.ts +39 -23
- package/src/loop-phase.ts +66 -21
- package/src/marked/core.ts +45 -19
- package/src/marked-list.ts +73 -0
- package/src/math-delimiters.ts +125 -5
- package/src/module-timer.ts +5 -5
- package/src/postmortem.ts +1 -1
- package/src/process-name.ts +2 -2
- package/src/procmgr.ts +58 -4
- package/src/prompt.ts +3 -1
- package/src/ptree.ts +28 -35
- package/src/readability/readability.ts +349 -39
- package/src/readability/types.ts +2 -0
- package/src/runtime-install.ts +2 -2
- package/src/sqlite.ts +58 -1
- package/src/stream.ts +34 -13
- package/src/temp.ts +13 -0
- package/src/template.ts +7 -4
- package/src/timing-buffer.ts +1 -1
- package/src/tls-fetch.ts +1 -1
- package/src/turndown/gfm.ts +16 -5
- package/src/turndown/html.ts +28 -8
- package/src/turndown/service.ts +43 -15
- package/src/turndown/types.ts +7 -0
- package/src/vterm/buffer.ts +31 -11
- package/src/vterm/query-responder.ts +31 -2
- package/src/vterm/terminal.ts +213 -100
package/src/dom/parser.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { Document, type DocumentFragment, Element, Text } from "./core";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/** Closing-tag patterns for raw-text elements, reused across parses (callers reset `lastIndex`). */
|
|
4
|
+
const RAW_TEXT_CLOSE: Record<string, RegExp> = { script: /<\/script\s*>/gi, style: /<\/style\s*>/gi };
|
|
5
|
+
const DOCTYPE_PATTERN = /<!doctype\b/iy;
|
|
6
|
+
const TAG_NAME_PATTERN = /[^\s/>]+/y;
|
|
7
|
+
const ATTRIBUTE_NAME_PATTERN = /[^\s=/>]+/y;
|
|
8
|
+
const UNQUOTED_VALUE_PATTERN = /[^\s>]+/y;
|
|
9
|
+
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
10
|
+
const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
|
|
11
|
+
|
|
4
12
|
const VOID_ELEMENTS: Record<string, true> = {
|
|
5
13
|
area: true,
|
|
6
14
|
base: true,
|
|
@@ -172,9 +180,12 @@ const NAMED_ENTITIES: Record<string, string> = {
|
|
|
172
180
|
yen: "¥",
|
|
173
181
|
};
|
|
174
182
|
|
|
183
|
+
const ENTITY_PATTERN = /&(#(?:x[0-9a-f]+|\d+)|[a-z][a-z0-9]+);/gi;
|
|
184
|
+
|
|
175
185
|
/** Decode common named entities and all valid numeric character references. */
|
|
176
186
|
export function decodeEntities(value: string): string {
|
|
177
|
-
|
|
187
|
+
if (!value.includes("&")) return value;
|
|
188
|
+
return value.replace(ENTITY_PATTERN, (whole, entity: string) => {
|
|
178
189
|
if (entity[0] !== "#") return NAMED_ENTITIES[entity] ?? NAMED_ENTITIES[entity.toLowerCase()] ?? whole;
|
|
179
190
|
const hexadecimal = entity[1]?.toLowerCase() === "x";
|
|
180
191
|
const codePoint = Number.parseInt(entity.slice(hexadecimal ? 2 : 1), hexadecimal ? 16 : 10);
|
|
@@ -210,7 +221,8 @@ function parseStartTag(
|
|
|
210
221
|
): { name: string; attributes: Array<[string, string]>; selfClosing: boolean } | null {
|
|
211
222
|
let index = 0;
|
|
212
223
|
while (/\s/.test(source[index] ?? "")) index++;
|
|
213
|
-
|
|
224
|
+
TAG_NAME_PATTERN.lastIndex = index;
|
|
225
|
+
const nameMatch = TAG_NAME_PATTERN.exec(source);
|
|
214
226
|
if (!nameMatch) return null;
|
|
215
227
|
const name = nameMatch[0].toLowerCase();
|
|
216
228
|
index += nameMatch[0].length;
|
|
@@ -222,7 +234,8 @@ function parseStartTag(
|
|
|
222
234
|
selfClosing = true;
|
|
223
235
|
break;
|
|
224
236
|
}
|
|
225
|
-
|
|
237
|
+
ATTRIBUTE_NAME_PATTERN.lastIndex = index;
|
|
238
|
+
const attributeMatch = ATTRIBUTE_NAME_PATTERN.exec(source);
|
|
226
239
|
if (!attributeMatch) break;
|
|
227
240
|
const attributeName = attributeMatch[0];
|
|
228
241
|
index += attributeMatch[0].length;
|
|
@@ -243,7 +256,8 @@ function parseStartTag(
|
|
|
243
256
|
index = end + 1;
|
|
244
257
|
}
|
|
245
258
|
} else {
|
|
246
|
-
|
|
259
|
+
UNQUOTED_VALUE_PATTERN.lastIndex = index;
|
|
260
|
+
const valueMatch = UNQUOTED_VALUE_PATTERN.exec(source);
|
|
247
261
|
value = valueMatch?.[0] ?? "";
|
|
248
262
|
index += value.length;
|
|
249
263
|
}
|
|
@@ -266,7 +280,8 @@ function closeOptionalElements(stack: Element[], incoming: string): void {
|
|
|
266
280
|
if (closeable?.includes(current.localName)) stack.pop();
|
|
267
281
|
}
|
|
268
282
|
|
|
269
|
-
|
|
283
|
+
/** Parse markup, appending the resulting nodes to `root` (a document, fragment, or element). */
|
|
284
|
+
export function parseInto(html: string, document: Document, root: Document | DocumentFragment | Element): void {
|
|
270
285
|
const stack: Element[] = [];
|
|
271
286
|
const xmlLike = root instanceof Document && /^\s*(?:<\?xml[\s\S]*?\?>\s*)?<(?:feed|rss)\b/i.test(html);
|
|
272
287
|
let parent: Document | DocumentFragment | Element = root;
|
|
@@ -286,7 +301,7 @@ function parseInto(html: string, document: Document, root: Document | DocumentFr
|
|
|
286
301
|
index = end;
|
|
287
302
|
continue;
|
|
288
303
|
}
|
|
289
|
-
if (
|
|
304
|
+
if (html[index + 1] === "!" && isDoctypeAt(html, index)) {
|
|
290
305
|
const end = html.indexOf(">", index + 2);
|
|
291
306
|
index = end < 0 ? html.length : end + 1;
|
|
292
307
|
continue;
|
|
@@ -325,12 +340,13 @@ function parseInto(html: string, document: Document, root: Document | DocumentFr
|
|
|
325
340
|
}
|
|
326
341
|
closeOptionalElements(stack, parsed.name);
|
|
327
342
|
parent = stack[stack.length - 1] ?? root;
|
|
343
|
+
// The root itself never supplies a namespace: fragments and documents are namespace-less, and
|
|
344
|
+
// element roots (innerHTML) parse their top-level nodes as HTML like a detached fragment would.
|
|
328
345
|
const namespace =
|
|
329
|
-
parent instanceof Element && parent.namespaceURI ===
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
: "http://www.w3.org/1999/xhtml";
|
|
346
|
+
(parent !== root && parent instanceof Element && parent.namespaceURI === SVG_NAMESPACE) ||
|
|
347
|
+
parsed.name === "svg"
|
|
348
|
+
? SVG_NAMESPACE
|
|
349
|
+
: HTML_NAMESPACE;
|
|
334
350
|
const element = document.createElementNS(namespace, parsed.name);
|
|
335
351
|
for (let attributeIndex = parsed.attributes.length - 1; attributeIndex >= 0; attributeIndex--) {
|
|
336
352
|
const [name, value] = parsed.attributes[attributeIndex];
|
|
@@ -338,8 +354,11 @@ function parseInto(html: string, document: Document, root: Document | DocumentFr
|
|
|
338
354
|
}
|
|
339
355
|
parent.appendChild(element);
|
|
340
356
|
index = end + 1;
|
|
341
|
-
if (
|
|
342
|
-
|
|
357
|
+
if (RAW_TEXT_CLOSE[parsed.name] && !parsed.selfClosing) {
|
|
358
|
+
// Prototype keys (e.g. `<constructor>`) were historically treated as raw text too; keep that.
|
|
359
|
+
const closePattern = Object.hasOwn(RAW_TEXT_CLOSE, parsed.name)
|
|
360
|
+
? RAW_TEXT_CLOSE[parsed.name]
|
|
361
|
+
: new RegExp(`</${parsed.name}\\s*>`, "ig");
|
|
343
362
|
closePattern.lastIndex = index;
|
|
344
363
|
const match = closePattern.exec(html);
|
|
345
364
|
const rawEnd = match?.index ?? html.length;
|
|
@@ -347,18 +366,22 @@ function parseInto(html: string, document: Document, root: Document | DocumentFr
|
|
|
347
366
|
index = match ? closePattern.lastIndex : html.length;
|
|
348
367
|
continue;
|
|
349
368
|
}
|
|
350
|
-
if (!VOID_ELEMENTS[parsed.name] && !(namespace ===
|
|
369
|
+
if (!VOID_ELEMENTS[parsed.name] && !(namespace === SVG_NAMESPACE && parsed.selfClosing)) {
|
|
351
370
|
stack.push(element);
|
|
352
371
|
parent = element;
|
|
353
372
|
}
|
|
354
373
|
}
|
|
355
|
-
void contextTag;
|
|
356
374
|
}
|
|
357
375
|
|
|
358
|
-
|
|
359
|
-
|
|
376
|
+
function isDoctypeAt(html: string, index: number): boolean {
|
|
377
|
+
DOCTYPE_PATTERN.lastIndex = index;
|
|
378
|
+
return DOCTYPE_PATTERN.test(html);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Parse markup into a document fragment. `_contextTag` is accepted for compatibility and ignored. */
|
|
382
|
+
export function parseFragment(html: string, document: Document, _contextTag?: string): DocumentFragment {
|
|
360
383
|
const fragment = document.createDocumentFragment();
|
|
361
|
-
parseInto(html, document, fragment
|
|
384
|
+
parseInto(html, document, fragment);
|
|
362
385
|
return fragment;
|
|
363
386
|
}
|
|
364
387
|
|
package/src/dom/selector.ts
CHANGED
|
@@ -1,10 +1,69 @@
|
|
|
1
1
|
import { type Document, type DocumentFragment, Element, Node } from "./core";
|
|
2
2
|
|
|
3
|
+
interface AttributeTest {
|
|
4
|
+
name: string;
|
|
5
|
+
operator: string | undefined;
|
|
6
|
+
value: string;
|
|
7
|
+
ignoreCase: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type NthTest =
|
|
11
|
+
| { kind: "odd" | "even" | "invalid" }
|
|
12
|
+
| { kind: "index"; index: number }
|
|
13
|
+
| { kind: "formula"; coefficient: number; offset: number };
|
|
14
|
+
|
|
15
|
+
interface PseudoTest {
|
|
16
|
+
name: string;
|
|
17
|
+
/** Compiled argument for `:not`, `:is`, `:where`. */
|
|
18
|
+
list: SelectorList | null;
|
|
19
|
+
nth: NthTest | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface CompoundSelector {
|
|
23
|
+
/** Lower-cased tag name, or null for any element. */
|
|
24
|
+
tag: string | null;
|
|
25
|
+
ids: string[];
|
|
26
|
+
classes: string[];
|
|
27
|
+
attributes: AttributeTest[];
|
|
28
|
+
pseudos: PseudoTest[];
|
|
29
|
+
/** Malformed or unsupported compounds never match. */
|
|
30
|
+
invalid: boolean;
|
|
31
|
+
/** Whether only `tag` (or nothing) constrains the compound. */
|
|
32
|
+
bare: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
3
35
|
interface ComplexSelector {
|
|
4
|
-
|
|
36
|
+
compounds: CompoundSelector[];
|
|
5
37
|
combinators: string[];
|
|
6
38
|
}
|
|
7
39
|
|
|
40
|
+
interface SelectorList {
|
|
41
|
+
/** A bare `*` part matches every element. */
|
|
42
|
+
universal: boolean;
|
|
43
|
+
/** Bare tag parts, matched by `localName`. */
|
|
44
|
+
tags: Set<string> | null;
|
|
45
|
+
complexes: ComplexSelector[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const SUPPORTED_PSEUDOS: Record<string, true> = {
|
|
49
|
+
not: true,
|
|
50
|
+
is: true,
|
|
51
|
+
where: true,
|
|
52
|
+
"first-child": true,
|
|
53
|
+
"last-child": true,
|
|
54
|
+
"only-child": true,
|
|
55
|
+
empty: true,
|
|
56
|
+
root: true,
|
|
57
|
+
"nth-child": true,
|
|
58
|
+
"first-of-type": true,
|
|
59
|
+
"last-of-type": true,
|
|
60
|
+
};
|
|
61
|
+
const ATTRIBUTE_PATTERN =
|
|
62
|
+
/^\s*([^\s~|^$*!=]+)\s*(?:(\^=|\$=|\*=|~=|\|=|=)\s*(?:(["'])(.*?)\3|([^\s]+))\s*([isIS])?)?\s*$/;
|
|
63
|
+
const IDENTIFIER_CHARACTER = /[a-zA-Z0-9_-]/;
|
|
64
|
+
const MAX_CACHED_SELECTORS = 512;
|
|
65
|
+
const compiledSelectors = new Map<string, SelectorList>();
|
|
66
|
+
|
|
8
67
|
function splitTopLevel(value: string, delimiter: string): string[] {
|
|
9
68
|
const parts: string[] = [];
|
|
10
69
|
let start = 0;
|
|
@@ -31,7 +90,7 @@ function splitTopLevel(value: string, delimiter: string): string[] {
|
|
|
31
90
|
return parts.filter(Boolean);
|
|
32
91
|
}
|
|
33
92
|
|
|
34
|
-
function
|
|
93
|
+
function splitComplex(selector: string): { simples: string[]; combinators: string[] } {
|
|
35
94
|
const simples: string[] = [];
|
|
36
95
|
const combinators: string[] = [];
|
|
37
96
|
let current = "";
|
|
@@ -92,7 +151,7 @@ function readIdentifier(source: string, start: number): { value: string; end: nu
|
|
|
92
151
|
index += 2;
|
|
93
152
|
continue;
|
|
94
153
|
}
|
|
95
|
-
if (
|
|
154
|
+
if (!IDENTIFIER_CHARACTER.test(character)) break;
|
|
96
155
|
value += character;
|
|
97
156
|
index++;
|
|
98
157
|
}
|
|
@@ -113,22 +172,166 @@ function findClosing(source: string, start: number, opener: string, closer: stri
|
|
|
113
172
|
return source.length - 1;
|
|
114
173
|
}
|
|
115
174
|
|
|
116
|
-
function
|
|
117
|
-
const match =
|
|
118
|
-
|
|
119
|
-
);
|
|
120
|
-
if (!match) return false;
|
|
175
|
+
function compileAttribute(expression: string): AttributeTest | null {
|
|
176
|
+
const match = ATTRIBUTE_PATTERN.exec(expression);
|
|
177
|
+
if (!match) return null;
|
|
121
178
|
const [, name, operator, , quotedValue, bareValue, flag] = match;
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
179
|
+
const ignoreCase = flag?.toLowerCase() === "i";
|
|
180
|
+
const value = (quotedValue ?? bareValue ?? "").replace(/\\(.)/g, "$1");
|
|
181
|
+
return { name, operator, value: ignoreCase ? value.toLowerCase() : value, ignoreCase };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function compileNth(expression: string): NthTest {
|
|
185
|
+
const normalized = expression.trim().toLowerCase().replace(/\s+/g, "");
|
|
186
|
+
if (normalized === "odd" || normalized === "even") return { kind: normalized };
|
|
187
|
+
if (/^[+-]?\d+$/.test(normalized)) return { kind: "index", index: Number(normalized) };
|
|
188
|
+
const match = /^([+-]?\d*)n([+-]\d+)?$/.exec(normalized);
|
|
189
|
+
if (!match) return { kind: "invalid" };
|
|
190
|
+
const coefficient = match[1] === "" || match[1] === "+" ? 1 : match[1] === "-" ? -1 : Number(match[1]);
|
|
191
|
+
return { kind: "formula", coefficient, offset: Number(match[2] ?? 0) };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function compilePseudo(name: string, argument: string | undefined): PseudoTest | null {
|
|
195
|
+
if (!SUPPORTED_PSEUDOS[name]) return null;
|
|
196
|
+
if (name === "not" || name === "is" || name === "where") {
|
|
197
|
+
return argument === undefined ? null : { name, list: compileSelector(argument), nth: null };
|
|
198
|
+
}
|
|
199
|
+
if (name === "nth-child") return argument === undefined ? null : { name, list: null, nth: compileNth(argument) };
|
|
200
|
+
return { name, list: null, nth: null };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function compileCompound(selector: string): CompoundSelector {
|
|
204
|
+
const compound: CompoundSelector = {
|
|
205
|
+
tag: null,
|
|
206
|
+
ids: [],
|
|
207
|
+
classes: [],
|
|
208
|
+
attributes: [],
|
|
209
|
+
pseudos: [],
|
|
210
|
+
invalid: false,
|
|
211
|
+
bare: false,
|
|
212
|
+
};
|
|
213
|
+
let index = 0;
|
|
214
|
+
if (selector[index] === "*") index++;
|
|
215
|
+
else if (/[a-zA-Z_]/.test(selector[index] ?? "")) {
|
|
216
|
+
const tag = readIdentifier(selector, index);
|
|
217
|
+
compound.tag = tag.value.toLowerCase();
|
|
218
|
+
index = tag.end;
|
|
219
|
+
}
|
|
220
|
+
while (index < selector.length) {
|
|
221
|
+
const marker = selector[index];
|
|
222
|
+
if (marker === "#" || marker === ".") {
|
|
223
|
+
const identifier = readIdentifier(selector, index + 1);
|
|
224
|
+
if (!identifier.value) {
|
|
225
|
+
compound.invalid = true;
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
(marker === "#" ? compound.ids : compound.classes).push(identifier.value);
|
|
229
|
+
index = identifier.end;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (marker === "[") {
|
|
233
|
+
const end = findClosing(selector, index, "[", "]");
|
|
234
|
+
const attribute = compileAttribute(selector.slice(index + 1, end));
|
|
235
|
+
if (!attribute) {
|
|
236
|
+
compound.invalid = true;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
compound.attributes.push(attribute);
|
|
240
|
+
index = end + 1;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (marker === ":") {
|
|
244
|
+
const identifier = readIdentifier(selector, index + 1);
|
|
245
|
+
let argument: string | undefined;
|
|
246
|
+
index = identifier.end;
|
|
247
|
+
if (selector[index] === "(") {
|
|
248
|
+
const end = findClosing(selector, index, "(", ")");
|
|
249
|
+
argument = selector.slice(index + 1, end);
|
|
250
|
+
index = end + 1;
|
|
251
|
+
}
|
|
252
|
+
const pseudo = compilePseudo(identifier.value.toLowerCase(), argument);
|
|
253
|
+
if (!pseudo) {
|
|
254
|
+
compound.invalid = true;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
compound.pseudos.push(pseudo);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
compound.invalid = true;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
compound.bare =
|
|
264
|
+
!compound.invalid &&
|
|
265
|
+
compound.ids.length === 0 &&
|
|
266
|
+
compound.classes.length === 0 &&
|
|
267
|
+
compound.attributes.length === 0 &&
|
|
268
|
+
compound.pseudos.length === 0;
|
|
269
|
+
return compound;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function compileSelector(selector: string): SelectorList {
|
|
273
|
+
const cached = compiledSelectors.get(selector);
|
|
274
|
+
if (cached) return cached;
|
|
275
|
+
const list: SelectorList = { universal: false, tags: null, complexes: [] };
|
|
276
|
+
for (const part of splitTopLevel(selector, ",")) {
|
|
277
|
+
const { simples, combinators } = splitComplex(part);
|
|
278
|
+
if (!simples.length) continue;
|
|
279
|
+
const compounds = simples.map(compileCompound);
|
|
280
|
+
const only = compounds[0];
|
|
281
|
+
if (compounds.length === 1 && only.bare) {
|
|
282
|
+
if (only.tag === null) list.universal = true;
|
|
283
|
+
else (list.tags ??= new Set()).add(only.tag);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
list.complexes.push({ compounds, combinators });
|
|
287
|
+
}
|
|
288
|
+
if (compiledSelectors.size >= MAX_CACHED_SELECTORS) compiledSelectors.clear();
|
|
289
|
+
compiledSelectors.set(selector, list);
|
|
290
|
+
return list;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function isWhitespace(code: number): boolean {
|
|
294
|
+
return (
|
|
295
|
+
code === 0x20 ||
|
|
296
|
+
(code >= 0x09 && code <= 0x0d) ||
|
|
297
|
+
code === 0xa0 ||
|
|
298
|
+
code === 0x1680 ||
|
|
299
|
+
(code >= 0x2000 && code <= 0x200a) ||
|
|
300
|
+
code === 0x2028 ||
|
|
301
|
+
code === 0x2029 ||
|
|
302
|
+
code === 0x202f ||
|
|
303
|
+
code === 0x205f ||
|
|
304
|
+
code === 0x3000 ||
|
|
305
|
+
code === 0xfeff
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Whether a whitespace-separated class string contains a token, without splitting it. */
|
|
310
|
+
export function hasClassToken(className: string, token: string): boolean {
|
|
311
|
+
if (!token) return false;
|
|
312
|
+
for (let index = 0; index < token.length; index++) if (isWhitespace(token.charCodeAt(index))) return false;
|
|
313
|
+
let from = 0;
|
|
314
|
+
while (true) {
|
|
315
|
+
const found = className.indexOf(token, from);
|
|
316
|
+
if (found < 0) return false;
|
|
317
|
+
const end = found + token.length;
|
|
318
|
+
if (
|
|
319
|
+
(found === 0 || isWhitespace(className.charCodeAt(found - 1))) &&
|
|
320
|
+
(end === className.length || isWhitespace(className.charCodeAt(end)))
|
|
321
|
+
) {
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
from = found + 1;
|
|
130
325
|
}
|
|
131
|
-
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function matchAttribute(element: Element, test: AttributeTest): boolean {
|
|
329
|
+
const actual = element.getAttribute(test.name);
|
|
330
|
+
if (!test.operator) return actual !== null;
|
|
331
|
+
if (actual === null) return false;
|
|
332
|
+
const left = test.ignoreCase ? actual.toLowerCase() : actual;
|
|
333
|
+
const right = test.value;
|
|
334
|
+
switch (test.operator) {
|
|
132
335
|
case "=":
|
|
133
336
|
return left === right;
|
|
134
337
|
case "^=":
|
|
@@ -146,41 +349,67 @@ function matchAttribute(element: Element, expression: string): boolean {
|
|
|
146
349
|
}
|
|
147
350
|
}
|
|
148
351
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
352
|
+
/** One-based position among element siblings, or 0 without a parent element. */
|
|
353
|
+
function elementIndex(element: Element): number {
|
|
354
|
+
const parent = element.parentElement;
|
|
355
|
+
if (!parent) return 0;
|
|
356
|
+
let index = 0;
|
|
357
|
+
for (const sibling of parent.childNodes) {
|
|
358
|
+
if (!(sibling instanceof Element)) continue;
|
|
359
|
+
index++;
|
|
360
|
+
if (sibling === element) return index;
|
|
361
|
+
}
|
|
362
|
+
return 0;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function matchNth(element: Element, test: NthTest): boolean {
|
|
366
|
+
if (test.kind === "invalid") return false;
|
|
367
|
+
const index = elementIndex(element);
|
|
368
|
+
switch (test.kind) {
|
|
369
|
+
case "odd":
|
|
370
|
+
return index % 2 === 1;
|
|
371
|
+
case "even":
|
|
372
|
+
return index % 2 === 0;
|
|
373
|
+
case "index":
|
|
374
|
+
return index === test.index;
|
|
375
|
+
case "formula":
|
|
376
|
+
return test.coefficient === 0
|
|
377
|
+
? index === test.offset
|
|
378
|
+
: (index - test.offset) / test.coefficient >= 0 &&
|
|
379
|
+
Number.isInteger((index - test.offset) / test.coefficient);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function hasSingleElementChild(parent: Element): boolean {
|
|
384
|
+
let count = 0;
|
|
385
|
+
for (const child of parent.childNodes) if (child instanceof Element && ++count > 1) return false;
|
|
386
|
+
return count === 1;
|
|
163
387
|
}
|
|
164
388
|
|
|
165
|
-
function matchPseudo(element: Element,
|
|
166
|
-
switch (name) {
|
|
389
|
+
function matchPseudo(element: Element, pseudo: PseudoTest): boolean {
|
|
390
|
+
switch (pseudo.name) {
|
|
167
391
|
case "not":
|
|
168
|
-
return
|
|
392
|
+
return !matchesList(element, pseudo.list as SelectorList);
|
|
169
393
|
case "is":
|
|
170
394
|
case "where":
|
|
171
|
-
return
|
|
395
|
+
return matchesList(element, pseudo.list as SelectorList);
|
|
172
396
|
case "first-child":
|
|
173
397
|
return element.parentElement?.firstElementChild === element;
|
|
174
398
|
case "last-child":
|
|
175
399
|
return element.parentElement?.lastElementChild === element;
|
|
176
|
-
case "only-child":
|
|
177
|
-
|
|
400
|
+
case "only-child": {
|
|
401
|
+
const parent = element.parentElement;
|
|
402
|
+
return parent !== null && hasSingleElementChild(parent);
|
|
403
|
+
}
|
|
178
404
|
case "empty":
|
|
179
|
-
return
|
|
405
|
+
return (
|
|
406
|
+
!element.hasChildNodes() ||
|
|
407
|
+
element.childNodes.every(child => child.nodeType === Node.COMMENT_NODE || child.textContent === "")
|
|
408
|
+
);
|
|
180
409
|
case "root":
|
|
181
410
|
return element.ownerDocument?.documentElement === element;
|
|
182
411
|
case "nth-child":
|
|
183
|
-
return
|
|
412
|
+
return matchNth(element, pseudo.nth as NthTest);
|
|
184
413
|
case "first-of-type": {
|
|
185
414
|
for (let sibling = element.previousElementSibling; sibling; sibling = sibling.previousElementSibling) {
|
|
186
415
|
if (sibling.tagName === element.tagName) return false;
|
|
@@ -198,57 +427,30 @@ function matchPseudo(element: Element, name: string, argument: string | undefine
|
|
|
198
427
|
}
|
|
199
428
|
}
|
|
200
429
|
|
|
201
|
-
function
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
while (index < selector.length) {
|
|
210
|
-
const marker = selector[index];
|
|
211
|
-
if (marker === "#" || marker === ".") {
|
|
212
|
-
const identifier = readIdentifier(selector, index + 1);
|
|
213
|
-
if (!identifier.value) return false;
|
|
214
|
-
if (marker === "#" ? element.id !== identifier.value : !element.classList.contains(identifier.value))
|
|
215
|
-
return false;
|
|
216
|
-
index = identifier.end;
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
if (marker === "[") {
|
|
220
|
-
const end = findClosing(selector, index, "[", "]");
|
|
221
|
-
if (!matchAttribute(element, selector.slice(index + 1, end))) return false;
|
|
222
|
-
index = end + 1;
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
if (marker === ":") {
|
|
226
|
-
const identifier = readIdentifier(selector, index + 1);
|
|
227
|
-
let argument: string | undefined;
|
|
228
|
-
index = identifier.end;
|
|
229
|
-
if (selector[index] === "(") {
|
|
230
|
-
const end = findClosing(selector, index, "(", ")");
|
|
231
|
-
argument = selector.slice(index + 1, end);
|
|
232
|
-
index = end + 1;
|
|
233
|
-
}
|
|
234
|
-
if (!matchPseudo(element, identifier.value.toLowerCase(), argument)) return false;
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
return false;
|
|
430
|
+
function matchCompound(element: Element, compound: CompoundSelector): boolean {
|
|
431
|
+
if (compound.invalid) return false;
|
|
432
|
+
if (compound.tag !== null && element.localName !== compound.tag) return false;
|
|
433
|
+
if (compound.bare) return true;
|
|
434
|
+
for (const id of compound.ids) if (element.id !== id) return false;
|
|
435
|
+
if (compound.classes.length) {
|
|
436
|
+
const className = element.className;
|
|
437
|
+
for (const token of compound.classes) if (!hasClassToken(className, token)) return false;
|
|
238
438
|
}
|
|
439
|
+
for (const attribute of compound.attributes) if (!matchAttribute(element, attribute)) return false;
|
|
440
|
+
for (const pseudo of compound.pseudos) if (!matchPseudo(element, pseudo)) return false;
|
|
239
441
|
return true;
|
|
240
442
|
}
|
|
241
443
|
|
|
242
444
|
function matchComplexAt(element: Element, complex: ComplexSelector, index: number): boolean {
|
|
243
|
-
if (!
|
|
445
|
+
if (!matchCompound(element, complex.compounds[index])) return false;
|
|
244
446
|
if (index === 0) return true;
|
|
245
447
|
const combinator = complex.combinators[index - 1] ?? " ";
|
|
246
448
|
if (combinator === ">")
|
|
247
449
|
return element.parentElement !== null && matchComplexAt(element.parentElement, complex, index - 1);
|
|
248
|
-
if (combinator === "+")
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
450
|
+
if (combinator === "+") {
|
|
451
|
+
const previous = element.previousElementSibling;
|
|
452
|
+
return previous !== null && matchComplexAt(previous, complex, index - 1);
|
|
453
|
+
}
|
|
252
454
|
if (combinator === "~") {
|
|
253
455
|
for (let sibling = element.previousElementSibling; sibling; sibling = sibling.previousElementSibling) {
|
|
254
456
|
if (matchComplexAt(sibling, complex, index - 1)) return true;
|
|
@@ -261,30 +463,61 @@ function matchComplexAt(element: Element, complex: ComplexSelector, index: numbe
|
|
|
261
463
|
return false;
|
|
262
464
|
}
|
|
263
465
|
|
|
466
|
+
function matchesList(element: Element, list: SelectorList): boolean {
|
|
467
|
+
if (list.universal) return true;
|
|
468
|
+
if (list.tags?.has(element.localName)) return true;
|
|
469
|
+
for (const complex of list.complexes) {
|
|
470
|
+
if (matchComplexAt(element, complex, complex.compounds.length - 1)) return true;
|
|
471
|
+
}
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function collectMatches(nodes: Node[], list: SelectorList, result: Element[]): void {
|
|
476
|
+
for (let index = 0; index < nodes.length; index++) {
|
|
477
|
+
const node = nodes[index];
|
|
478
|
+
if (node instanceof Element && matchesList(node, list)) result.push(node);
|
|
479
|
+
if (node.hasChildNodes()) collectMatches(node.childNodes, list, result);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function findMatch(nodes: Node[], list: SelectorList): Element | null {
|
|
484
|
+
for (let index = 0; index < nodes.length; index++) {
|
|
485
|
+
const node = nodes[index];
|
|
486
|
+
if (node instanceof Element && matchesList(node, list)) return node;
|
|
487
|
+
if (node.hasChildNodes()) {
|
|
488
|
+
const found = findMatch(node.childNodes, list);
|
|
489
|
+
if (found) return found;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
|
|
264
495
|
/** Whether an element matches a CSS selector list. */
|
|
265
496
|
export function matchesSelector(element: Element, selector: string): boolean {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
497
|
+
return matchesList(element, compileSelector(selector));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Nearest inclusive ancestor of an element matching a CSS selector list. */
|
|
501
|
+
export function closestMatching(element: Element, selector: string): Element | null {
|
|
502
|
+
const list = compileSelector(selector);
|
|
503
|
+
for (let current: Element | null = element; current; current = current.parentElement) {
|
|
504
|
+
if (matchesList(current, list)) return current;
|
|
269
505
|
}
|
|
270
|
-
return
|
|
506
|
+
return null;
|
|
271
507
|
}
|
|
272
508
|
|
|
273
509
|
/** Query descendants of a node in document order. */
|
|
274
510
|
export function querySelectorAllFrom(
|
|
275
511
|
root: Document | DocumentFragment | Element,
|
|
276
512
|
selector: string,
|
|
277
|
-
|
|
513
|
+
_includeRoot: boolean,
|
|
278
514
|
): Element[] {
|
|
279
515
|
const result: Element[] = [];
|
|
280
|
-
|
|
281
|
-
if (node instanceof Element && matchesSelector(node, selector)) result.push(node);
|
|
282
|
-
for (const child of node.childNodes) visit(child);
|
|
283
|
-
};
|
|
284
|
-
if (includeRoot) {
|
|
285
|
-
for (const child of root.childNodes) visit(child);
|
|
286
|
-
} else {
|
|
287
|
-
for (const child of root.childNodes) visit(child);
|
|
288
|
-
}
|
|
516
|
+
if (root.hasChildNodes()) collectMatches(root.childNodes, compileSelector(selector), result);
|
|
289
517
|
return result;
|
|
290
518
|
}
|
|
519
|
+
|
|
520
|
+
/** First descendant of a node in document order matching a selector, stopping at the first match. */
|
|
521
|
+
export function querySelectorFrom(root: Document | DocumentFragment | Element, selector: string): Element | null {
|
|
522
|
+
return root.hasChildNodes() ? findMatch(root.childNodes, compileSelector(selector)) : null;
|
|
523
|
+
}
|
package/src/fetch-retry.ts
CHANGED
|
@@ -382,6 +382,8 @@ export async function fetchWithRetry(
|
|
|
382
382
|
if (hint !== undefined && hint > maxDelayMs) return response;
|
|
383
383
|
|
|
384
384
|
const delayMs = Math.min(hint ?? resolveDefaultDelay(defaultDelayMs, attempt, maxDelayMs), maxDelayMs);
|
|
385
|
+
// The retried response is discarded; release its unread tee branch.
|
|
386
|
+
if (!response.bodyUsed) response.body?.cancel().catch(() => {});
|
|
385
387
|
await waitForRetry(delayMs, signal);
|
|
386
388
|
}
|
|
387
389
|
}
|
package/src/format.ts
CHANGED
|
@@ -10,7 +10,11 @@ const DAY = 24 * HOUR;
|
|
|
10
10
|
export function formatDuration(ms: number): string {
|
|
11
11
|
if (!Number.isFinite(ms) || ms <= 0) return "0ms";
|
|
12
12
|
if (ms < SEC) return `${Math.floor(ms)}ms`;
|
|
13
|
-
if (ms < MIN)
|
|
13
|
+
if (ms < MIN) {
|
|
14
|
+
// Round to tenths first: `toFixed(1)` alone turns 59.95s+ into an impossible `60.0s`.
|
|
15
|
+
const tenths = Math.round(ms / 100);
|
|
16
|
+
return tenths < 600 ? `${(tenths / 10).toFixed(1)}s` : "1m";
|
|
17
|
+
}
|
|
14
18
|
if (ms < HOUR) {
|
|
15
19
|
const mins = Math.floor(ms / MIN);
|
|
16
20
|
const secs = Math.floor((ms % MIN) / SEC);
|