@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
@@ -0,0 +1,363 @@
1
+ import { log } from '../debug.js';
2
+ import { matched } from '../match-result.js';
3
+ import { Token } from '../token/index.js';
4
+ /**
5
+ * Fetch Directives — value grammar is a `serialized-source-list`.
6
+ *
7
+ * @see https://www.w3.org/TR/CSP3/#directives-fetch
8
+ */
9
+ const FETCH_DIRECTIVES = new Set([
10
+ 'child-src',
11
+ 'connect-src',
12
+ 'default-src',
13
+ 'font-src',
14
+ 'frame-src',
15
+ 'img-src',
16
+ 'manifest-src',
17
+ 'media-src',
18
+ 'object-src',
19
+ 'script-src',
20
+ 'script-src-attr',
21
+ 'script-src-elem',
22
+ 'style-src',
23
+ 'style-src-attr',
24
+ 'style-src-elem',
25
+ 'worker-src',
26
+ ]);
27
+ /**
28
+ * Directives whose value is also a `serialized-source-list`, per
29
+ * CSP3 §6.7.2 ("This section defines the syntax common to all fetch
30
+ * directives, as well as `base-uri`, `form-action`, and `frame-ancestors`").
31
+ *
32
+ * @see https://www.w3.org/TR/CSP3/#framework-directive-source-list
33
+ */
34
+ const SOURCE_LIST_DIRECTIVES = new Set([...FETCH_DIRECTIVES, 'base-uri', 'form-action', 'frame-ancestors']);
35
+ /**
36
+ * Directives that carry no value at all.
37
+ *
38
+ * @see https://www.w3.org/TR/upgrade-insecure-requests/#delivery
39
+ */
40
+ const NO_VALUE_DIRECTIVES = new Set(['upgrade-insecure-requests']);
41
+ /**
42
+ * Directives recognized by name, whose value grammar this checker does not
43
+ * further validate (accepts anything, including empty): `report-uri` /
44
+ * `report-to` (URI-reference list / single token) and `webrtc` (`'allow'` /
45
+ * `'block'`), none of which are exercised by the nu-validator bench corpus.
46
+ */
47
+ const UNVALIDATED_VALUE_DIRECTIVES = new Set(['report-uri', 'report-to', 'webrtc']);
48
+ /**
49
+ * The complete set of currently-specified CSP directive names: CSP3 §6
50
+ * itself (fetch directives, `base-uri`, `sandbox`, `form-action`,
51
+ * `frame-ancestors`, `report-uri`, `report-to`, `webrtc`), plus directives
52
+ * defined by companion specs that are nonetheless part of the CSP delivery
53
+ * mechanism and widely deployed: `require-trusted-types-for` / `trusted-types`
54
+ * (Trusted Types) and `upgrade-insecure-requests` (Upgrade Insecure Requests).
55
+ *
56
+ * @see https://www.w3.org/TR/CSP3/#directives
57
+ * @see https://www.w3.org/TR/trusted-types/#require-trusted-types-for-csp-directive
58
+ * @see https://www.w3.org/TR/trusted-types/#trusted-types-csp-directive
59
+ * @see https://www.w3.org/TR/upgrade-insecure-requests/#delivery
60
+ */
61
+ const KNOWN_DIRECTIVES = new Set([
62
+ ...SOURCE_LIST_DIRECTIVES,
63
+ ...NO_VALUE_DIRECTIVES,
64
+ ...UNVALIDATED_VALUE_DIRECTIVES,
65
+ 'sandbox',
66
+ 'require-trusted-types-for',
67
+ 'trusted-types',
68
+ ]);
69
+ /**
70
+ * Mirrors the `sandbox` attribute's sandboxing-token enum in
71
+ * `packages/@markuplint/html-spec/src/spec.iframe.jsonc`. Keep the two in
72
+ * sync — there is no shared runtime source because `@markuplint/types` does
73
+ * not depend on `@markuplint/html-spec`.
74
+ *
75
+ * @see https://html.spec.whatwg.org/multipage/iframe-embed-object.html#attr-iframe-sandbox
76
+ */
77
+ const SANDBOX_TOKENS = new Set([
78
+ 'allow-downloads',
79
+ 'allow-forms',
80
+ 'allow-modals',
81
+ 'allow-orientation-lock',
82
+ 'allow-pointer-lock',
83
+ 'allow-popups',
84
+ 'allow-popups-to-escape-sandbox',
85
+ 'allow-presentation',
86
+ 'allow-same-origin',
87
+ 'allow-scripts',
88
+ 'allow-top-navigation',
89
+ 'allow-top-navigation-by-user-activation',
90
+ 'allow-custom-protocols-navigation',
91
+ ]);
92
+ /**
93
+ * `source-expression` keyword-source alternatives, excluding `'none'`
94
+ * (which the grammar restricts to being the sole item of the list).
95
+ *
96
+ * @see https://www.w3.org/TR/CSP3/#grammardef-source-expression
97
+ */
98
+ const KEYWORD_SOURCES = new Set([
99
+ "'self'",
100
+ "'unsafe-inline'",
101
+ "'unsafe-eval'",
102
+ "'strict-dynamic'",
103
+ "'unsafe-hashes'",
104
+ "'report-sample'",
105
+ "'unsafe-allow-redirects'",
106
+ ]);
107
+ const NONE_SOURCE = "'none'";
108
+ /**
109
+ * `scheme-part ":"`, e.g. `https:`, `data:`, `blob:`.
110
+ */
111
+ const SCHEME_SOURCE = /^[A-Z][A-Z0-9+.-]*:$/i;
112
+ /**
113
+ * `[ scheme-part "://" ] host-part [ port-part ] [ path-part ]`.
114
+ * Non-ASCII authorities are already rejected by the ASCII-only guard in
115
+ * {@link checkContentSecurityPolicy} before this pattern is reached.
116
+ */
117
+ const HOST_SOURCE = /^(?:[A-Z][A-Z0-9+.-]*:\/\/)?(?:\*|(?:\*\.)?[A-Z0-9-]+(?:\.[A-Z0-9-]+)*)(?::(?:\d+|\*))?(?:\/\S*)?$/i;
118
+ /**
119
+ * `"'nonce-" base64-value "'"`.
120
+ */
121
+ const NONCE_SOURCE = /^'nonce-[\w+/-]+={0,2}'$/;
122
+ /**
123
+ * `"'" hash-algorithm "-" base64-value "'"`.
124
+ */
125
+ const HASH_SOURCE = /^'sha(?:256|384|512)-[\w+/-]+={0,2}'$/;
126
+ /**
127
+ * @see https://www.w3.org/TR/CSP3/#grammardef-directive-name
128
+ */
129
+ const DIRECTIVE_NAME = /^[A-Z0-9-]+$/i;
130
+ /**
131
+ * `trusted-types-policy-name` charset.
132
+ *
133
+ * @see https://www.w3.org/TR/trusted-types/#framework-directive-trusted-types
134
+ */
135
+ const TRUSTED_TYPES_POLICY_NAME = /^[\w\-#=/@.%]+$/;
136
+ const TRUSTED_TYPES_KEYWORDS = new Set(["'allow-duplicates'", "'none'"]);
137
+ /**
138
+ * `directive-value = *( %x09 / %x20-2B / %x2D-3A / %x3C-7E )`, i.e. HTAB or
139
+ * printable ASCII. The full serialized policy (delimiters included) must be
140
+ * ASCII by construction, since `,`/`;` fall inside `%x20-7E` too.
141
+ */
142
+ const ASCII_ONLY = /[^\t\u0020-\u007E]/;
143
+ /**
144
+ * Splits `token` on every occurrence of the literal `delimiter`, returning
145
+ * each piece as its own {@link Token} with an `offset` correct relative to
146
+ * `token.originalValue` — the position precision this checker needs to
147
+ * report the actual offending token (not the whole attribute value) is only
148
+ * available by tracking offsets through every split, the same approach
149
+ * `check-serialized-permissions-policy.ts` takes via `TokenCollection`.
150
+ */
151
+ function splitToken(token, delimiter) {
152
+ const pieces = [];
153
+ let start = 0;
154
+ let idx = token.value.indexOf(delimiter, start);
155
+ while (idx !== -1) {
156
+ pieces.push(new Token(token.value.slice(start, idx), token.offset + start, token.originalValue));
157
+ start = idx + delimiter.length;
158
+ idx = token.value.indexOf(delimiter, start);
159
+ }
160
+ pieces.push(new Token(token.value.slice(start), token.offset + start, token.originalValue));
161
+ return pieces;
162
+ }
163
+ /** Same as {@link splitToken}, but on a whitespace-run pattern; empty pieces are dropped. */
164
+ function splitTokenByWhitespace(token) {
165
+ const pieces = [];
166
+ const re = /\s+/g;
167
+ let lastIndex = 0;
168
+ let match = re.exec(token.value);
169
+ while (match) {
170
+ pieces.push(new Token(token.value.slice(lastIndex, match.index), token.offset + lastIndex, token.originalValue));
171
+ lastIndex = match.index + match[0].length;
172
+ match = re.exec(token.value);
173
+ }
174
+ pieces.push(new Token(token.value.slice(lastIndex), token.offset + lastIndex, token.originalValue));
175
+ return pieces.filter(piece => piece.value.length > 0);
176
+ }
177
+ function trimToken(token) {
178
+ const leadingLength = token.value.length - token.value.trimStart().length;
179
+ return new Token(token.value.trim(), token.offset + leadingLength, token.originalValue);
180
+ }
181
+ /**
182
+ * Validates the `content` attribute value of
183
+ * `<meta http-equiv="content-security-policy">` against the Content
184
+ * Security Policy Level 3 `serialized-policy` grammar.
185
+ *
186
+ * Directive-value-specific grammars are only enforced for the directives
187
+ * exercised by the nu-validator bench corpus (fetch directives + `base-uri`
188
+ * / `form-action` / `frame-ancestors`, `sandbox`, `require-trusted-types-for`,
189
+ * `trusted-types`, `upgrade-insecure-requests`); `report-uri` / `report-to` /
190
+ * `webrtc` are recognized as registered directive names but their values are
191
+ * not further validated.
192
+ *
193
+ * @see https://www.w3.org/TR/CSP3/#framework-policy
194
+ * @see https://www.w3.org/TR/CSP3/#framework-directives
195
+ * @see https://www.w3.org/TR/CSP3/#meta-element
196
+ */
197
+ export const checkContentSecurityPolicy = () => function checkContentSecurityPolicy(value) {
198
+ log('CHECK: meta http-equiv="content-security-policy" content');
199
+ // See ASCII_ONLY above. A non-ASCII authority (e.g. an IDN host written
200
+ // as Unicode rather than punycode) fails here first.
201
+ const nonAsciiMatch = ASCII_ONLY.exec(value);
202
+ if (nonAsciiMatch) {
203
+ const token = new Token(nonAsciiMatch[0], nonAsciiMatch.index, value);
204
+ return token.unmatched({
205
+ reason: 'syntax-error',
206
+ expects: [{ type: 'format', value: 'ASCII-only Content Security Policy' }],
207
+ });
208
+ }
209
+ /**
210
+ * The `<meta>` delivery algorithm strictly splits the attribute value
211
+ * on "," to obtain a list of independently-parsed policies, mirroring
212
+ * how multiple `Content-Security-Policy` HTTP header instances combine.
213
+ *
214
+ * @see https://www.w3.org/TR/CSP3/#meta-element
215
+ */
216
+ const root = new Token(value, 0, value);
217
+ for (const policyToken of splitToken(root, ',')) {
218
+ const result = checkSerializedPolicy(policyToken);
219
+ if (!result.matched)
220
+ return result;
221
+ }
222
+ return matched();
223
+ };
224
+ function checkSerializedPolicy(policyToken) {
225
+ for (const rawDirectiveToken of splitToken(policyToken, ';')) {
226
+ const directiveToken = trimToken(rawDirectiveToken);
227
+ // OWS-only segments are permitted: a trailing ";", an empty policy
228
+ // (`content=""`), and the separator between adjacent directives.
229
+ if (!directiveToken.value)
230
+ continue;
231
+ const spaceIndex = directiveToken.value.search(/\s/);
232
+ const nameToken = spaceIndex === -1
233
+ ? directiveToken
234
+ : new Token(directiveToken.value.slice(0, spaceIndex), directiveToken.offset, directiveToken.originalValue);
235
+ const name = nameToken.value.toLowerCase();
236
+ const directiveValueToken = spaceIndex === -1
237
+ ? new Token('', directiveToken.offset + directiveToken.value.length, directiveToken.originalValue)
238
+ : trimToken(new Token(directiveToken.value.slice(spaceIndex + 1), directiveToken.offset + spaceIndex + 1, directiveToken.originalValue));
239
+ if (!DIRECTIVE_NAME.test(nameToken.value)) {
240
+ return nameToken.unmatched({
241
+ reason: 'unexpected-token',
242
+ expects: [{ type: 'format', value: 'directive-name' }],
243
+ partName: 'directive-name',
244
+ });
245
+ }
246
+ if (!KNOWN_DIRECTIVES.has(name)) {
247
+ return nameToken.unmatched({
248
+ reason: 'doesnt-exist-in-enum',
249
+ expects: [{ type: 'common', value: 'registered CSP directive name' }],
250
+ partName: 'directive-name',
251
+ });
252
+ }
253
+ const result = checkDirectiveValue(name, directiveValueToken);
254
+ if (!result.matched)
255
+ return result;
256
+ }
257
+ return matched();
258
+ }
259
+ function checkDirectiveValue(name, valueToken) {
260
+ if (SOURCE_LIST_DIRECTIVES.has(name))
261
+ return checkSourceList(valueToken);
262
+ if (name === 'sandbox')
263
+ return checkSandbox(valueToken);
264
+ if (name === 'require-trusted-types-for')
265
+ return checkRequireTrustedTypesFor(valueToken);
266
+ if (name === 'trusted-types')
267
+ return checkTrustedTypes(valueToken);
268
+ if (NO_VALUE_DIRECTIVES.has(name) && valueToken.value) {
269
+ return valueToken.unmatched({
270
+ reason: 'extra-token',
271
+ extra: { type: 'common', value: 'directive-value' },
272
+ partName: name,
273
+ });
274
+ }
275
+ // UNVALIDATED_VALUE_DIRECTIVES (report-uri / report-to / webrtc): accept anything.
276
+ return matched();
277
+ }
278
+ function checkSourceList(valueToken) {
279
+ if (!valueToken.value)
280
+ return matched();
281
+ const tokens = splitTokenByWhitespace(valueToken);
282
+ // `serialized-source-list = (source-expression *(RWS source-expression)) / "'none'"`
283
+ // — `'none'` is only valid as the sole item of the list.
284
+ if (tokens.length > 1) {
285
+ const noneToken = tokens.find(token => token.value.toLowerCase() === NONE_SOURCE);
286
+ if (noneToken) {
287
+ return noneToken.unmatched({
288
+ reason: 'illegal-combination',
289
+ expects: [{ type: 'format', value: 'source-expression' }],
290
+ partName: 'source-list',
291
+ });
292
+ }
293
+ }
294
+ for (const token of tokens) {
295
+ const lower = token.value.toLowerCase();
296
+ if (lower === NONE_SOURCE ||
297
+ token.value === '*' ||
298
+ KEYWORD_SOURCES.has(lower) ||
299
+ NONCE_SOURCE.test(token.value) ||
300
+ HASH_SOURCE.test(token.value) ||
301
+ SCHEME_SOURCE.test(token.value) ||
302
+ HOST_SOURCE.test(token.value)) {
303
+ continue;
304
+ }
305
+ return token.unmatched({
306
+ reason: 'unexpected-token',
307
+ expects: [{ type: 'format', value: 'source-expression' }],
308
+ partName: 'source-expression',
309
+ });
310
+ }
311
+ return matched();
312
+ }
313
+ function checkSandbox(valueToken) {
314
+ if (!valueToken.value)
315
+ return matched();
316
+ for (const token of splitTokenByWhitespace(valueToken)) {
317
+ if (!SANDBOX_TOKENS.has(token.value.toLowerCase())) {
318
+ return token.unmatched({
319
+ reason: 'doesnt-exist-in-enum',
320
+ expects: [{ type: 'common', value: 'sandboxing-token' }],
321
+ partName: 'sandbox',
322
+ });
323
+ }
324
+ }
325
+ return matched();
326
+ }
327
+ function checkRequireTrustedTypesFor(valueToken) {
328
+ const tokens = splitTokenByWhitespace(valueToken);
329
+ if (tokens.length === 0) {
330
+ return valueToken.unmatched({
331
+ reason: 'missing-token',
332
+ expects: [{ type: 'const', value: "'script'" }],
333
+ partName: 'require-trusted-types-for',
334
+ });
335
+ }
336
+ for (const token of tokens) {
337
+ if (token.value.toLowerCase() !== "'script'") {
338
+ return token.unmatched({
339
+ reason: 'doesnt-exist-in-enum',
340
+ expects: [{ type: 'const', value: "'script'" }],
341
+ partName: 'require-trusted-types-for',
342
+ });
343
+ }
344
+ }
345
+ return matched();
346
+ }
347
+ function checkTrustedTypes(valueToken) {
348
+ if (!valueToken.value)
349
+ return matched();
350
+ for (const token of splitTokenByWhitespace(valueToken)) {
351
+ if (token.value === '*' ||
352
+ TRUSTED_TYPES_KEYWORDS.has(token.value.toLowerCase()) ||
353
+ TRUSTED_TYPES_POLICY_NAME.test(token.value)) {
354
+ continue;
355
+ }
356
+ return token.unmatched({
357
+ reason: 'unexpected-token',
358
+ expects: [{ type: 'format', value: 'trusted-types-value' }],
359
+ partName: 'trusted-types',
360
+ });
361
+ }
362
+ return matched();
363
+ }
@@ -1,7 +1,5 @@
1
1
  import type { CustomSyntaxChecker } from '../types.js';
2
2
  /**
3
- * Validates a serialized permissions policy string according to the W3C specification.
4
- *
5
3
  * @see https://w3c.github.io/webappsec-permissions-policy/#serialized-permissions-policy
6
4
  *
7
5
  * > Policy Directives in HTML attributes are represented as their
@@ -2,8 +2,6 @@ import { log } from '../debug.js';
2
2
  import { matched, unmatched } from '../match-result.js';
3
3
  import { TokenCollection } from '../token/index.js';
4
4
  /**
5
- * Validates a serialized permissions policy string according to the W3C specification.
6
- *
7
5
  * @see https://w3c.github.io/webappsec-permissions-policy/#serialized-permissions-policy
8
6
  *
9
7
  * > Policy Directives in HTML attributes are represented as their
@@ -1,15 +1,32 @@
1
1
  import type { CustomSyntaxChecker } from '../types.js';
2
2
  /**
3
- * Validates the `autocomplete` attribute value according to the WHATWG specification.
3
+ * Element/state-specific tightening applied on top of the shared
4
+ * autocomplete grammar.
4
5
  *
5
- * Uses backward parsing (right-to-left) to match the spec algorithm:
6
- * 1. Determine field name from the last token
7
- * 2. Handle `webauthn` credential token and category re-determination
8
- * 3. Validate optional contacting token (home/work/mobile/fax/pager)
9
- * 4. Validate optional shipping/billing token
10
- * 5. Validate optional section-* named group
11
- * 6. Check maximum token count per category
6
+ * - `noWebauthn`: reject the `webauthn` token. Applies to elements
7
+ * where webauthn is not valid per HTML LS
8
+ * §attr-fe-autocomplete-webauthn ("webauthn is only valid for input
9
+ * and textarea elements"). Wired for `<select>`.
10
+ * - `anchorMantle`: reject the `on` / `off` keywords per HTML LS
11
+ * §autofill-anchor-mantle: "When wearing the autofill anchor
12
+ * mantle, the autocomplete attribute [...] must have a value that is
13
+ * an ordered set of space-separated tokens consisting of just
14
+ * autofill detail tokens (i.e. the 'on' and 'off' keywords are not
15
+ * allowed)." The anchor mantle applies to `<input type=hidden>`
16
+ * only; every other autocomplete-carrying control wears the
17
+ * expectation mantle where on/off are valid.
18
+ *
19
+ * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-autocomplete-webauthn
20
+ * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-anchor-mantle
21
+ */
22
+ export type CheckAutoCompleteOptions = {
23
+ readonly noWebauthn?: boolean;
24
+ readonly anchorMantle?: boolean;
25
+ };
26
+ /**
27
+ * Parses backward (right-to-left) to match the spec algorithm, which is
28
+ * anchored on the trailing field name.
12
29
  *
13
30
  * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-autocomplete
14
31
  */
15
- export declare const checkAutoComplete: CustomSyntaxChecker;
32
+ export declare const checkAutoComplete: CustomSyntaxChecker<CheckAutoCompleteOptions>;
@@ -89,9 +89,6 @@ const URL_NAMED_GROUP = 'https://html.spec.whatwg.org/multipage/form-control-inf
89
89
  const URL_PART_OF_ADDRESS = 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-autocomplete-shipping';
90
90
  const URL_AUTOFILL_FIELD = 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-field';
91
91
  /**
92
- * Determines the field category and maximum allowed token count
93
- * based on the last meaningful token (the field name).
94
- *
95
92
  * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-field
96
93
  */
97
94
  function determineFieldCategory(value) {
@@ -108,19 +105,12 @@ function determineFieldCategory(value) {
108
105
  return null;
109
106
  }
110
107
  /**
111
- * Validates the `autocomplete` attribute value according to the WHATWG specification.
112
- *
113
- * Uses backward parsing (right-to-left) to match the spec algorithm:
114
- * 1. Determine field name from the last token
115
- * 2. Handle `webauthn` credential token and category re-determination
116
- * 3. Validate optional contacting token (home/work/mobile/fax/pager)
117
- * 4. Validate optional shipping/billing token
118
- * 5. Validate optional section-* named group
119
- * 6. Check maximum token count per category
108
+ * Parses backward (right-to-left) to match the spec algorithm, which is
109
+ * anchored on the trailing field name.
120
110
  *
121
111
  * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-autocomplete
122
112
  */
123
- export const checkAutoComplete = () => value => {
113
+ export const checkAutoComplete = (options = {}) => value => {
124
114
  const tokens = new TokenCollection(value, {
125
115
  disallowToSurroundBySpaces: false,
126
116
  allowEmpty: false,
@@ -146,6 +136,23 @@ export const checkAutoComplete = () => value => {
146
136
  // Never — TokenCollection.check would catch empty
147
137
  throw new Error('TokenCollection is empty');
148
138
  }
139
+ // Anchor-mantle gate: HTML LS §autofill-anchor-mantle forbids the
140
+ // on/off keywords anywhere in the value ("consisting of just
141
+ // autofill detail tokens"). Scan every token so the diagnostic
142
+ // cites the anchor-mantle spec even when on/off is not the first
143
+ // token (e.g. `name on`, `off name`); the "unknown field name"
144
+ // fallback further down would otherwise mask the true reason.
145
+ if (options.anchorMantle) {
146
+ const onOffToken = identTokens.find(t => t.matches(['on', 'off'], true));
147
+ if (onOffToken) {
148
+ acLog('[Unmatched ("%s")] on/off keyword rejected in autofill anchor mantle', value);
149
+ return onOffToken.unmatched({
150
+ reason: 'unexpected-token',
151
+ expects: [{ type: 'common', value: 'autofill field name' }],
152
+ ref: 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-anchor-mantle',
153
+ });
154
+ }
155
+ }
149
156
  const firstToken = identTokens[0];
150
157
  // Check for "on" / "off"
151
158
  if (firstToken.matches(['on', 'off'], true)) {
@@ -221,6 +228,19 @@ export const checkAutoComplete = () => value => {
221
228
  index--;
222
229
  // Step 2: Handle webauthn (Credential category re-determination)
223
230
  if (category === 'Credential') {
231
+ // Per HTML LS §attr-fe-autocomplete-webauthn, webauthn "is only valid
232
+ // for input and textarea elements." Elements that pass `noWebauthn`
233
+ // (button / fieldset / object / output / select) reject the token
234
+ // with a spec-cited unexpected-token result so the diagnostic
235
+ // distinguishes it from a generic unknown field name.
236
+ if (options.noWebauthn) {
237
+ acLog('[Unmatched ("%s")] webauthn is not valid on this element', value);
238
+ return lastToken.unmatched({
239
+ reason: 'unexpected-token',
240
+ expects: [{ type: 'common', value: 'autofill field name' }],
241
+ ref: 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-autocomplete-webauthn',
242
+ });
243
+ }
224
244
  // webauthn token consumed; if there are more tokens, re-determine category
225
245
  if (index >= 0) {
226
246
  const preWebauthnToken = identTokens[index];
@@ -253,8 +273,15 @@ export const checkAutoComplete = () => value => {
253
273
  }
254
274
  }
255
275
  else {
256
- // Standalone "webauthn" is valid
257
- return matched();
276
+ // Spec: "The webauthn token must not be the only token in the
277
+ // autocomplete attribute's value."
278
+ // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-autocomplete-webauthn
279
+ acLog('[Unmatched ("%s")] Standalone webauthn rejected', value);
280
+ return lastToken.unmatched({
281
+ reason: 'unexpected-token',
282
+ expects: [{ type: 'common', value: 'autofill field name' }],
283
+ ref: URL_AUTOFILL_FIELD,
284
+ });
258
285
  }
259
286
  }
260
287
  // No more tokens to validate — only the field name was present
@@ -0,0 +1,11 @@
1
+ import type { CustomSyntaxChecker } from '../../types.js';
2
+ /**
3
+ * Used by attributes whose spec defines the value as "a valid date string with
4
+ * optional time" (for example `<del>`/`<ins>` `datetime`). The accepted
5
+ * production is strictly the union of those two formats — month-only, year-only,
6
+ * week, yearless date, time-only, local datetime, and duration strings are all
7
+ * rejected.
8
+ *
9
+ * @see https://html.spec.whatwg.org/multipage/edits.html#attr-mod-datetime
10
+ */
11
+ export declare const checkDateStringWithOptionalTime: CustomSyntaxChecker;
@@ -0,0 +1,16 @@
1
+ import { checkMultiTypes } from '../../check-multi-types.js';
2
+ import { checkDateString } from './date-string.js';
3
+ import { checkGlobalDateAndTimeString } from './global-date-and-time-string.js';
4
+ const checks = [checkDateString(), checkGlobalDateAndTimeString()];
5
+ /**
6
+ * Used by attributes whose spec defines the value as "a valid date string with
7
+ * optional time" (for example `<del>`/`<ins>` `datetime`). The accepted
8
+ * production is strictly the union of those two formats — month-only, year-only,
9
+ * week, yearless date, time-only, local datetime, and duration strings are all
10
+ * rejected.
11
+ *
12
+ * @see https://html.spec.whatwg.org/multipage/edits.html#attr-mod-datetime
13
+ */
14
+ export const checkDateStringWithOptionalTime = () => value => {
15
+ return checkMultiTypes(value, checks);
16
+ };
@@ -1,7 +1,5 @@
1
1
  import type { CustomSyntaxChecker } from '../../types.js';
2
2
  /**
3
- * Validates a date string in the format `YYYY-MM-DD`.
4
- *
5
3
  * @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#dates
6
4
  */
7
5
  export declare const checkDateString: CustomSyntaxChecker;
@@ -2,8 +2,6 @@ import { log } from '../../debug.js';
2
2
  import { TokenCollection } from '../../token/index.js';
3
3
  import { datetimeTokenCheck } from './datetime-tokens.js';
4
4
  /**
5
- * Validates a date string in the format `YYYY-MM-DD`.
6
- *
7
5
  * @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#dates
8
6
  */
9
7
  export const checkDateString = () => function checkDateString(value) {
@@ -1,16 +1,3 @@
1
1
  import type { TokenEachCheck } from '../../token/token-collection.js';
2
- /**
3
- * Collection of token check functions for datetime component validation.
4
- *
5
- * Each property is a {@link TokenEachCheck} function that validates a specific
6
- * datetime component (year, month, date, hour, minute, second, etc.)
7
- * according to the WHATWG specification.
8
- */
9
2
  export declare const datetimeTokenCheck: Record<'year' | 'month' | 'date' | 'hour' | 'minute' | 'second' | 'secondFractionalPart' | 'week' | 'hyphen' | 'colon' | 'extra' | 'colonOrEnd' | 'decimalPointOrEnd' | 'localDateTimeSeparator' | 'normalizedlocalDateTimeSeparator' | 'plusOrMinusSign' | 'weekSign', TokenEachCheck> & Record<'_year' | '_month', number | null>;
10
- /**
11
- * Calculates the maximum ISO week number for a given year.
12
- *
13
- * @param year - The year to calculate for
14
- * @returns The maximum week number (52 or 53) for the year
15
- */
16
3
  export declare function getMaxWeekNum(year: number): number;
@@ -1,12 +1,5 @@
1
1
  import { log } from '../../debug.js';
2
2
  import { matched, unmatched } from '../../match-result.js';
3
- /**
4
- * Collection of token check functions for datetime component validation.
5
- *
6
- * Each property is a {@link TokenEachCheck} function that validates a specific
7
- * datetime component (year, month, date, hour, minute, second, etc.)
8
- * according to the WHATWG specification.
9
- */
10
3
  export const datetimeTokenCheck = {
11
4
  /**
12
5
  * Temporary year state
@@ -457,9 +450,6 @@ export const datetimeTokenCheck = {
457
450
  });
458
451
  }
459
452
  },
460
- /**
461
- * Extra token
462
- */
463
453
  extra(extra) {
464
454
  log('Parsing Datetime EXTRA STRING: "%s"', extra?.value);
465
455
  if (extra && extra.value) {
@@ -496,12 +486,6 @@ const daysOfMonth = [
496
486
  // 12
497
487
  31,
498
488
  ];
499
- /**
500
- * Calculates the maximum ISO week number for a given year.
501
- *
502
- * @param year - The year to calculate for
503
- * @returns The maximum week number (52 or 53) for the year
504
- */
505
489
  export function getMaxWeekNum(year) {
506
490
  let date = 31;
507
491
  while (date > 0) {
@@ -1,13 +1,9 @@
1
1
  import type { CustomSyntaxChecker } from '../../types.js';
2
2
  /**
3
- * Validates a duration string in ISO 8601-like format (e.g., `PT1H30M`).
4
- *
5
3
  * @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#durations
6
4
  */
7
5
  export declare const checkDurationISO8601LikeString: CustomSyntaxChecker;
8
6
  /**
9
- * Validates a duration string in component list format (e.g., `1h 30m 5s`).
10
- *
11
7
  * @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#durations
12
8
  */
13
9
  export declare const checkDurationComponentListString: CustomSyntaxChecker;
@@ -3,8 +3,6 @@ import { matched, unmatched } from '../../match-result.js';
3
3
  import { TokenCollection } from '../../token/index.js';
4
4
  import { datetimeTokenCheck } from './datetime-tokens.js';
5
5
  /**
6
- * Validates a duration string in ISO 8601-like format (e.g., `PT1H30M`).
7
- *
8
6
  * @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#durations
9
7
  */
10
8
  export const checkDurationISO8601LikeString = () => function checkDurationISO8601LikeString(value) {
@@ -204,8 +202,6 @@ export const checkDurationISO8601LikeString = () => function checkDurationISO860
204
202
  return res;
205
203
  };
206
204
  /**
207
- * Validates a duration string in component list format (e.g., `1h 30m 5s`).
208
- *
209
205
  * @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#durations
210
206
  */
211
207
  export const checkDurationComponentListString = () => function checkDurationComponentListString(value) {
@@ -1,7 +1,5 @@
1
1
  import type { CustomSyntaxChecker } from '../../types.js';
2
2
  /**
3
- * Validates a global date and time string (date + time + time-zone offset).
4
- *
5
3
  * @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#global-dates-and-times
6
4
  */
7
5
  export declare const checkGlobalDateAndTimeString: CustomSyntaxChecker;