agent-sanitizer 2.64.0 → 2.64.2
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/LICENSE-THIRD-PARTY +31 -3
- package/THREAT-MODEL.md +7 -3
- package/package.json +4 -1
- package/src/css-syntax-data.mjs +274 -0
- package/src/gfm.mjs +3 -2
- package/src/html-tree-adapter.mjs +150 -1
- package/src/html.mjs +873 -354
- package/src/url-attributes.mjs +76 -0
- package/src/vendor/gfm-autolink-literal.mjs +53 -178
- package/types/css-syntax-data.d.mts +6 -0
- package/types/html-tree-adapter.d.mts +46 -0
- package/types/html.d.mts +43 -7
- package/types/url-attributes.d.mts +11 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The attributes a browser resolves as URLs: the one table every Layer-3 URL
|
|
3
|
+
* read (and Layer 2's data: resource count) goes through; its test partitions
|
|
4
|
+
* the HTML Standard's URL attributes between it and an exempt list. `property`
|
|
5
|
+
* is the hast spelling; `elements` null means any element; a "form" or
|
|
6
|
+
* "refresh" `context` is also flagged for leaving the origin; `fetched` is a
|
|
7
|
+
* load nobody decides on, except on `notFetchedOn`. Rows state only where they
|
|
8
|
+
* differ from {@link DEFAULTS}.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {{
|
|
13
|
+
* attribute: string,
|
|
14
|
+
* property: string,
|
|
15
|
+
* grammar: "url" | "urls" | "srcset" | "refresh" | "css",
|
|
16
|
+
* elements: readonly string[] | null,
|
|
17
|
+
* context: "resource" | "form" | "refresh",
|
|
18
|
+
* fetched: boolean,
|
|
19
|
+
* notFetchedOn: readonly string[],
|
|
20
|
+
* }} UrlAttribute
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const DEFAULTS = Object.freeze({
|
|
24
|
+
grammar: "url",
|
|
25
|
+
elements: null,
|
|
26
|
+
context: "resource",
|
|
27
|
+
fetched: true,
|
|
28
|
+
notFetchedOn: [],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/** @type {readonly UrlAttribute[]} */
|
|
32
|
+
export const URL_ATTRIBUTES = Object.freeze(
|
|
33
|
+
/** @type {Array<Partial<UrlAttribute> & { attribute: string, property: string }>} */ ([
|
|
34
|
+
{ attribute: "src", property: "src" },
|
|
35
|
+
// A link waits for a click; `<base>` only re-bases relative URLs.
|
|
36
|
+
{
|
|
37
|
+
attribute: "href",
|
|
38
|
+
property: "href",
|
|
39
|
+
notFetchedOn: ["a", "area", "base"],
|
|
40
|
+
},
|
|
41
|
+
{ attribute: "xlink:href", property: "xLinkHref", notFetchedOn: ["a"] },
|
|
42
|
+
{ attribute: "background", property: "background" },
|
|
43
|
+
{ attribute: "srcset", property: "srcSet", grammar: "srcset" },
|
|
44
|
+
{
|
|
45
|
+
attribute: "imagesrcset",
|
|
46
|
+
property: "imageSrcSet",
|
|
47
|
+
grammar: "srcset",
|
|
48
|
+
elements: ["link"],
|
|
49
|
+
},
|
|
50
|
+
{ attribute: "ping", property: "ping", grammar: "urls" },
|
|
51
|
+
{ attribute: "action", property: "action", context: "form" },
|
|
52
|
+
{ attribute: "formaction", property: "formAction", context: "form" },
|
|
53
|
+
{ attribute: "data", property: "data", elements: ["object"] },
|
|
54
|
+
{ attribute: "poster", property: "poster", elements: ["video"] },
|
|
55
|
+
{
|
|
56
|
+
attribute: "cite",
|
|
57
|
+
property: "cite",
|
|
58
|
+
elements: ["blockquote", "del", "ins", "q"],
|
|
59
|
+
fetched: false,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
attribute: "longdesc",
|
|
63
|
+
property: "longDesc",
|
|
64
|
+
elements: ["img", "iframe", "frame"],
|
|
65
|
+
fetched: false,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
attribute: "content",
|
|
69
|
+
property: "content",
|
|
70
|
+
grammar: "refresh",
|
|
71
|
+
elements: ["meta"],
|
|
72
|
+
context: "refresh",
|
|
73
|
+
},
|
|
74
|
+
{ attribute: "style", property: "style", grammar: "css" },
|
|
75
|
+
]).map((row) => Object.freeze({ ...DEFAULTS, ...row })),
|
|
76
|
+
);
|
|
@@ -1,81 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Wormer). The licence is in `LICENSE-THIRD-PARTY` at the repository root.
|
|
2
|
+
* GENERATED by scripts/gen-gfm-autolink.mjs at install, pack and build time;
|
|
3
|
+
* git-ignored, never committed — DO NOT EDIT.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* and costs one full walk per word character. 172 KB of prose behind a single
|
|
12
|
-
* `[` took 20.2s to parse; the same text takes 0.17s here. Layer 3 reads its
|
|
13
|
-
* URLs off this parse, so the cost lands on every `detectExfil` call.
|
|
14
|
-
*
|
|
15
|
-
* WHAT CHANGED. One function. `previousUnbalanced` now also records where the
|
|
16
|
-
* walk stopped, and a later walk resumes from that index rather than re-reading
|
|
17
|
-
* the run before it. The record is used only while `events[index]` still holds
|
|
18
|
-
* the same token, so a resolver that splices events invalidates it and the walk
|
|
19
|
-
* falls back to reading everything. Nothing else in this file differs from
|
|
20
|
-
* upstream — diff it against the published package to confirm.
|
|
21
|
-
*
|
|
22
|
-
* WHY NOT A PATCH. A `pnpm` patch fixes this repository's own installs and the
|
|
23
|
-
* plugin bundle built from them, and reaches no consumer of the published npm
|
|
24
|
-
* package (see the npm-consumer gap in the project history). A copy under
|
|
25
|
-
* `src/` ships.
|
|
26
|
-
*
|
|
27
|
-
* WHEN TO DELETE IT. When a published release of the upstream package carries
|
|
28
|
-
* an equivalent fix: drop this file and the assembly in `./gfm.mjs`, and go
|
|
29
|
-
* back to `remark-gfm`. `test/gfm-autolink-parity.test.mjs` pins the behaviour
|
|
30
|
-
* either way.
|
|
5
|
+
* `micromark-extension-gfm-autolink-literal@2.1.0`'s `lib/syntax.js` (MIT, (c) 2020
|
|
6
|
+
* Titus Wormer; licence in LICENSE-THIRD-PARTY) with the one-function
|
|
7
|
+
* `previousUnbalanced` performance fix from
|
|
8
|
+
* `scripts/patches/micromark-extension-gfm-autolink-literal@2.1.0.patch`. The
|
|
9
|
+
* generator's header says why it is patched and when to delete it.
|
|
31
10
|
*/
|
|
32
|
-
|
|
33
11
|
/**
|
|
34
12
|
* @import {Code, ConstructRecord, Event, Extension, Previous, State, TokenizeContext, Tokenizer} from 'micromark-util-types'
|
|
35
13
|
*/
|
|
36
14
|
|
|
37
|
-
import {
|
|
38
|
-
asciiAlpha,
|
|
39
|
-
asciiAlphanumeric,
|
|
40
|
-
asciiControl,
|
|
41
|
-
markdownLineEndingOrSpace,
|
|
42
|
-
unicodePunctuation,
|
|
43
|
-
unicodeWhitespace,
|
|
44
|
-
} from "micromark-util-character";
|
|
15
|
+
import { asciiAlpha, asciiAlphanumeric, asciiControl, markdownLineEndingOrSpace, unicodePunctuation, unicodeWhitespace } from 'micromark-util-character';
|
|
45
16
|
const wwwPrefix = {
|
|
46
17
|
tokenize: tokenizeWwwPrefix,
|
|
47
|
-
partial: true
|
|
18
|
+
partial: true
|
|
48
19
|
};
|
|
49
20
|
const domain = {
|
|
50
21
|
tokenize: tokenizeDomain,
|
|
51
|
-
partial: true
|
|
22
|
+
partial: true
|
|
52
23
|
};
|
|
53
24
|
const path = {
|
|
54
25
|
tokenize: tokenizePath,
|
|
55
|
-
partial: true
|
|
26
|
+
partial: true
|
|
56
27
|
};
|
|
57
28
|
const trail = {
|
|
58
29
|
tokenize: tokenizeTrail,
|
|
59
|
-
partial: true
|
|
30
|
+
partial: true
|
|
60
31
|
};
|
|
61
32
|
const emailDomainDotTrail = {
|
|
62
33
|
tokenize: tokenizeEmailDomainDotTrail,
|
|
63
|
-
partial: true
|
|
34
|
+
partial: true
|
|
64
35
|
};
|
|
65
36
|
const wwwAutolink = {
|
|
66
|
-
name:
|
|
37
|
+
name: 'wwwAutolink',
|
|
67
38
|
tokenize: tokenizeWwwAutolink,
|
|
68
|
-
previous: previousWww
|
|
39
|
+
previous: previousWww
|
|
69
40
|
};
|
|
70
41
|
const protocolAutolink = {
|
|
71
|
-
name:
|
|
42
|
+
name: 'protocolAutolink',
|
|
72
43
|
tokenize: tokenizeProtocolAutolink,
|
|
73
|
-
previous: previousProtocol
|
|
44
|
+
previous: previousProtocol
|
|
74
45
|
};
|
|
75
46
|
const emailAutolink = {
|
|
76
|
-
name:
|
|
47
|
+
name: 'emailAutolink',
|
|
77
48
|
tokenize: tokenizeEmailAutolink,
|
|
78
|
-
previous: previousEmail
|
|
49
|
+
previous: previousEmail
|
|
79
50
|
};
|
|
80
51
|
|
|
81
52
|
/** @type {ConstructRecord} */
|
|
@@ -91,7 +62,7 @@ const text = {};
|
|
|
91
62
|
*/
|
|
92
63
|
export function gfmAutolinkLiteral() {
|
|
93
64
|
return {
|
|
94
|
-
text
|
|
65
|
+
text
|
|
95
66
|
};
|
|
96
67
|
}
|
|
97
68
|
|
|
@@ -102,8 +73,7 @@ let code = 48;
|
|
|
102
73
|
while (code < 123) {
|
|
103
74
|
text[code] = emailAutolink;
|
|
104
75
|
code++;
|
|
105
|
-
if (code === 58) code = 65;
|
|
106
|
-
else if (code === 91) code = 97;
|
|
76
|
+
if (code === 58) code = 65;else if (code === 91) code = 97;
|
|
107
77
|
}
|
|
108
78
|
text[43] = emailAutolink;
|
|
109
79
|
text[45] = emailAutolink;
|
|
@@ -150,15 +120,11 @@ function tokenizeEmailAutolink(effects, ok, nok) {
|
|
|
150
120
|
* @type {State}
|
|
151
121
|
*/
|
|
152
122
|
function start(code) {
|
|
153
|
-
if (
|
|
154
|
-
!gfmAtext(code) ||
|
|
155
|
-
!previousEmail.call(self, self.previous) ||
|
|
156
|
-
previousUnbalanced(self.events)
|
|
157
|
-
) {
|
|
123
|
+
if (!gfmAtext(code) || !previousEmail.call(self, self.previous) || previousUnbalanced(self.events)) {
|
|
158
124
|
return nok(code);
|
|
159
125
|
}
|
|
160
|
-
effects.enter(
|
|
161
|
-
effects.enter(
|
|
126
|
+
effects.enter('literalAutolink');
|
|
127
|
+
effects.enter('literalAutolinkEmail');
|
|
162
128
|
return atext(code);
|
|
163
129
|
}
|
|
164
130
|
|
|
@@ -201,11 +167,7 @@ function tokenizeEmailAutolink(effects, ok, nok) {
|
|
|
201
167
|
function emailDomain(code) {
|
|
202
168
|
// Dot followed by alphanumerical (not `-` or `_`).
|
|
203
169
|
if (code === 46) {
|
|
204
|
-
return effects.check(
|
|
205
|
-
emailDomainDotTrail,
|
|
206
|
-
emailDomainAfter,
|
|
207
|
-
emailDomainDot,
|
|
208
|
-
)(code);
|
|
170
|
+
return effects.check(emailDomainDotTrail, emailDomainAfter, emailDomainDot)(code);
|
|
209
171
|
}
|
|
210
172
|
|
|
211
173
|
// Alphanumerical, `-`, and `_`.
|
|
@@ -254,8 +216,8 @@ function tokenizeEmailAutolink(effects, ok, nok) {
|
|
|
254
216
|
// Domain must not be empty, must include a dot, and must end in alphabetical.
|
|
255
217
|
// Source: <https://github.com/github/cmark-gfm/blob/ef1cfcb/extensions/autolink.c#L332>.
|
|
256
218
|
if (data && dot && asciiAlpha(self.previous)) {
|
|
257
|
-
effects.exit(
|
|
258
|
-
effects.exit(
|
|
219
|
+
effects.exit('literalAutolinkEmail');
|
|
220
|
+
effects.exit('literalAutolink');
|
|
259
221
|
return ok(code);
|
|
260
222
|
}
|
|
261
223
|
return nok(code);
|
|
@@ -288,22 +250,14 @@ function tokenizeWwwAutolink(effects, ok, nok) {
|
|
|
288
250
|
* @type {State}
|
|
289
251
|
*/
|
|
290
252
|
function wwwStart(code) {
|
|
291
|
-
if (
|
|
292
|
-
(code !== 87 && code !== 119) ||
|
|
293
|
-
!previousWww.call(self, self.previous) ||
|
|
294
|
-
previousUnbalanced(self.events)
|
|
295
|
-
) {
|
|
253
|
+
if (code !== 87 && code !== 119 || !previousWww.call(self, self.previous) || previousUnbalanced(self.events)) {
|
|
296
254
|
return nok(code);
|
|
297
255
|
}
|
|
298
|
-
effects.enter(
|
|
299
|
-
effects.enter(
|
|
256
|
+
effects.enter('literalAutolink');
|
|
257
|
+
effects.enter('literalAutolinkWww');
|
|
300
258
|
// Note: we *check*, so we can discard the `www.` we parsed.
|
|
301
259
|
// If it worked, we consider it as a part of the domain.
|
|
302
|
-
return effects.check(
|
|
303
|
-
wwwPrefix,
|
|
304
|
-
effects.attempt(domain, effects.attempt(path, wwwAfter), nok),
|
|
305
|
-
nok,
|
|
306
|
-
)(code);
|
|
260
|
+
return effects.check(wwwPrefix, effects.attempt(domain, effects.attempt(path, wwwAfter), nok), nok)(code);
|
|
307
261
|
}
|
|
308
262
|
|
|
309
263
|
/**
|
|
@@ -317,8 +271,8 @@ function tokenizeWwwAutolink(effects, ok, nok) {
|
|
|
317
271
|
* @type {State}
|
|
318
272
|
*/
|
|
319
273
|
function wwwAfter(code) {
|
|
320
|
-
effects.exit(
|
|
321
|
-
effects.exit(
|
|
274
|
+
effects.exit('literalAutolinkWww');
|
|
275
|
+
effects.exit('literalAutolink');
|
|
322
276
|
return ok(code);
|
|
323
277
|
}
|
|
324
278
|
}
|
|
@@ -336,7 +290,7 @@ function tokenizeWwwAutolink(effects, ok, nok) {
|
|
|
336
290
|
*/
|
|
337
291
|
function tokenizeProtocolAutolink(effects, ok, nok) {
|
|
338
292
|
const self = this;
|
|
339
|
-
let buffer =
|
|
293
|
+
let buffer = '';
|
|
340
294
|
let seen = false;
|
|
341
295
|
return protocolStart;
|
|
342
296
|
|
|
@@ -351,13 +305,9 @@ function tokenizeProtocolAutolink(effects, ok, nok) {
|
|
|
351
305
|
* @type {State}
|
|
352
306
|
*/
|
|
353
307
|
function protocolStart(code) {
|
|
354
|
-
if (
|
|
355
|
-
(
|
|
356
|
-
|
|
357
|
-
!previousUnbalanced(self.events)
|
|
358
|
-
) {
|
|
359
|
-
effects.enter("literalAutolink");
|
|
360
|
-
effects.enter("literalAutolinkHttp");
|
|
308
|
+
if ((code === 72 || code === 104) && previousProtocol.call(self, self.previous) && !previousUnbalanced(self.events)) {
|
|
309
|
+
effects.enter('literalAutolink');
|
|
310
|
+
effects.enter('literalAutolinkHttp');
|
|
361
311
|
buffer += String.fromCodePoint(code);
|
|
362
312
|
effects.consume(code);
|
|
363
313
|
return protocolPrefixInside;
|
|
@@ -385,7 +335,7 @@ function tokenizeProtocolAutolink(effects, ok, nok) {
|
|
|
385
335
|
}
|
|
386
336
|
if (code === 58) {
|
|
387
337
|
const protocol = buffer.toLowerCase();
|
|
388
|
-
if (protocol ===
|
|
338
|
+
if (protocol === 'http' || protocol === 'https') {
|
|
389
339
|
effects.consume(code);
|
|
390
340
|
return protocolSlashesInside;
|
|
391
341
|
}
|
|
@@ -428,17 +378,7 @@ function tokenizeProtocolAutolink(effects, ok, nok) {
|
|
|
428
378
|
function afterProtocol(code) {
|
|
429
379
|
// To do: this is different from `markdown-rs`:
|
|
430
380
|
// https://github.com/wooorm/markdown-rs/blob/b3a921c761309ae00a51fe348d8a43adbc54b518/src/construct/gfm_autolink_literal.rs#L172-L182
|
|
431
|
-
return code === null ||
|
|
432
|
-
asciiControl(code) ||
|
|
433
|
-
markdownLineEndingOrSpace(code) ||
|
|
434
|
-
unicodeWhitespace(code) ||
|
|
435
|
-
unicodePunctuation(code)
|
|
436
|
-
? nok(code)
|
|
437
|
-
: effects.attempt(
|
|
438
|
-
domain,
|
|
439
|
-
effects.attempt(path, protocolAfter),
|
|
440
|
-
nok,
|
|
441
|
-
)(code);
|
|
381
|
+
return code === null || asciiControl(code) || markdownLineEndingOrSpace(code) || unicodeWhitespace(code) || unicodePunctuation(code) ? nok(code) : effects.attempt(domain, effects.attempt(path, protocolAfter), nok)(code);
|
|
442
382
|
}
|
|
443
383
|
|
|
444
384
|
/**
|
|
@@ -452,8 +392,8 @@ function tokenizeProtocolAutolink(effects, ok, nok) {
|
|
|
452
392
|
* @type {State}
|
|
453
393
|
*/
|
|
454
394
|
function protocolAfter(code) {
|
|
455
|
-
effects.exit(
|
|
456
|
-
effects.exit(
|
|
395
|
+
effects.exit('literalAutolinkHttp');
|
|
396
|
+
effects.exit('literalAutolink');
|
|
457
397
|
return ok(code);
|
|
458
398
|
}
|
|
459
399
|
}
|
|
@@ -556,12 +496,7 @@ function tokenizeDomain(effects, ok, nok) {
|
|
|
556
496
|
// Instead of some new production for Unicode alphanumerics, markdown
|
|
557
497
|
// already has that for Unicode punctuation and whitespace, so use those.
|
|
558
498
|
// Source: <https://github.com/github/cmark-gfm/blob/ef1cfcb/extensions/autolink.c#L12>.
|
|
559
|
-
if (
|
|
560
|
-
code === null ||
|
|
561
|
-
markdownLineEndingOrSpace(code) ||
|
|
562
|
-
unicodeWhitespace(code) ||
|
|
563
|
-
(code !== 45 && unicodePunctuation(code))
|
|
564
|
-
) {
|
|
499
|
+
if (code === null || markdownLineEndingOrSpace(code) || unicodeWhitespace(code) || code !== 45 && unicodePunctuation(code)) {
|
|
565
500
|
return domainAfter(code);
|
|
566
501
|
}
|
|
567
502
|
seen = true;
|
|
@@ -656,30 +591,10 @@ function tokenizePath(effects, ok) {
|
|
|
656
591
|
// Check whether this trailing punctuation marker is optionally
|
|
657
592
|
// followed by more trailing markers, and then followed
|
|
658
593
|
// by an end.
|
|
659
|
-
if (
|
|
660
|
-
code === 33 ||
|
|
661
|
-
code === 34 ||
|
|
662
|
-
code === 38 ||
|
|
663
|
-
code === 39 ||
|
|
664
|
-
code === 41 ||
|
|
665
|
-
code === 42 ||
|
|
666
|
-
code === 44 ||
|
|
667
|
-
code === 46 ||
|
|
668
|
-
code === 58 ||
|
|
669
|
-
code === 59 ||
|
|
670
|
-
code === 60 ||
|
|
671
|
-
code === 63 ||
|
|
672
|
-
code === 93 ||
|
|
673
|
-
code === 95 ||
|
|
674
|
-
code === 126
|
|
675
|
-
) {
|
|
594
|
+
if (code === 33 || code === 34 || code === 38 || code === 39 || code === 41 || code === 42 || code === 44 || code === 46 || code === 58 || code === 59 || code === 60 || code === 63 || code === 93 || code === 95 || code === 126) {
|
|
676
595
|
return effects.check(trail, ok, pathAtPunctuation)(code);
|
|
677
596
|
}
|
|
678
|
-
if (
|
|
679
|
-
code === null ||
|
|
680
|
-
markdownLineEndingOrSpace(code) ||
|
|
681
|
-
unicodeWhitespace(code)
|
|
682
|
-
) {
|
|
597
|
+
if (code === null || markdownLineEndingOrSpace(code) || unicodeWhitespace(code)) {
|
|
683
598
|
return ok(code);
|
|
684
599
|
}
|
|
685
600
|
effects.consume(code);
|
|
@@ -736,20 +651,7 @@ function tokenizeTrail(effects, ok, nok) {
|
|
|
736
651
|
*/
|
|
737
652
|
function trail(code) {
|
|
738
653
|
// Regular trailing punctuation.
|
|
739
|
-
if (
|
|
740
|
-
code === 33 ||
|
|
741
|
-
code === 34 ||
|
|
742
|
-
code === 39 ||
|
|
743
|
-
code === 41 ||
|
|
744
|
-
code === 42 ||
|
|
745
|
-
code === 44 ||
|
|
746
|
-
code === 46 ||
|
|
747
|
-
code === 58 ||
|
|
748
|
-
code === 59 ||
|
|
749
|
-
code === 63 ||
|
|
750
|
-
code === 95 ||
|
|
751
|
-
code === 126
|
|
752
|
-
) {
|
|
654
|
+
if (code === 33 || code === 34 || code === 39 || code === 41 || code === 42 || code === 44 || code === 46 || code === 58 || code === 59 || code === 63 || code === 95 || code === 126) {
|
|
753
655
|
effects.consume(code);
|
|
754
656
|
return trail;
|
|
755
657
|
}
|
|
@@ -770,13 +672,10 @@ function tokenizeTrail(effects, ok, nok) {
|
|
|
770
672
|
return trailBracketAfter;
|
|
771
673
|
}
|
|
772
674
|
if (
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
markdownLineEndingOrSpace(code) ||
|
|
778
|
-
unicodeWhitespace(code)
|
|
779
|
-
) {
|
|
675
|
+
// `<` is an end.
|
|
676
|
+
code === 60 ||
|
|
677
|
+
// So is whitespace.
|
|
678
|
+
code === null || markdownLineEndingOrSpace(code) || unicodeWhitespace(code)) {
|
|
780
679
|
return ok(code);
|
|
781
680
|
}
|
|
782
681
|
return nok(code);
|
|
@@ -798,13 +697,7 @@ function tokenizeTrail(effects, ok, nok) {
|
|
|
798
697
|
function trailBracketAfter(code) {
|
|
799
698
|
// Whitespace or something that could start a resource or reference is the end.
|
|
800
699
|
// Switch back to trail otherwise.
|
|
801
|
-
if (
|
|
802
|
-
code === null ||
|
|
803
|
-
code === 40 ||
|
|
804
|
-
code === 91 ||
|
|
805
|
-
markdownLineEndingOrSpace(code) ||
|
|
806
|
-
unicodeWhitespace(code)
|
|
807
|
-
) {
|
|
700
|
+
if (code === null || code === 40 || code === 91 || markdownLineEndingOrSpace(code) || unicodeWhitespace(code)) {
|
|
808
701
|
return ok(code);
|
|
809
702
|
}
|
|
810
703
|
return trail(code);
|
|
@@ -908,16 +801,7 @@ function tokenizeEmailDomainDotTrail(effects, ok, nok) {
|
|
|
908
801
|
* @type {Previous}
|
|
909
802
|
*/
|
|
910
803
|
function previousWww(code) {
|
|
911
|
-
return (
|
|
912
|
-
code === null ||
|
|
913
|
-
code === 40 ||
|
|
914
|
-
code === 42 ||
|
|
915
|
-
code === 95 ||
|
|
916
|
-
code === 91 ||
|
|
917
|
-
code === 93 ||
|
|
918
|
-
code === 126 ||
|
|
919
|
-
markdownLineEndingOrSpace(code)
|
|
920
|
-
);
|
|
804
|
+
return code === null || code === 40 || code === 42 || code === 95 || code === 91 || code === 93 || code === 126 || markdownLineEndingOrSpace(code);
|
|
921
805
|
}
|
|
922
806
|
|
|
923
807
|
/**
|
|
@@ -947,13 +831,7 @@ function previousEmail(code) {
|
|
|
947
831
|
* @returns {boolean}
|
|
948
832
|
*/
|
|
949
833
|
function gfmAtext(code) {
|
|
950
|
-
return (
|
|
951
|
-
code === 43 ||
|
|
952
|
-
code === 45 ||
|
|
953
|
-
code === 46 ||
|
|
954
|
-
code === 95 ||
|
|
955
|
-
asciiAlphanumeric(code)
|
|
956
|
-
);
|
|
834
|
+
return code === 43 || code === 45 || code === 46 || code === 95 || asciiAlphanumeric(code);
|
|
957
835
|
}
|
|
958
836
|
|
|
959
837
|
/**
|
|
@@ -965,10 +843,7 @@ function previousUnbalanced(events) {
|
|
|
965
843
|
let result = false;
|
|
966
844
|
while (index--) {
|
|
967
845
|
const token = events[index][1];
|
|
968
|
-
if (
|
|
969
|
-
(token.type === "labelLink" || token.type === "labelImage") &&
|
|
970
|
-
!token._balanced
|
|
971
|
-
) {
|
|
846
|
+
if ((token.type === 'labelLink' || token.type === 'labelImage') && !token._balanced) {
|
|
972
847
|
result = true;
|
|
973
848
|
break;
|
|
974
849
|
}
|
|
@@ -21,3 +21,49 @@ export function createTreeAdapter(): {
|
|
|
21
21
|
* @returns {any}
|
|
22
22
|
*/
|
|
23
23
|
export function parseHtmlFragment(html: string): any;
|
|
24
|
+
/**
|
|
25
|
+
* `fromParse5(tree, { file })`, recursing at most `chunkDepth` levels at a time.
|
|
26
|
+
*
|
|
27
|
+
* Every node a multiple of `chunkDepth` levels down is converted on its own,
|
|
28
|
+
* deepest first, and then stands in its parent's `childNodes` as a placeholder
|
|
29
|
+
* text node carrying the converted subtree's position — the one thing about a
|
|
30
|
+
* child its parent's conversion reads (an unclosed element ends where its last
|
|
31
|
+
* child does). One walk then swaps each placeholder for its subtree. The result
|
|
32
|
+
* is the tree a single `fromParse5` call returns, which
|
|
33
|
+
* `html-tree-adapter.test` holds it to — except that an element the converter
|
|
34
|
+
* would take for a template with no content to take (see
|
|
35
|
+
* {@link CONVERTS_AS_TEMPLATE}) converts as an ordinary element instead of
|
|
36
|
+
* throwing.
|
|
37
|
+
*
|
|
38
|
+
* Mutates `tree`: {@link parseHtmlFragment} owns the parse and drops it after.
|
|
39
|
+
* @param {any} tree
|
|
40
|
+
* @param {VFile} file
|
|
41
|
+
* @param {number} [chunkDepth]
|
|
42
|
+
* @returns {any}
|
|
43
|
+
*/
|
|
44
|
+
export function fromParse5Bounded(tree: any, file: VFile, chunkDepth?: number): any;
|
|
45
|
+
/**
|
|
46
|
+
* How many levels of the tree one `fromParse5` call converts.
|
|
47
|
+
*
|
|
48
|
+
* WHY IT EXISTS — `hast-util-from-parse5` converts by recursion, three frames
|
|
49
|
+
* (`one` → `all` → `element`) per level, so how deep a tree it survives is
|
|
50
|
+
* whatever stack is left, and that depends on the JIT tier: a cold call's
|
|
51
|
+
* interpreted frames are bigger than an optimized one's. ~2500 nested elements
|
|
52
|
+
* overflowed on the first call in a process and converted fine on a later one,
|
|
53
|
+
* so the same document came back withheld or sanitized depending on what had
|
|
54
|
+
* run before it. 128 levels still convert cold on a 100 KB stack, a tenth of
|
|
55
|
+
* Node's default.
|
|
56
|
+
*/
|
|
57
|
+
export const CONVERT_DEPTH: 128;
|
|
58
|
+
/**
|
|
59
|
+
* The deepest a node may sit below the fragment root; a deeper parse throws, so
|
|
60
|
+
* the caller fails closed on it.
|
|
61
|
+
*
|
|
62
|
+
* Conversion no longer limits depth (see {@link CONVERT_DEPTH}), so this is the
|
|
63
|
+
* one place that does, and it answers the same way at every JIT tier. No real
|
|
64
|
+
* document comes near it — Blink's parser flattens anything nested past 512 —
|
|
65
|
+
* and what lies past it is built to cost: parse5's scope checks walk the
|
|
66
|
+
* open-element stack per tag, so parsing is quadratic in depth.
|
|
67
|
+
*/
|
|
68
|
+
export const MAX_HTML_DEPTH: 4096;
|
|
69
|
+
import { VFile } from "vfile";
|
package/types/html.d.mts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* True when an inline style hides its element's text, read as a block box
|
|
3
|
+
* with nothing inherited.
|
|
2
4
|
* @param {string} styleStr
|
|
3
5
|
* @returns {boolean}
|
|
4
6
|
*/
|
|
5
7
|
export function isHiddenStyle(styleStr: string): boolean;
|
|
6
8
|
/**
|
|
7
|
-
* True for an element a rendered page would not show
|
|
8
|
-
*
|
|
9
|
+
* True for an element a rendered page would not show (`hidden` or a hiding
|
|
10
|
+
* inline style, in element context). Takes hast nodes and parseHtmlTag results.
|
|
9
11
|
* @param {any} node
|
|
12
|
+
* @param {any} [parent] the element's parent, when known
|
|
10
13
|
* @returns {boolean}
|
|
11
14
|
*/
|
|
12
|
-
export function isHiddenElement(node: any): boolean;
|
|
15
|
+
export function isHiddenElement(node: any, parent?: any): boolean;
|
|
13
16
|
/**
|
|
14
17
|
* @param {string} htmlValue
|
|
15
18
|
* @returns {string | null}
|
|
@@ -119,10 +122,9 @@ export function checkExfilUrl(url: string, options?: {
|
|
|
119
122
|
*/
|
|
120
123
|
export function urlHost(url: string): string;
|
|
121
124
|
/**
|
|
122
|
-
* Layer 3: report data-exfil-shaped URLs
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* surfaces the threats as a warning.
|
|
125
|
+
* Layer 3: report data-exfil-shaped URLs among the ones {@link collectUrls}
|
|
126
|
+
* reads. Detection only — the text is never modified; the caller surfaces the
|
|
127
|
+
* threats as a warning.
|
|
126
128
|
*
|
|
127
129
|
* `autoFetched` marks a threat that needs no deliberate act to fire — a
|
|
128
130
|
* rendered image, a stylesheet, a form target, a meta refresh — as opposed to a
|
|
@@ -161,6 +163,40 @@ export const REPORTED_TAGS: Set<string>;
|
|
|
161
163
|
export const HIDDEN_PLACEHOLDER: "[hidden HTML removed";
|
|
162
164
|
export const COMMENT_PLACEHOLDER: "[HTML comment removed";
|
|
163
165
|
export const DATA_URI_LENGTH_THRESHOLD: 4096;
|
|
166
|
+
export type StyleDeclaration = {
|
|
167
|
+
property: string;
|
|
168
|
+
value: any;
|
|
169
|
+
important: boolean;
|
|
170
|
+
index: number;
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* `winnerOf`: the declaration applied (its own, a shorthand's or `all`'s);
|
|
174
|
+
* `nodeOf`/`textOf`: its value when it is the property's OWN (else null/"");
|
|
175
|
+
* `exhausted`: a lookup ran out of {@link VALIDATION_BUDGET}.
|
|
176
|
+
*/
|
|
177
|
+
export type Cascade = {
|
|
178
|
+
winnerOf: (property: string) => StyleDeclaration | null;
|
|
179
|
+
nodeOf: (property: string) => any;
|
|
180
|
+
textOf: (property: string) => string;
|
|
181
|
+
exhausted: boolean;
|
|
182
|
+
};
|
|
183
|
+
export type InheritedHide = "font-size" | "visibility" | "color" | "text-indent";
|
|
184
|
+
/**
|
|
185
|
+
* `color` and a concrete `-webkit-text-fill-color` (`textFill`) inherit
|
|
186
|
+
* SEPARATELY, so a child's `color` does not undo an inherited transparent fill.
|
|
187
|
+
*/
|
|
188
|
+
export type ColorState = {
|
|
189
|
+
color: string;
|
|
190
|
+
textFill: string;
|
|
191
|
+
backdrop: string;
|
|
192
|
+
imageLayer: boolean;
|
|
193
|
+
paintedVisible: boolean;
|
|
194
|
+
};
|
|
195
|
+
export type RegionState = {
|
|
196
|
+
kinds: Set<InheritedHide>;
|
|
197
|
+
color: ColorState;
|
|
198
|
+
};
|
|
199
|
+
export type RegionVerdict = "hidden" | "neutral" | "visible" | "mixed";
|
|
164
200
|
export type CollectedUrl = {
|
|
165
201
|
url: string;
|
|
166
202
|
isImage: boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** @type {readonly UrlAttribute[]} */
|
|
2
|
+
export const URL_ATTRIBUTES: readonly UrlAttribute[];
|
|
3
|
+
export type UrlAttribute = {
|
|
4
|
+
attribute: string;
|
|
5
|
+
property: string;
|
|
6
|
+
grammar: "url" | "urls" | "srcset" | "refresh" | "css";
|
|
7
|
+
elements: readonly string[] | null;
|
|
8
|
+
context: "resource" | "form" | "refresh";
|
|
9
|
+
fetched: boolean;
|
|
10
|
+
notFetchedOn: readonly string[];
|
|
11
|
+
};
|