@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.
Files changed (116) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +83 -75
  3. package/lib/check-multi-types.d.ts +2 -9
  4. package/lib/check-multi-types.js +2 -9
  5. package/lib/css-defs.d.ts +0 -8
  6. package/lib/css-defs.js +0 -8
  7. package/lib/css-overrides.d.ts +2 -5
  8. package/lib/css-overrides.js +2 -5
  9. package/lib/css-syntax.d.ts +0 -11
  10. package/lib/css-syntax.js +6 -18
  11. package/lib/css-tokenizers.d.ts +0 -6
  12. package/lib/css-tokenizers.js +0 -6
  13. package/lib/debug.d.ts +0 -3
  14. package/lib/debug.js +0 -3
  15. package/lib/defs.d.ts +0 -8
  16. package/lib/defs.js +308 -37
  17. package/lib/directive.d.ts +0 -14
  18. package/lib/directive.js +0 -14
  19. package/lib/enum.d.ts +0 -11
  20. package/lib/enum.js +0 -11
  21. package/lib/keyword-type.d.ts +0 -13
  22. package/lib/keyword-type.js +0 -13
  23. package/lib/list.d.ts +0 -13
  24. package/lib/list.js +0 -13
  25. package/lib/match-result.d.ts +0 -21
  26. package/lib/match-result.js +0 -21
  27. package/lib/number.d.ts +0 -12
  28. package/lib/number.js +0 -12
  29. package/lib/primitive/is-float.d.ts +3 -4
  30. package/lib/primitive/is-float.js +3 -4
  31. package/lib/primitive/is-int.d.ts +0 -5
  32. package/lib/primitive/is-int.js +0 -5
  33. package/lib/primitive/is-non-zero-uint.d.ts +0 -6
  34. package/lib/primitive/is-non-zero-uint.js +0 -6
  35. package/lib/primitive/is-quantity.d.ts +0 -8
  36. package/lib/primitive/is-quantity.js +0 -8
  37. package/lib/primitive/is-uint.d.ts +0 -6
  38. package/lib/primitive/is-uint.js +0 -6
  39. package/lib/primitive/range.d.ts +0 -8
  40. package/lib/primitive/range.js +0 -8
  41. package/lib/primitive/split-unit.d.ts +0 -6
  42. package/lib/primitive/split-unit.js +0 -6
  43. package/lib/rfc/is-bcp-47.d.ts +24 -1
  44. package/lib/rfc/is-bcp-47.js +88 -3
  45. package/lib/token/token-collection.d.ts +1 -111
  46. package/lib/token/token-collection.js +0 -108
  47. package/lib/token/token.d.ts +1 -73
  48. package/lib/token/token.js +0 -72
  49. package/lib/token/types.d.ts +0 -9
  50. package/lib/types.schema.d.ts +1 -1
  51. package/lib/w3c/check-content-security-policy.d.ts +18 -0
  52. package/lib/w3c/check-content-security-policy.js +363 -0
  53. package/lib/w3c/check-serialized-permissions-policy.d.ts +0 -2
  54. package/lib/w3c/check-serialized-permissions-policy.js +0 -2
  55. package/lib/whatwg/check-autocomplete.d.ts +26 -9
  56. package/lib/whatwg/check-autocomplete.js +42 -15
  57. package/lib/whatwg/check-datetime/date-string-with-optional-time.d.ts +11 -0
  58. package/lib/whatwg/check-datetime/date-string-with-optional-time.js +16 -0
  59. package/lib/whatwg/check-datetime/date-string.d.ts +0 -2
  60. package/lib/whatwg/check-datetime/date-string.js +0 -2
  61. package/lib/whatwg/check-datetime/datetime-tokens.d.ts +0 -13
  62. package/lib/whatwg/check-datetime/datetime-tokens.js +0 -16
  63. package/lib/whatwg/check-datetime/duration-string.d.ts +0 -4
  64. package/lib/whatwg/check-datetime/duration-string.js +0 -4
  65. package/lib/whatwg/check-datetime/global-date-and-time-string.d.ts +0 -2
  66. package/lib/whatwg/check-datetime/global-date-and-time-string.js +4 -4
  67. package/lib/whatwg/check-datetime/index.d.ts +0 -5
  68. package/lib/whatwg/check-datetime/index.js +0 -5
  69. package/lib/whatwg/check-datetime/local-date-and-time-string.d.ts +0 -4
  70. package/lib/whatwg/check-datetime/local-date-and-time-string.js +0 -4
  71. package/lib/whatwg/check-datetime/month-string.d.ts +0 -2
  72. package/lib/whatwg/check-datetime/month-string.js +0 -2
  73. package/lib/whatwg/check-datetime/time-string.d.ts +0 -2
  74. package/lib/whatwg/check-datetime/time-string.js +0 -2
  75. package/lib/whatwg/check-datetime/time-zone-offset-string.d.ts +0 -8
  76. package/lib/whatwg/check-datetime/time-zone-offset-string.js +0 -8
  77. package/lib/whatwg/check-datetime/week-string.d.ts +0 -2
  78. package/lib/whatwg/check-datetime/week-string.js +0 -2
  79. package/lib/whatwg/check-datetime/year-string.d.ts +0 -2
  80. package/lib/whatwg/check-datetime/year-string.js +0 -2
  81. package/lib/whatwg/check-datetime/yearless-date-string.d.ts +0 -2
  82. package/lib/whatwg/check-datetime/yearless-date-string.js +0 -2
  83. package/lib/whatwg/check-email.js +0 -2
  84. package/lib/whatwg/check-http-equiv-content-type.d.ts +12 -0
  85. package/lib/whatwg/check-http-equiv-content-type.js +32 -0
  86. package/lib/whatwg/check-http-equiv-refresh.d.ts +21 -0
  87. package/lib/whatwg/check-http-equiv-refresh.js +106 -0
  88. package/lib/whatwg/check-media-query-list.d.ts +61 -0
  89. package/lib/whatwg/check-media-query-list.js +484 -0
  90. package/lib/whatwg/check-mime-type.d.ts +0 -4
  91. package/lib/whatwg/check-mime-type.js +55 -3
  92. package/lib/whatwg/check-simple-color.d.ts +2 -4
  93. package/lib/whatwg/check-simple-color.js +2 -4
  94. package/lib/whatwg/check-url.d.ts +15 -7
  95. package/lib/whatwg/check-url.js +333 -25
  96. package/lib/whatwg/is-abs-url.d.ts +0 -2
  97. package/lib/whatwg/is-abs-url.js +0 -2
  98. package/lib/whatwg/is-browser-context-name.d.ts +0 -2
  99. package/lib/whatwg/is-browser-context-name.js +0 -2
  100. package/lib/whatwg/is-itemprop-name.d.ts +0 -2
  101. package/lib/whatwg/is-itemprop-name.js +0 -2
  102. package/lib/whatwg/is-navigable-target-name.d.ts +0 -2
  103. package/lib/whatwg/is-navigable-target-name.js +0 -2
  104. package/package.json +6 -5
  105. package/types.schema.json +9 -0
  106. package/ARCHITECTURE.ja.md +0 -256
  107. package/ARCHITECTURE.md +0 -256
  108. package/SKILL.md +0 -100
  109. package/docs/check-pipeline.ja.md +0 -494
  110. package/docs/check-pipeline.md +0 -494
  111. package/docs/token-system.ja.md +0 -584
  112. package/docs/token-system.md +0 -584
  113. package/docs/type-system.ja.md +0 -623
  114. package/docs/type-system.md +0 -623
  115. package/docs/validators.ja.md +0 -536
  116. package/docs/validators.md +0 -536
@@ -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
- * C0 control characters (U+0000–U+001F) and DEL (U+007F) are forbidden
23
- * in URLs. Excludes TAB/LF/CR which are caught by ILLEGAL_WHITESPACE.
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
- const C0_CONTROL = /[\u0000-\u0008\v\u000E-\u001F\u007F]/;
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 adds strict checks matching
38
- * the nu-html-checker's galimatias StrictErrorHandler behavior:
39
- * - Illegal whitespace (tabs, newlines) in URL
40
- * - Malformed percent-encoding
41
- * - C0 control characters
42
- * - URLs that fail `new URL()` parsing (even with a dummy base)
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-code-points
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.trim();
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 control characters
61
- if (C0_CONTROL.test(trimmed)) {
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
- // Try parsing as absolute URL first
73
- try {
74
- new URL(trimmed);
75
- return matched();
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
- catch {
78
- // Not a valid absolute URL try as relative
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
- // Try parsing as relative URL with dummy base
81
- try {
82
- new URL(trimmed, DUMMY_BASE);
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
- catch {
86
- // Both absolute and relative parsing failed
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
  };
@@ -1,7 +1,5 @@
1
1
  import type { FormattedPrimitiveTypeCreator } from '../types.js';
2
2
  /**
3
- * Checks whether a string is a valid absolute URL.
4
- *
5
3
  * @see https://url.spec.whatwg.org/#syntax-url-absolute
6
4
  *
7
5
  * > An absolute-URL string must be one of the following:
@@ -1,6 +1,4 @@
1
1
  /**
2
- * Checks whether a string is a valid absolute URL.
3
- *
4
2
  * @see https://url.spec.whatwg.org/#syntax-url-absolute
5
3
  *
6
4
  * > An absolute-URL string must be one of the following:
@@ -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,6 +1,4 @@
1
1
  /**
2
- * Checks whether a string is a valid browsing context name.
3
- *
4
2
  * @see https://html.spec.whatwg.org/multipage/browsers.html#valid-browsing-context-name
5
3
  *
6
4
  * > 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,6 +1,4 @@
1
1
  /**
2
- * Checks whether a string is a valid itemprop property name.
3
- *
4
2
  * @see https://html.spec.whatwg.org/multipage/microdata.html#defined-property-name
5
3
  *
6
4
  * > 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.2",
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": ">=22"
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.2",
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.5.0",
45
+ "type-fest": "5.6.0",
45
46
  "whatwg-mimetype": "5.0.0"
46
47
  },
47
- "gitHead": "e43763858d9234c417053becc73dbd088c1e7ea6"
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",