@markuplint/types 5.0.0-rc.2 → 5.0.0-rc.5
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 +38 -0
- package/README.md +83 -75
- package/lib/check-multi-types.d.ts +2 -9
- package/lib/check-multi-types.js +2 -9
- package/lib/css-defs.d.ts +0 -8
- package/lib/css-defs.js +0 -8
- package/lib/css-overrides.d.ts +2 -5
- package/lib/css-overrides.js +2 -5
- package/lib/css-syntax.d.ts +0 -11
- package/lib/css-syntax.js +6 -18
- package/lib/css-tokenizers.d.ts +0 -6
- package/lib/css-tokenizers.js +0 -6
- package/lib/debug.d.ts +0 -3
- package/lib/debug.js +0 -3
- package/lib/defs.d.ts +0 -8
- package/lib/defs.js +308 -37
- package/lib/directive.d.ts +0 -14
- package/lib/directive.js +0 -14
- package/lib/enum.d.ts +0 -11
- package/lib/enum.js +0 -11
- package/lib/keyword-type.d.ts +0 -13
- package/lib/keyword-type.js +0 -13
- package/lib/list.d.ts +0 -13
- package/lib/list.js +0 -13
- package/lib/match-result.d.ts +0 -21
- package/lib/match-result.js +0 -21
- package/lib/number.d.ts +0 -12
- package/lib/number.js +0 -12
- package/lib/primitive/is-float.d.ts +3 -4
- package/lib/primitive/is-float.js +3 -4
- package/lib/primitive/is-int.d.ts +0 -5
- package/lib/primitive/is-int.js +0 -5
- package/lib/primitive/is-non-zero-uint.d.ts +0 -6
- package/lib/primitive/is-non-zero-uint.js +0 -6
- package/lib/primitive/is-quantity.d.ts +0 -8
- package/lib/primitive/is-quantity.js +0 -8
- package/lib/primitive/is-uint.d.ts +0 -6
- package/lib/primitive/is-uint.js +0 -6
- package/lib/primitive/range.d.ts +0 -8
- package/lib/primitive/range.js +0 -8
- package/lib/primitive/split-unit.d.ts +0 -6
- package/lib/primitive/split-unit.js +0 -6
- package/lib/rfc/is-bcp-47.d.ts +24 -1
- package/lib/rfc/is-bcp-47.js +88 -3
- package/lib/token/token-collection.d.ts +1 -111
- package/lib/token/token-collection.js +0 -108
- package/lib/token/token.d.ts +1 -73
- package/lib/token/token.js +0 -72
- package/lib/token/types.d.ts +0 -9
- package/lib/types.schema.d.ts +1 -1
- package/lib/w3c/check-content-security-policy.d.ts +18 -0
- package/lib/w3c/check-content-security-policy.js +363 -0
- package/lib/w3c/check-serialized-permissions-policy.d.ts +0 -2
- package/lib/w3c/check-serialized-permissions-policy.js +0 -2
- package/lib/whatwg/check-autocomplete.d.ts +26 -9
- package/lib/whatwg/check-autocomplete.js +42 -15
- package/lib/whatwg/check-datetime/date-string-with-optional-time.d.ts +11 -0
- package/lib/whatwg/check-datetime/date-string-with-optional-time.js +16 -0
- package/lib/whatwg/check-datetime/date-string.d.ts +0 -2
- package/lib/whatwg/check-datetime/date-string.js +0 -2
- package/lib/whatwg/check-datetime/datetime-tokens.d.ts +0 -13
- package/lib/whatwg/check-datetime/datetime-tokens.js +0 -16
- package/lib/whatwg/check-datetime/duration-string.d.ts +0 -4
- package/lib/whatwg/check-datetime/duration-string.js +0 -4
- package/lib/whatwg/check-datetime/global-date-and-time-string.d.ts +0 -2
- package/lib/whatwg/check-datetime/global-date-and-time-string.js +4 -4
- package/lib/whatwg/check-datetime/index.d.ts +0 -5
- package/lib/whatwg/check-datetime/index.js +0 -5
- package/lib/whatwg/check-datetime/local-date-and-time-string.d.ts +0 -4
- package/lib/whatwg/check-datetime/local-date-and-time-string.js +0 -4
- package/lib/whatwg/check-datetime/month-string.d.ts +0 -2
- package/lib/whatwg/check-datetime/month-string.js +0 -2
- package/lib/whatwg/check-datetime/time-string.d.ts +0 -2
- package/lib/whatwg/check-datetime/time-string.js +0 -2
- package/lib/whatwg/check-datetime/time-zone-offset-string.d.ts +0 -8
- package/lib/whatwg/check-datetime/time-zone-offset-string.js +0 -8
- package/lib/whatwg/check-datetime/week-string.d.ts +0 -2
- package/lib/whatwg/check-datetime/week-string.js +0 -2
- package/lib/whatwg/check-datetime/year-string.d.ts +0 -2
- package/lib/whatwg/check-datetime/year-string.js +0 -2
- package/lib/whatwg/check-datetime/yearless-date-string.d.ts +0 -2
- package/lib/whatwg/check-datetime/yearless-date-string.js +0 -2
- package/lib/whatwg/check-email.js +0 -2
- package/lib/whatwg/check-http-equiv-content-type.d.ts +12 -0
- package/lib/whatwg/check-http-equiv-content-type.js +32 -0
- package/lib/whatwg/check-http-equiv-refresh.d.ts +21 -0
- package/lib/whatwg/check-http-equiv-refresh.js +106 -0
- package/lib/whatwg/check-media-query-list.d.ts +61 -0
- package/lib/whatwg/check-media-query-list.js +484 -0
- package/lib/whatwg/check-mime-type.d.ts +0 -4
- package/lib/whatwg/check-mime-type.js +55 -3
- package/lib/whatwg/check-simple-color.d.ts +2 -4
- package/lib/whatwg/check-simple-color.js +2 -4
- package/lib/whatwg/check-url.d.ts +15 -7
- package/lib/whatwg/check-url.js +333 -25
- package/lib/whatwg/is-abs-url.d.ts +0 -2
- package/lib/whatwg/is-abs-url.js +0 -2
- package/lib/whatwg/is-browser-context-name.d.ts +0 -2
- package/lib/whatwg/is-browser-context-name.js +0 -2
- package/lib/whatwg/is-itemprop-name.d.ts +0 -2
- package/lib/whatwg/is-itemprop-name.js +0 -2
- package/lib/whatwg/is-navigable-target-name.d.ts +0 -2
- package/lib/whatwg/is-navigable-target-name.js +0 -2
- package/package.json +6 -5
- package/types.schema.json +9 -0
- package/ARCHITECTURE.ja.md +0 -256
- package/ARCHITECTURE.md +0 -256
- package/SKILL.md +0 -100
- package/docs/check-pipeline.ja.md +0 -494
- package/docs/check-pipeline.md +0 -494
- package/docs/token-system.ja.md +0 -584
- package/docs/token-system.md +0 -584
- package/docs/type-system.ja.md +0 -623
- package/docs/type-system.md +0 -623
- package/docs/validators.ja.md +0 -536
- package/docs/validators.md +0 -536
package/lib/whatwg/check-url.js
CHANGED
|
@@ -1,11 +1,135 @@
|
|
|
1
1
|
import { log } from '../debug.js';
|
|
2
2
|
import { matched, unmatched } from '../match-result.js';
|
|
3
|
+
// cspell:ignore FDEF FFFE XFFFE XFFFF AFFFE AFFFF BFFFE BFFFF CFFFE CFFFF DFFFE DFFFF EFFFE EFFFF FFFFE FFFFF
|
|
3
4
|
/**
|
|
4
5
|
* Dummy base URL for resolving relative URLs.
|
|
5
6
|
* Used only for syntax validation — the actual base URL is irrelevant.
|
|
6
7
|
* Matches the approach used by nu-html-checker (galimatias).
|
|
7
8
|
*/
|
|
8
9
|
const DUMMY_BASE = 'http://example.org/foo/bar';
|
|
10
|
+
/**
|
|
11
|
+
* Pattern: a URL whose scheme is one of the special schemes (case-insensitive).
|
|
12
|
+
*
|
|
13
|
+
* Special schemes per URL Living Standard §3.5: `ftp`, `file`, `http`,
|
|
14
|
+
* `https`, `ws`, `wss`.
|
|
15
|
+
*
|
|
16
|
+
* @see https://url.spec.whatwg.org/#special-scheme
|
|
17
|
+
*/
|
|
18
|
+
const SPECIAL_SCHEME_PREFIX = /^(?:ftp|file|https?|wss?):/i;
|
|
19
|
+
/**
|
|
20
|
+
* Pattern: a special-scheme URL that is **not** followed by `//`.
|
|
21
|
+
*
|
|
22
|
+
* Triggers `special-scheme-missing-following-solidus` for `ftp`/`http`/`https`/`ws`/`wss`
|
|
23
|
+
* (and the file-scheme variant for `file:`).
|
|
24
|
+
*
|
|
25
|
+
* Examples that match: `http:foo`, `https:/foo`, `file:foo`, `file:/foo`.
|
|
26
|
+
*
|
|
27
|
+
* @see https://url.spec.whatwg.org/#special-scheme-missing-following-solidus
|
|
28
|
+
* @see https://url.spec.whatwg.org/#file-scheme-missing-following-solidus
|
|
29
|
+
*/
|
|
30
|
+
const SPECIAL_SCHEME_MISSING_SOLIDUS = /^(?:ftp|file|https?|wss?):(?!\/\/)/i;
|
|
31
|
+
/**
|
|
32
|
+
* Pattern: a special-scheme URL with a single slash where two are required.
|
|
33
|
+
*
|
|
34
|
+
* Examples that match: `http:/foo`, `file:/C:/foo`.
|
|
35
|
+
*
|
|
36
|
+
* Combined with `SPECIAL_SCHEME_MISSING_SOLIDUS`, this also rejects the
|
|
37
|
+
* `file:/single-slash` form even though `file:/` is **not** caught by the
|
|
38
|
+
* "missing following solidus" rule alone (the parser tolerates exactly one
|
|
39
|
+
* solidus before transitioning into the file slash state — but nu-validator
|
|
40
|
+
* reports it as a validation error against the strict "valid URL string"
|
|
41
|
+
* production).
|
|
42
|
+
*
|
|
43
|
+
* @see https://url.spec.whatwg.org/#file-scheme-missing-following-solidus
|
|
44
|
+
*/
|
|
45
|
+
const SPECIAL_SCHEME_SINGLE_SLASH = /^(?:ftp|file|https?|wss?):\/(?!\/)/i;
|
|
46
|
+
/**
|
|
47
|
+
* Pattern: special-scheme URL (or scheme-relative URL) whose authority
|
|
48
|
+
* component contains an `@` sign — i.e., the URL declares userinfo even
|
|
49
|
+
* when that userinfo is empty (`http://@host`).
|
|
50
|
+
*
|
|
51
|
+
* URL LS reports `invalid-credentials` whenever the input has an `@` in
|
|
52
|
+
* the authority, regardless of whether `username`/`password` are non-empty
|
|
53
|
+
* after parsing. The post-parse `parsed.username/password` check below
|
|
54
|
+
* does not catch the empty-userinfo variant on its own, so we surface it
|
|
55
|
+
* here on the raw input.
|
|
56
|
+
*
|
|
57
|
+
* @see https://url.spec.whatwg.org/#invalid-credentials
|
|
58
|
+
*/
|
|
59
|
+
const SPECIAL_SCHEME_AUTHORITY_HAS_AT_SIGN = /^(?:(?:ftp|file|https?|wss?):)?\/\/[^/?#]*@/i;
|
|
60
|
+
/**
|
|
61
|
+
* Pattern: more than one `#` in the URL.
|
|
62
|
+
*
|
|
63
|
+
* URL LS treats the first `#` as the fragment delimiter; any subsequent
|
|
64
|
+
* `#` is auto-percent-encoded by `new URL()` but reported as
|
|
65
|
+
* `invalid-URL-unit` because `#` is not a URL code point inside the
|
|
66
|
+
* fragment grammar of a "valid URL string".
|
|
67
|
+
*
|
|
68
|
+
* Example caught: `http://example.com/path#f#g`.
|
|
69
|
+
*
|
|
70
|
+
* The regex has no `s` flag, so `.` does not match a newline. This is
|
|
71
|
+
* deliberate and depends on `ILLEGAL_WHITESPACE` (TAB/LF/CR) running
|
|
72
|
+
* earlier in `checkURL` — a `#…\n…#` input is already rejected before
|
|
73
|
+
* this regex is consulted. If you reorder the checks in `checkURL`, add
|
|
74
|
+
* the `s` flag here.
|
|
75
|
+
*
|
|
76
|
+
* @see https://url.spec.whatwg.org/#invalid-url-unit
|
|
77
|
+
*/
|
|
78
|
+
const MULTIPLE_HASH = /#.*#/;
|
|
79
|
+
/**
|
|
80
|
+
* Pattern: a URL whose path / query / fragment contains `[` or `]`.
|
|
81
|
+
*
|
|
82
|
+
* Square brackets are URL code points **only** in the host position of a
|
|
83
|
+
* special-scheme URL, where they delimit an IPv6 address (e.g.,
|
|
84
|
+
* `http://[::1]/`). Outside the authority component they are not URL code
|
|
85
|
+
* points and trigger `invalid-URL-unit`.
|
|
86
|
+
*
|
|
87
|
+
* Detection: strip the authority portion (everything between `://` and the
|
|
88
|
+
* first `/`/`?`/`#`) and check the remainder for any `[` or `]`. Relative
|
|
89
|
+
* URLs without `://` are checked as-is.
|
|
90
|
+
*
|
|
91
|
+
* Examples caught:
|
|
92
|
+
* - `[61:24:74]:98` — relative URL with IPv6-looking brackets in path
|
|
93
|
+
* - `http://example.com/path[a]` — brackets in path of special-scheme URL
|
|
94
|
+
* - `data:[foo]` — brackets in opaque path of non-special scheme
|
|
95
|
+
*
|
|
96
|
+
* @see https://url.spec.whatwg.org/#invalid-url-unit
|
|
97
|
+
* @see https://url.spec.whatwg.org/#url-code-points
|
|
98
|
+
*/
|
|
99
|
+
function hasBracketsOutsideHost(value) {
|
|
100
|
+
if (!/[[\]]/.test(value)) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
// Strip the authority component (host[:port][@userinfo]) if present.
|
|
104
|
+
// The optional scheme prefix matches both special and non-special schemes;
|
|
105
|
+
// the `[^/?#]*` swallows everything up to the first path/query/fragment
|
|
106
|
+
// delimiter, which includes a legitimate `[::1]` IPv6 host.
|
|
107
|
+
const withoutAuthority = value.replace(/^(?:[a-z][a-z0-9+.-]*:)?\/\/[^/?#]*/i, '');
|
|
108
|
+
return /[[\]]/.test(withoutAuthority);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Pattern: a `data:` URL with no `,` separator.
|
|
112
|
+
*
|
|
113
|
+
* RFC 2397 requires the form `data:[<mediatype>][;base64],<data>`; the
|
|
114
|
+
* `,` is mandatory. `data:` URLs without a comma (e.g., `data:/example.com/`,
|
|
115
|
+
* `data:`) are syntactically invalid even though `URL.canParse` accepts them
|
|
116
|
+
* as opaque-path non-special-scheme URLs.
|
|
117
|
+
*
|
|
118
|
+
* @see https://datatracker.ietf.org/doc/html/rfc2397
|
|
119
|
+
*/
|
|
120
|
+
const DATA_URL_MISSING_COMMA = /^data:[^,]*$/i;
|
|
121
|
+
/**
|
|
122
|
+
* Pattern: file-scheme URL with a Windows drive letter that uses `|` instead of `:`.
|
|
123
|
+
*
|
|
124
|
+
* URL LS §6 says `C|` is auto-corrected to `C:` and triggers a
|
|
125
|
+
* `file-invalid-Windows-drive-letter` validation error. nu-validator surfaces
|
|
126
|
+
* this and so do we.
|
|
127
|
+
*
|
|
128
|
+
* Matches `file:C|...`, `file:/C|...`, `file://C|...`, `file:///C|...`, etc.
|
|
129
|
+
*
|
|
130
|
+
* @see https://url.spec.whatwg.org/#file-invalid-windows-drive-letter
|
|
131
|
+
*/
|
|
132
|
+
const FILE_WINDOWS_DRIVE_LETTER_WITH_BAR = /^file:\/{0,3}[a-z]\|/i;
|
|
9
133
|
/**
|
|
10
134
|
* Characters that are illegal in URLs per WHATWG URL Standard.
|
|
11
135
|
* Tabs and newlines are stripped during parsing but are validation errors.
|
|
@@ -19,34 +143,169 @@ const ILLEGAL_WHITESPACE = /[\t\n\r]/;
|
|
|
19
143
|
*/
|
|
20
144
|
const MALFORMED_PERCENT = /%(?![0-9A-F]{2})/i;
|
|
21
145
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
146
|
+
* Forbidden code points per HTML Living Standard and URL spec.
|
|
147
|
+
*
|
|
148
|
+
* Covers:
|
|
149
|
+
* - C0 controls (U+0000–U+001F) except TAB/LF/CR which are caught by
|
|
150
|
+
* ILLEGAL_WHITESPACE
|
|
151
|
+
* - DEL (U+007F) and C1 controls (U+0080–U+009F)
|
|
152
|
+
* - Unicode noncharacters:
|
|
153
|
+
* - BMP: U+FDD0–U+FDEF, U+FFFE, U+FFFF
|
|
154
|
+
* - Supplementary planes: U+XFFFE / U+XFFFF for X = 1..10 (hex)
|
|
155
|
+
*
|
|
156
|
+
* Requires the `u` flag for code points above U+FFFF.
|
|
157
|
+
*
|
|
158
|
+
* @see https://infra.spec.whatwg.org/#noncharacter
|
|
159
|
+
* @see https://html.spec.whatwg.org/multipage/parsing.html#preprocessing-the-input-stream
|
|
24
160
|
*/
|
|
161
|
+
const FORBIDDEN_CODE_POINT =
|
|
25
162
|
// eslint-disable-next-line no-control-regex
|
|
26
|
-
|
|
163
|
+
/[\u0000-\u0008\v\u000E-\u001F\u007F-\u009F\uFDD0-\uFDEF\uFFFE\uFFFF\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u;
|
|
164
|
+
/**
|
|
165
|
+
* Strips only HTML ASCII whitespace (TAB/LF/FF/CR/SPACE) from both ends.
|
|
166
|
+
*
|
|
167
|
+
* Unlike `String.prototype.trim()`, this does not strip U+000B (vertical tab)
|
|
168
|
+
* or any other Unicode whitespace, so forbidden code points at URL
|
|
169
|
+
* boundaries are preserved for the forbidden-code-point check below.
|
|
170
|
+
*
|
|
171
|
+
* @see https://infra.spec.whatwg.org/#strip-leading-and-trailing-ascii-whitespace
|
|
172
|
+
*/
|
|
173
|
+
function stripAsciiWhitespace(value) {
|
|
174
|
+
return value.replaceAll(/^[\t\n\f\r ]+|[\t\n\f\r ]+$/g, '');
|
|
175
|
+
}
|
|
27
176
|
/**
|
|
28
177
|
* Unencoded space in URL body (after trimming leading/trailing spaces).
|
|
29
178
|
* `new URL()` accepts spaces by percent-encoding them, but the WHATWG spec
|
|
30
179
|
* considers them a validation error. nu-validator rejects them.
|
|
31
180
|
*/
|
|
32
181
|
const UNENCODED_SPACE = / /;
|
|
182
|
+
/**
|
|
183
|
+
* Pattern: a dot-separated host label written in hex (`0x…`/`0X…`) or
|
|
184
|
+
* leading-zero octal notation (a `0` immediately followed by another digit).
|
|
185
|
+
*
|
|
186
|
+
* The URL Standard's IPv4 number parser sets `validationError` to true for
|
|
187
|
+
* either form even though the number still parses successfully — `new URL()`
|
|
188
|
+
* silently normalizes such labels to plain decimal (e.g. `192.0x00A80001` →
|
|
189
|
+
* `192.168.0.1`), so this must be tested against the pre-normalization host.
|
|
190
|
+
*
|
|
191
|
+
* @see https://url.spec.whatwg.org/#ipv4-number-parser
|
|
192
|
+
*/
|
|
193
|
+
const IPV4_NON_DECIMAL_PART = /^0(?:x[0-9a-f]*|\d+)$/i;
|
|
194
|
+
/**
|
|
195
|
+
* Pattern: a host label that "ends in a number" per the URL Standard —
|
|
196
|
+
* either plain ASCII digits, or a `0x`/`0X`-prefixed hex run (which is
|
|
197
|
+
* still a number, just written non-decimally).
|
|
198
|
+
*
|
|
199
|
+
* Gates {@link hasIPv4NonDecimalPart} so it never fires on an ordinary
|
|
200
|
+
* domain (`example.com`) merely because an earlier label happens to
|
|
201
|
+
* contain digits — the IPv4 parser only runs when the domain's last
|
|
202
|
+
* label looks numeric in the first place.
|
|
203
|
+
*
|
|
204
|
+
* @see https://url.spec.whatwg.org/#ends-in-a-number-checker
|
|
205
|
+
*/
|
|
206
|
+
const IPV4_LAST_PART_NUMERIC = /^(?:\d+|0x[0-9a-f]*)$/i;
|
|
207
|
+
/**
|
|
208
|
+
* Extracts the raw (pre-percent-decode, pre-IDNA) host component from a
|
|
209
|
+
* special-scheme or scheme-relative URL's authority, or `null` when there
|
|
210
|
+
* is no authority to extract (non-special scheme, or a relative URL with
|
|
211
|
+
* no `//`) or the host is an IPv6 literal (`[::1]`, never IPv4-parsed).
|
|
212
|
+
*/
|
|
213
|
+
function extractRawHost(trimmed) {
|
|
214
|
+
const authorityMatch = /^(?:[a-z][a-z0-9+.-]*:)?\/\/([^/?#]*)/i.exec(trimmed);
|
|
215
|
+
if (!authorityMatch) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
const authority = authorityMatch[1] ?? '';
|
|
219
|
+
const host = authority.includes('@') ? authority.slice(authority.lastIndexOf('@') + 1) : authority;
|
|
220
|
+
if (host === '' || host.startsWith('[')) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
const portIndex = host.indexOf(':');
|
|
224
|
+
return portIndex === -1 ? host : host.slice(0, portIndex);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Detects an `IPv4-non-decimal-part` validation error on a special-scheme
|
|
228
|
+
* URL's host, before `new URL()` normalizes it away.
|
|
229
|
+
*
|
|
230
|
+
* Mirrors the URL Standard's host parser preprocessing (percent-decode,
|
|
231
|
+
* then the Unicode mapping IDNA's "domain to ASCII" step performs — here
|
|
232
|
+
* approximated with `String.prototype.normalize('NFKC')`, which folds
|
|
233
|
+
* fullwidth digits/letters to their ASCII equivalents) before splitting on
|
|
234
|
+
* `.` and testing each label. `IPV4_LAST_PART_NUMERIC` gates the whole
|
|
235
|
+
* check on the domain "ending in a number", matching the spec's own gate
|
|
236
|
+
* for whether the IPv4 parser runs at all.
|
|
237
|
+
*
|
|
238
|
+
* @see https://url.spec.whatwg.org/#concept-host-parser
|
|
239
|
+
* @see https://url.spec.whatwg.org/#concept-ipv4-parser
|
|
240
|
+
*/
|
|
241
|
+
function hasIPv4NonDecimalPart(trimmed) {
|
|
242
|
+
const rawHost = extractRawHost(trimmed);
|
|
243
|
+
if (rawHost == null) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
let host;
|
|
247
|
+
try {
|
|
248
|
+
host = decodeURIComponent(rawHost).normalize('NFKC');
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
let labels = host.split('.');
|
|
254
|
+
if (labels.length > 1 && labels.at(-1) === '') {
|
|
255
|
+
labels = labels.slice(0, -1);
|
|
256
|
+
}
|
|
257
|
+
const lastLabel = labels.at(-1) ?? '';
|
|
258
|
+
if (!IPV4_LAST_PART_NUMERIC.test(lastLabel)) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
return labels.some(label => IPV4_NON_DECIMAL_PART.test(label));
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Returns `true` when `value` should be treated as a special-scheme URL —
|
|
265
|
+
* either it has an explicit special scheme prefix, or it is scheme-relative
|
|
266
|
+
* (no scheme, so it inherits the document's base URL, which in HTML attribute
|
|
267
|
+
* context is overwhelmingly a special-scheme URL).
|
|
268
|
+
*
|
|
269
|
+
* Used to gate validations that are only meaningful for special schemes
|
|
270
|
+
* (e.g., backslash → invalid-reverse-solidus).
|
|
271
|
+
*
|
|
272
|
+
* @see https://url.spec.whatwg.org/#special-url
|
|
273
|
+
*/
|
|
274
|
+
function isSpecialOrSchemeless(value) {
|
|
275
|
+
if (SPECIAL_SCHEME_PREFIX.test(value)) {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
// A URL that starts with a non-special scheme like `mailto:`, `data:`,
|
|
279
|
+
// `javascript:`, `tel:`, etc. is not a special URL.
|
|
280
|
+
// We only need to detect the absence of a scheme; the explicit
|
|
281
|
+
// non-special-scheme cases are handled by the negative match here.
|
|
282
|
+
return !/^[a-z][a-z0-9+.-]*:/i.test(value);
|
|
283
|
+
}
|
|
33
284
|
/**
|
|
34
285
|
* Validates a URL string (potentially surrounded by spaces) per the WHATWG
|
|
35
286
|
* URL Standard. Accepts both absolute and relative URLs.
|
|
36
287
|
*
|
|
37
|
-
* Uses `new URL()` for structural parsing and
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* -
|
|
288
|
+
* Uses `new URL()` for structural parsing and surfaces the validation errors
|
|
289
|
+
* that `new URL()` silently accepts but nu-validator (and the URL LS) report.
|
|
290
|
+
*
|
|
291
|
+
* Categories caught:
|
|
292
|
+
*
|
|
293
|
+
* - **invalid-URL-unit** (forbidden code points, malformed percent-encoding,
|
|
294
|
+
* unencoded space, tab/CR/LF)
|
|
295
|
+
* - **invalid-reverse-solidus** (`\` in a special-scheme URL)
|
|
296
|
+
* - **special-scheme-missing-following-solidus** (`http:foo`, `file:bar`)
|
|
297
|
+
* and `file-scheme-missing-following-solidus` / single-slash variants
|
|
298
|
+
* - **file-invalid-Windows-drive-letter** (`file:///C|/foo`)
|
|
299
|
+
* - **invalid-credentials** (`http://user:pass@example.com`)
|
|
300
|
+
* - **IPv4-non-decimal-part** (`http://192.0x00A80001`, hex/octal host labels)
|
|
301
|
+
* - any URL that fails `new URL()` parsing (even with a dummy base)
|
|
43
302
|
*
|
|
44
303
|
* @see https://html.spec.whatwg.org/multipage/urls-and-fetching.html#valid-url-potentially-surrounded-by-spaces
|
|
45
|
-
* @see https://url.spec.whatwg.org/#url-
|
|
304
|
+
* @see https://url.spec.whatwg.org/#url-parsing
|
|
46
305
|
*/
|
|
47
306
|
export const checkURL = () => function checkURL(value) {
|
|
48
307
|
log('CHECK: url');
|
|
49
|
-
const trimmed = value
|
|
308
|
+
const trimmed = stripAsciiWhitespace(value);
|
|
50
309
|
// Empty URL is valid for some attributes (e.g., <a href="">)
|
|
51
310
|
// The spec says "valid URL potentially surrounded by spaces"
|
|
52
311
|
// and the empty string resolves to the document's URL.
|
|
@@ -57,8 +316,8 @@ export const checkURL = () => function checkURL(value) {
|
|
|
57
316
|
if (ILLEGAL_WHITESPACE.test(trimmed)) {
|
|
58
317
|
return unmatched(trimmed, 'unexpected-token');
|
|
59
318
|
}
|
|
60
|
-
// Check for C0
|
|
61
|
-
if (
|
|
319
|
+
// Check for forbidden code points (C0/C1 controls, noncharacters)
|
|
320
|
+
if (FORBIDDEN_CODE_POINT.test(trimmed)) {
|
|
62
321
|
return unmatched(trimmed, 'unexpected-token');
|
|
63
322
|
}
|
|
64
323
|
// Check for malformed percent-encoding
|
|
@@ -69,21 +328,70 @@ export const checkURL = () => function checkURL(value) {
|
|
|
69
328
|
if (UNENCODED_SPACE.test(trimmed)) {
|
|
70
329
|
return unmatched(trimmed, 'unexpected-token');
|
|
71
330
|
}
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
331
|
+
// URL LS validation errors that `new URL()` silently auto-corrects:
|
|
332
|
+
// special-scheme-missing-following-solidus / file-scheme-missing-following-solidus
|
|
333
|
+
// (e.g., `http:foo`, `file:bar`, `file:` alone)
|
|
334
|
+
if (SPECIAL_SCHEME_MISSING_SOLIDUS.test(trimmed)) {
|
|
335
|
+
return unmatched(trimmed, 'unexpected-token');
|
|
76
336
|
}
|
|
77
|
-
|
|
78
|
-
|
|
337
|
+
// Single-slash variant: `http:/foo`, `file:/foo`
|
|
338
|
+
// (URL LS recovers but flags it as a validation error.)
|
|
339
|
+
if (SPECIAL_SCHEME_SINGLE_SLASH.test(trimmed)) {
|
|
340
|
+
return unmatched(trimmed, 'unexpected-token');
|
|
79
341
|
}
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return matched();
|
|
342
|
+
// file-invalid-Windows-drive-letter (e.g., `file:///C|/foo`)
|
|
343
|
+
if (FILE_WINDOWS_DRIVE_LETTER_WITH_BAR.test(trimmed)) {
|
|
344
|
+
return unmatched(trimmed, 'unexpected-token');
|
|
84
345
|
}
|
|
85
|
-
|
|
86
|
-
|
|
346
|
+
// invalid-reverse-solidus: `\` in a special-scheme (or scheme-relative) URL.
|
|
347
|
+
// Non-special schemes (e.g., `data:`, `mailto:`) allow backslash in their
|
|
348
|
+
// opaque path, so we gate this on the scheme.
|
|
349
|
+
if (isSpecialOrSchemeless(trimmed) && trimmed.includes('\\')) {
|
|
350
|
+
return unmatched(trimmed, 'unexpected-token');
|
|
351
|
+
}
|
|
352
|
+
// invalid-credentials with empty userinfo: `http://@host`, `//@host`.
|
|
353
|
+
// `new URL()` parses these into empty username/password fields, so the
|
|
354
|
+
// post-parse property check below would miss them.
|
|
355
|
+
if (SPECIAL_SCHEME_AUTHORITY_HAS_AT_SIGN.test(trimmed)) {
|
|
87
356
|
return unmatched(trimmed, 'unexpected-token');
|
|
88
357
|
}
|
|
358
|
+
// More than one `#` in the URL — multiple hash delimiters.
|
|
359
|
+
// Only meaningful for special-scheme / scheme-relative URLs; non-special
|
|
360
|
+
// schemes (`data:`, `javascript:`) treat everything before/after as
|
|
361
|
+
// opaque content where extra `#` is the user's own data.
|
|
362
|
+
if (isSpecialOrSchemeless(trimmed) && MULTIPLE_HASH.test(trimmed)) {
|
|
363
|
+
return unmatched(trimmed, 'unexpected-token');
|
|
364
|
+
}
|
|
365
|
+
// invalid-URL-unit: `[` or `]` outside the IPv6 host position.
|
|
366
|
+
if (hasBracketsOutsideHost(trimmed)) {
|
|
367
|
+
return unmatched(trimmed, 'unexpected-token');
|
|
368
|
+
}
|
|
369
|
+
// data: URL without a comma — RFC 2397 violation.
|
|
370
|
+
if (DATA_URL_MISSING_COMMA.test(trimmed)) {
|
|
371
|
+
return unmatched(trimmed, 'unexpected-token');
|
|
372
|
+
}
|
|
373
|
+
// IPv4-non-decimal-part: a host label written in hex/octal notation.
|
|
374
|
+
// Only special-scheme (or scheme-relative) URLs have their host run
|
|
375
|
+
// through the IPv4 parser; non-special schemes have an opaque host.
|
|
376
|
+
if (isSpecialOrSchemeless(trimmed) && hasIPv4NonDecimalPart(trimmed)) {
|
|
377
|
+
return unmatched(trimmed, 'unexpected-token');
|
|
378
|
+
}
|
|
379
|
+
// Structural parse — `URL.canParse` is preferred over `new URL()` +
|
|
380
|
+
// try/catch because it returns a boolean instead of throwing, which
|
|
381
|
+
// would otherwise need an `isFatalError` guard per the project's
|
|
382
|
+
// Three-Tier Error policy. (Available since Node 19; the package
|
|
383
|
+
// requires Node 22+.)
|
|
384
|
+
//
|
|
385
|
+
// invalid-credentials with a non-empty userinfo (`http://user:pass@`)
|
|
386
|
+
// is also caught structurally here — the regex above
|
|
387
|
+
// (`SPECIAL_SCHEME_AUTHORITY_HAS_AT_SIGN`) already covers every
|
|
388
|
+
// `@`-in-authority case (including the empty-userinfo variant), so a
|
|
389
|
+
// post-parse `parsed.username/password` check would be unreachable.
|
|
390
|
+
if (URL.canParse(trimmed)) {
|
|
391
|
+
return matched();
|
|
392
|
+
}
|
|
393
|
+
if (URL.canParse(trimmed, DUMMY_BASE)) {
|
|
394
|
+
return matched();
|
|
395
|
+
}
|
|
396
|
+
return unmatched(trimmed, 'unexpected-token');
|
|
89
397
|
};
|
package/lib/whatwg/is-abs-url.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { FormattedPrimitiveTypeCreator } from '../types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Checks whether a string is a valid browsing context name.
|
|
4
|
-
*
|
|
5
3
|
* @see https://html.spec.whatwg.org/multipage/browsers.html#valid-browsing-context-name
|
|
6
4
|
*
|
|
7
5
|
* > A valid browsing context name is any string with at least one character
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { FormattedPrimitiveTypeCreator } from '../types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Checks whether a string is a valid itemprop property name.
|
|
4
|
-
*
|
|
5
3
|
* @see https://html.spec.whatwg.org/multipage/microdata.html#defined-property-name
|
|
6
4
|
*
|
|
7
5
|
* > The rules above disallow U+003A COLON characters (:) in non-URL values
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { FormattedPrimitiveTypeCreator } from '../types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Checks whether a string is a valid navigable target name.
|
|
4
|
-
*
|
|
5
3
|
* @see https://html.spec.whatwg.org/multipage/document-sequences.html#valid-navigable-target-name
|
|
6
4
|
*
|
|
7
5
|
* > A valid navigable target name is any string with at least one character
|
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
const TAB_OR_NEWLINE = /[\t\n\r]/;
|
|
7
7
|
/**
|
|
8
|
-
* Checks whether a string is a valid navigable target name.
|
|
9
|
-
*
|
|
10
8
|
* @see https://html.spec.whatwg.org/multipage/document-sequences.html#valid-navigable-target-name
|
|
11
9
|
*
|
|
12
10
|
* > A valid navigable target name is any string with at least one character
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/types",
|
|
3
|
-
"version": "5.0.0-rc.
|
|
3
|
+
"version": "5.0.0-rc.5",
|
|
4
4
|
"description": "Type declaration and value checker",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"author": "Yusuke Hirao <yusukehirao@me.com>",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"engines": {
|
|
13
|
-
"node": ">=
|
|
13
|
+
"node": ">=24"
|
|
14
14
|
},
|
|
15
15
|
"type": "module",
|
|
16
16
|
"exports": {
|
|
@@ -34,15 +34,16 @@
|
|
|
34
34
|
"schema:schema": "npx json2ts \"./types.schema.json\" > \"./src/types.schema.ts\""
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@markuplint/shared": "5.0.0-rc.
|
|
37
|
+
"@markuplint/shared": "5.0.0-rc.5",
|
|
38
38
|
"@types/css-tree": "2.3.11",
|
|
39
39
|
"@types/debug": "4.1.13",
|
|
40
40
|
"bcp-47": "2.1.0",
|
|
41
41
|
"css-tree": "3.2.1",
|
|
42
42
|
"debug": "4.4.3",
|
|
43
|
+
"language-subtag-registry": "0.4.2",
|
|
43
44
|
"leven": "4.1.0",
|
|
44
|
-
"type-fest": "5.
|
|
45
|
+
"type-fest": "5.6.0",
|
|
45
46
|
"whatwg-mimetype": "5.0.0"
|
|
46
47
|
},
|
|
47
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "8d87463af2ff3f1b83fb28da20f1819362cf3555"
|
|
48
49
|
}
|
package/types.schema.json
CHANGED
|
@@ -1246,19 +1246,26 @@
|
|
|
1246
1246
|
"<text-coordinate>",
|
|
1247
1247
|
"<view-box>",
|
|
1248
1248
|
"AbsoluteURL",
|
|
1249
|
+
"AbsoluteURLOrEmpty",
|
|
1249
1250
|
"Accept",
|
|
1250
1251
|
"Any",
|
|
1251
1252
|
"AutoComplete",
|
|
1253
|
+
"AutoCompleteAnchorMantle",
|
|
1254
|
+
"AutoCompleteNoWebauthn",
|
|
1252
1255
|
"BCP47",
|
|
1253
1256
|
"BaseURL",
|
|
1254
1257
|
"BrowsingContextName",
|
|
1255
1258
|
"BrowsingContextNameOrKeyword",
|
|
1259
|
+
"ContentSecurityPolicy",
|
|
1256
1260
|
"CustomElementName",
|
|
1257
1261
|
"DOMID",
|
|
1258
1262
|
"DateString",
|
|
1263
|
+
"DateStringWithOptionalTime",
|
|
1259
1264
|
"DateTime",
|
|
1260
1265
|
"Email",
|
|
1261
1266
|
"FunctionBody",
|
|
1267
|
+
"HTTPEquivContentType",
|
|
1268
|
+
"HTTPEquivRefresh",
|
|
1262
1269
|
"HTTPSchemaURL",
|
|
1263
1270
|
"HashName",
|
|
1264
1271
|
"IconSize",
|
|
@@ -1271,10 +1278,12 @@
|
|
|
1271
1278
|
"LinkTypeForLinkElementInBody",
|
|
1272
1279
|
"LocalDateTimeString",
|
|
1273
1280
|
"MIMEType",
|
|
1281
|
+
"MediaQueryList",
|
|
1274
1282
|
"MonthString",
|
|
1275
1283
|
"NavigableTargetName",
|
|
1276
1284
|
"NavigableTargetNameOrKeyword",
|
|
1277
1285
|
"NoEmptyAny",
|
|
1286
|
+
"NonEmptyURL",
|
|
1278
1287
|
"Number",
|
|
1279
1288
|
"OneCodePointChar",
|
|
1280
1289
|
"OneLineAny",
|