@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,484 @@
1
+ import * as csstree from 'css-tree';
2
+ import { cssSyntaxMatch } from '../css-syntax.js';
3
+ import { matched, unmatched } from '../match-result.js';
4
+ import { Token } from '../token/index.js';
5
+ const expects = [
6
+ {
7
+ type: 'format',
8
+ value: 'media query list',
9
+ },
10
+ ];
11
+ /**
12
+ * Media types defined by Media Queries Level 5 §2.3 as currently active.
13
+ *
14
+ * @see https://www.w3.org/TR/mediaqueries-5/#media-types
15
+ */
16
+ const ACTIVE_MEDIA_TYPES = new Set(['all', 'screen', 'print']);
17
+ /**
18
+ * Media types that Media Queries Level 5 §2.3 still recognises as syntax
19
+ * (so user agents must not throw on them) but **must make match nothing**.
20
+ * Authoring conformance: do not use these.
21
+ */
22
+ const DEPRECATED_MEDIA_TYPES = new Set([
23
+ 'tty',
24
+ 'tv',
25
+ 'projection',
26
+ 'handheld',
27
+ 'braille',
28
+ 'embossed',
29
+ 'aural',
30
+ 'speech',
31
+ ]);
32
+ /**
33
+ * Media features deprecated since Media Queries Level 4. MDN's `@media`
34
+ * reference explicitly lists `device-width`, `device-height`, and
35
+ * `device-aspect-ratio` as deprecated; the `min-` / `max-` prefixed
36
+ * variants are deprecated as derivatives of the same features (MQL4's
37
+ * range-feature syntax replaces the explicit prefix forms).
38
+ *
39
+ * Note: Media Queries Level 5 Appendix A is the normative location, but
40
+ * its content was not directly retrievable via WebFetch at the time this
41
+ * list was authored, so MDN is the verified source.
42
+ *
43
+ * @see https://developer.mozilla.org/en-US/docs/Web/CSS/@media
44
+ */
45
+ const DEPRECATED_MEDIA_FEATURES = new Set([
46
+ 'device-width',
47
+ 'min-device-width',
48
+ 'max-device-width',
49
+ 'device-height',
50
+ 'min-device-height',
51
+ 'max-device-height',
52
+ 'device-aspect-ratio',
53
+ 'min-device-aspect-ratio',
54
+ 'max-device-aspect-ratio',
55
+ ]);
56
+ /**
57
+ * Maps each MQL5 §4 range/discrete-numeric feature to the CSS value
58
+ * type its argument must satisfy. The actual unit / sign / dimension
59
+ * checking is delegated to `csstree.lexer.match()` below — only the
60
+ * feature-to-type association needs to live in markuplint because
61
+ * css-tree does not expose a per-feature value-type registry.
62
+ *
63
+ * **How to add a new MQL feature** when the spec extends the catalogue
64
+ * (e.g., a future Level 6 may add a new resolution or ratio feature):
65
+ *
66
+ * 1. Find the feature definition in
67
+ * {@link https://www.w3.org/TR/mediaqueries-5/#mq-features Media Queries §4}
68
+ * (or its successor) and read its `Value:` line — that's the CSS
69
+ * type, expressed as `<length>` / `<integer>` / `<resolution>` /
70
+ * `<ratio>` (or a discrete keyword set, which is *not* handled here).
71
+ * 2. Add an entry to this map. The value must be one of the four
72
+ * string literals — keep this list narrow because each new type
73
+ * string also needs to be a valid `csstree.lexer.match()` syntax.
74
+ * 3. If the new feature carries an MQL-side semantic constraint that
75
+ * goes beyond CSS Values §6 (e.g., "must be non-negative" beyond
76
+ * `<integer>`'s signed grammar, "must be strictly positive" beyond
77
+ * `<ratio>`'s `[0,∞]` range), encode it in Stage B of
78
+ * `validateFeatureValue()`.
79
+ * 4. Add table-driven cases in `check-media-query-list.spec.ts` —
80
+ * cover at least one valid and one invalid value, plus one case
81
+ * that exercises Stage B if applicable.
82
+ * 5. Discrete keyword features (e.g., `prefers-color-scheme: dark`)
83
+ * intentionally fall through this map; they are validated by the
84
+ * enum-driven attribute checking elsewhere.
85
+ *
86
+ * @see https://www.w3.org/TR/mediaqueries-5/#mq-features
87
+ */
88
+ const FEATURE_VALUE_TYPE = {
89
+ // <length>
90
+ width: '<length>',
91
+ height: '<length>',
92
+ 'min-width': '<length>',
93
+ 'max-width': '<length>',
94
+ 'min-height': '<length>',
95
+ 'max-height': '<length>',
96
+ // <integer> — MQL5 §4.4 imposes a non-negative additional constraint
97
+ // enforced separately below
98
+ color: '<integer>',
99
+ 'min-color': '<integer>',
100
+ 'max-color': '<integer>',
101
+ 'color-index': '<integer>',
102
+ 'min-color-index': '<integer>',
103
+ 'max-color-index': '<integer>',
104
+ monochrome: '<integer>',
105
+ 'min-monochrome': '<integer>',
106
+ 'max-monochrome': '<integer>',
107
+ 'horizontal-viewport-segments': '<integer>',
108
+ 'min-horizontal-viewport-segments': '<integer>',
109
+ 'max-horizontal-viewport-segments': '<integer>',
110
+ 'vertical-viewport-segments': '<integer>',
111
+ 'min-vertical-viewport-segments': '<integer>',
112
+ 'max-vertical-viewport-segments': '<integer>',
113
+ // <resolution>
114
+ resolution: '<resolution>',
115
+ 'min-resolution': '<resolution>',
116
+ 'max-resolution': '<resolution>',
117
+ // <ratio> — MQL5 §4.5 imposes a positive additional constraint
118
+ // enforced separately below
119
+ 'aspect-ratio': '<ratio>',
120
+ 'min-aspect-ratio': '<ratio>',
121
+ 'max-aspect-ratio': '<ratio>',
122
+ };
123
+ /**
124
+ * Validates a `media` attribute value against the Media Queries Level 5
125
+ * grammar and authoring constraints.
126
+ *
127
+ * Catches three classes of conformance error that nu-validator reports but
128
+ * `whatwg-mimetype`-style checks do not:
129
+ *
130
+ * 1. **Syntax errors** — unbalanced parens, stray semicolons inside `()`,
131
+ * unrecognised dimensions. Detected via `css-tree`'s `mediaQueryList`
132
+ * parser entry-point.
133
+ * 2. **Unknown / deprecated media types** — anything outside
134
+ * {`all`, `screen`, `print`} (e.g., `alla`, `notscreen`, `projection`).
135
+ * Per [Media Queries Level 5 §2.3](https://www.w3.org/TR/mediaqueries-5/#media-types),
136
+ * deprecated types are syntactically valid but author conformance
137
+ * forbids them.
138
+ * 3. **Deprecated media features** — `device-width` / `device-height` /
139
+ * `device-aspect-ratio` and their min-/max- variants. MDN's `@media`
140
+ * reference marks them as
141
+ * [deprecated since Media Queries Level 4](https://developer.mozilla.org/en-US/docs/Web/CSS/@media).
142
+ * 4. **Wrong-type feature values** — `(min-width: 400)` (unitless
143
+ * non-zero number for a `<length>` feature), `(min-width: 400dpi)`
144
+ * (resolution unit on a length feature), `(color: 1em)` (length
145
+ * on an integer feature). The matrix covers length / integer /
146
+ * resolution / ratio features per Media Queries Level 5 §4. Unknown
147
+ * features are passed through unchanged so forward-compat additions
148
+ * do not regress to errors.
149
+ *
150
+ * @see https://www.w3.org/TR/mediaqueries-5/
151
+ */
152
+ export const checkMediaQueryList = () => value => {
153
+ if (!value) {
154
+ return unmatched(value, 'empty-token', { expects });
155
+ }
156
+ // Stage 1: delegate to the CSS Syntax matcher (the same engine that
157
+ // `<media-query-list>` used to use). Catches malformed grammar that
158
+ // the lenient `csstree.parse(..., { context: 'mediaQueryList' })`
159
+ // silently accepts: missing `and` whitespace (`screenand (...)`),
160
+ // unterminated declarations (`screen and (min-width:`), trailing
161
+ // combinators (`screen and (...) and`), trailing commas (`screen,`),
162
+ // whitespace-only values, etc.
163
+ const syntaxResult = cssSyntaxMatch(value, '<media-query-list>');
164
+ if (!syntaxResult.matched) {
165
+ return syntaxResult;
166
+ }
167
+ // css-tree silently swallows stray semicolons inside `()` (e.g.,
168
+ // `(min-width: 400px;)`), but the CSS Syntax tokenizer rule says a
169
+ // `<semicolon-token>` is invalid inside a `<media-condition>`. Detect
170
+ // it manually before delegating to css-tree.
171
+ const semicolon = findSemicolonInsideParens(value);
172
+ if (semicolon != null) {
173
+ return new Token(';', semicolon, value).unmatched({
174
+ reason: 'unexpected-token',
175
+ expects,
176
+ partName: 'a stray semicolon inside the media condition',
177
+ });
178
+ }
179
+ // Stage 2: re-parse with the lenient mediaQueryList AST so we can
180
+ // walk it and reject deprecated/unknown identifiers.
181
+ let ast;
182
+ try {
183
+ ast = csstree.parse(value, { context: 'mediaQueryList', positions: true });
184
+ }
185
+ catch (error) {
186
+ // css-tree's parser raises a JS-builtin `SyntaxError` decorated
187
+ // with `source` / `offset` / `formattedMessage` for user-input
188
+ // parse failures (Tier-3 violations). Anything else — programmer-
189
+ // error fatal errors (`TypeError` / `ReferenceError` / bare
190
+ // `SyntaxError`) or non-`Error` throws — bubbles up so the three-
191
+ // tier policy can treat it as Tier-1.
192
+ if (!isCssTreeParseError(error))
193
+ throw error;
194
+ return unmatched(value, 'syntax-error', { expects, partName: error.message });
195
+ }
196
+ if (ast.type !== 'MediaQueryList') {
197
+ return unmatched(value, 'syntax-error', { expects });
198
+ }
199
+ for (const query of ast.children ?? []) {
200
+ if (query.type !== 'MediaQuery')
201
+ continue;
202
+ // css-tree's `MediaQuery` runtime nodes carry `mediaType` and
203
+ // `condition` properties (verified empirically from `csstree.parse`),
204
+ // but `@types/css-tree` does not surface them. Cast through the
205
+ // minimal shape we actually consume rather than `any`.
206
+ const q = query;
207
+ // Validate the media type identifier (only present when the query
208
+ // uses `<media-type>` form; condition-only queries skip this check).
209
+ const mediaType = q.mediaType;
210
+ if (typeof mediaType === 'string') {
211
+ const lowered = mediaType.toLowerCase();
212
+ if (DEPRECATED_MEDIA_TYPES.has(lowered)) {
213
+ const offset = q.loc?.start.offset ?? 0;
214
+ return new Token(mediaType, offset, value).unmatched({
215
+ reason: 'doesnt-exist-in-enum',
216
+ expects,
217
+ partName: `deprecated media type "${mediaType}"`,
218
+ });
219
+ }
220
+ if (!ACTIVE_MEDIA_TYPES.has(lowered)) {
221
+ const offset = q.loc?.start.offset ?? 0;
222
+ return new Token(mediaType, offset, value).unmatched({
223
+ reason: 'doesnt-exist-in-enum',
224
+ expects,
225
+ candidate: nearestMediaType(lowered),
226
+ });
227
+ }
228
+ }
229
+ // Walk the condition tree for Feature / MediaFeature nodes and
230
+ // reject deprecated names. Features the engine doesn't recognise
231
+ // at all surface as parse errors via the try/catch above.
232
+ if (q.condition) {
233
+ const result = walkCondition(q.condition, value);
234
+ if (result)
235
+ return result;
236
+ }
237
+ }
238
+ return matched();
239
+ };
240
+ function walkCondition(node, source) {
241
+ let result = null;
242
+ csstree.walk(node, child => {
243
+ if (result)
244
+ return;
245
+ // `Feature` / `MediaFeature` are runtime node types emitted by
246
+ // css-tree's mediaQueryList parser but not exposed by
247
+ // `@types/css-tree`. Compare via cast to keep the narrow typing.
248
+ const childType = child.type;
249
+ // Reject <general-enclosed> fallback matches. Media Queries Level 5 §3
250
+ // (the <general-enclosed> Production) states: "Authors must not use
251
+ // <general-enclosed> in their stylesheets. It exists only for
252
+ // future-compatibility, so that new syntax additions do not invalidate
253
+ // too much of a <media-condition> in older user agents." css-tree only
254
+ // emits GeneralEnclosed when the enclosed tokens genuinely fail
255
+ // <media-feature> grammar (e.g., `(min-width:)` — empty value after
256
+ // the colon; `(123)` — number token where <ident> is expected).
257
+ // Well-formed `(<ident>: <value>)` shapes with unknown feature names
258
+ // still parse as `Feature`, so forward-compatibility for new feature
259
+ // names is preserved.
260
+ if (childType === 'GeneralEnclosed') {
261
+ const offset = child.loc?.start.offset ?? 0;
262
+ const endOffset = child.loc?.end.offset ?? offset;
263
+ const raw = source.slice(offset, endOffset);
264
+ result = new Token(raw, offset, source).unmatched({
265
+ reason: 'syntax-error',
266
+ expects,
267
+ partName: `unknown or malformed media condition ${JSON.stringify(raw)} (Media Queries Level 5 §3 forbids <general-enclosed> in author stylesheets)`,
268
+ });
269
+ return;
270
+ }
271
+ if (childType !== 'Feature' && childType !== 'MediaFeature')
272
+ return;
273
+ const feat = child;
274
+ const name = feat.name;
275
+ if (typeof name !== 'string')
276
+ return;
277
+ const lowered = name.toLowerCase();
278
+ if (DEPRECATED_MEDIA_FEATURES.has(lowered)) {
279
+ const offset = child.loc?.start.offset ?? 0;
280
+ // Skip the leading `(` so the highlight lands on the feature name.
281
+ const nameOffset = source.slice(offset).search(/[A-Z]/i);
282
+ result = new Token(name, offset + Math.max(nameOffset, 0), source).unmatched({
283
+ reason: 'doesnt-exist-in-enum',
284
+ expects,
285
+ partName: `deprecated media feature "${name}"`,
286
+ });
287
+ return;
288
+ }
289
+ // Boolean form `(name)` carries `value: null` and is type-agnostic.
290
+ if (!feat.value)
291
+ return;
292
+ const typeError = validateFeatureValue(lowered, feat.value, source);
293
+ if (typeError)
294
+ result = typeError;
295
+ });
296
+ return result;
297
+ }
298
+ /**
299
+ * Cross-checks a media feature's parsed value against the expected
300
+ * type per Media Queries Level 5 §4. Returns an `unmatched` result
301
+ * when the value type does not satisfy the feature's grammar (e.g.,
302
+ * `<integer>` features given a `<dimension>`, or `<length>` features
303
+ * given a unitless non-zero number).
304
+ *
305
+ * Returns null for:
306
+ * - Unknown features (treated as forward-compat — defer to css-tree)
307
+ * - Custom CSS variables / functions / calc() (not statically reducible)
308
+ *
309
+ * @see https://www.w3.org/TR/mediaqueries-5/#mq-features
310
+ */
311
+ function validateFeatureValue(feature, value, source) {
312
+ const expectedType = FEATURE_VALUE_TYPE[feature];
313
+ if (!expectedType)
314
+ return null;
315
+ // Defer calc()/var()/clamp() — their static value cannot be computed
316
+ // without resolving cascades. css-tree accepts them; we trust
317
+ // nu-validator does the same in practice.
318
+ if (value.type === 'Function' || value.type === 'Parentheses')
319
+ return null;
320
+ const offset = value.loc?.start.offset ?? 0;
321
+ const raw = source.slice(offset, value.loc?.end.offset ?? offset);
322
+ // Stage A: CSS-syntax conformance — delegate the dimension / unit /
323
+ // fractional / scientific-notation matrix to `csstree.lexer.match()`
324
+ // rather than maintain a parallel hardcoded table. css-tree's
325
+ // `<length>` / `<integer>` / `<resolution>` / `<ratio>` definitions
326
+ // mirror CSS Values and Units Level 4 §6 and stay in sync as the
327
+ // language evolves (container query units, viewport-relative
328
+ // variants, etc.). `error: null` signals a successful match; a
329
+ // non-null `SyntaxMatchError` describes the mismatch and is included
330
+ // in the partName so the user sees which unit / dimension failed.
331
+ // `lexer.match()` *throws* a `SyntaxReferenceError` for unknown
332
+ // syntax names (e.g., a typo in `FEATURE_VALUE_TYPE`); let it bubble
333
+ // up as Tier-1 fatal so a programmer error never masquerades as a
334
+ // user-facing lint violation.
335
+ const lexerResult = csstree.lexer.match(expectedType, value);
336
+ if (lexerResult.error) {
337
+ const detail = lexerResult.error.message?.split('\n')[0] ?? '';
338
+ return new Token(raw, offset, source).unmatched({
339
+ reason: 'syntax-error',
340
+ expects,
341
+ partName: detail
342
+ ? `${expectedType} required for "${feature}" (${detail})`
343
+ : `${expectedType} required for "${feature}"`,
344
+ });
345
+ }
346
+ // Stage B: MQL5 semantic constraints that go beyond CSS Values §6.
347
+ // Stage A already rejects negative numbers for `<ratio>` (the type's
348
+ // `<number [0,∞]>` range constraint excludes them), but accepts `0`
349
+ // as in-range — Stage B adds the strictly-positive constraint from
350
+ // MQL5 §4.5. Stage A also accepts negative `<integer>` per CSS Values
351
+ // §6.2 grammar, so MQL5 §4.4's non-negative requirement for
352
+ // `color` / `monochrome` / `*-viewport-segments` is enforced here.
353
+ if (expectedType === '<integer>' &&
354
+ value.type === 'Number' &&
355
+ Number.parseInt(value.value, 10) < 0) {
356
+ return new Token(raw, offset, source).unmatched({
357
+ reason: 'syntax-error',
358
+ expects,
359
+ partName: `<integer> for "${feature}" must be non-negative (MQL5 §4.4)`,
360
+ });
361
+ }
362
+ if (expectedType === '<ratio>') {
363
+ // `@types/css-tree` declares `Ratio.left/.right` as `string`, but the
364
+ // runtime mediaQueryList parser emits `NumberNode` children — cast
365
+ // through `unknown` to consume the actual runtime shape.
366
+ const ratio = value.type === 'Ratio'
367
+ ? value
368
+ : null;
369
+ const numerator = ratio
370
+ ? Number.parseFloat(ratio.left.value)
371
+ : value.type === 'Number'
372
+ ? Number.parseFloat(value.value)
373
+ : undefined;
374
+ const denominator = ratio ? Number.parseFloat(ratio.right.value) : 1;
375
+ if (numerator !== undefined && (!(numerator > 0) || !(denominator > 0))) {
376
+ return new Token(raw, offset, source).unmatched({
377
+ reason: 'syntax-error',
378
+ expects,
379
+ partName: `<ratio> for "${feature}" must be positive (MQL5 §4.5)`,
380
+ });
381
+ }
382
+ }
383
+ return null;
384
+ }
385
+ /**
386
+ * Only used for the "candidate" typo hint shown to the user, not for validation.
387
+ */
388
+ function nearestMediaType(input) {
389
+ for (const candidate of ACTIVE_MEDIA_TYPES) {
390
+ if (input.startsWith(candidate) || candidate.startsWith(input)) {
391
+ return candidate;
392
+ }
393
+ }
394
+ return undefined;
395
+ }
396
+ /**
397
+ * Returns the offset of the first `;` that appears inside a balanced `(`
398
+ * region, or `null` when no such semicolon exists. Used as a pre-parse
399
+ * guard because css-tree's media-query parser silently absorbs stray
400
+ * semicolons rather than flagging them.
401
+ *
402
+ * Implementation note: Media Queries Level 5 grammar contains no quoted
403
+ * strings, so a naive paren-depth counter is sufficient — there is no
404
+ * `"a;b"` literal context to consider. If a future MQ level introduces
405
+ * quoted-string production this scan would falsely flag the inner `;`
406
+ * and need a string-aware tokenizer instead.
407
+ */
408
+ /**
409
+ * Distinguishes css-tree's user-input parse errors (Tier-3 violations)
410
+ * from JS-builtin `SyntaxError`s thrown elsewhere (Tier-1 fatal errors like
411
+ * `JSON.parse('bad')`). css-tree decorates its `SyntaxError` instances
412
+ * with `source` and `formattedMessage`; the JS builtin does not.
413
+ */
414
+ function isCssTreeParseError(error) {
415
+ return (error instanceof SyntaxError &&
416
+ typeof error.source === 'string' &&
417
+ typeof error.formattedMessage === 'string');
418
+ }
419
+ function findSemicolonInsideParens(input) {
420
+ let depth = 0;
421
+ for (let i = 0; i < input.length; i++) {
422
+ const ch = input[i];
423
+ if (ch === '(')
424
+ depth++;
425
+ else if (ch === ')')
426
+ depth = Math.max(0, depth - 1);
427
+ else if (ch === ';' && depth > 0)
428
+ return i;
429
+ }
430
+ return null;
431
+ }
432
+ /**
433
+ * Exists so callers that embed a `<media-condition>` inside a larger grammar
434
+ * (`<source-size-list>` in `<sizes>`) can enforce the same forbidden-in-authoring
435
+ * rule that {@link checkMediaQueryList} applies to `<media-query-list>` values.
436
+ *
437
+ * @see https://www.w3.org/TR/mediaqueries-5/#general-enclosed — "Authors must
438
+ * not use `<general-enclosed>` in their stylesheets. It exists only for
439
+ * future-compatibility, so that new syntax additions do not invalidate too
440
+ * much of a `<media-condition>` in older user agents."
441
+ *
442
+ * Contract:
443
+ *
444
+ * - css-tree emits `GeneralEnclosed` only when the enclosed tokens fail
445
+ * `<media-feature>` grammar (`(min-width:)` empty value, `(123)` non-ident);
446
+ * well-formed `(<ident>: <value>)` shapes parse as `Feature`, so unknown
447
+ * feature names keep passing (forward-compatibility).
448
+ * - Returns `null` on any css-tree parse failure — the enclosing checker owns
449
+ * grammar-error reporting. Only a *successful* parse reaching the fallback
450
+ * surfaces here.
451
+ * - Programmer-error throws (`TypeError` etc.) bubble up; only Tier-3 css-tree
452
+ * `SyntaxError` shapes are swallowed via `isCssTreeParseError`.
453
+ *
454
+ * @param snippet raw parenthesised group extracted from the outer value
455
+ * @returns first match's `{ raw, offset }` (offset is 0-based within `snippet`
456
+ * — callers add their own outer offset when reporting), or `null` if none
457
+ */
458
+ export function findGeneralEnclosed(snippet) {
459
+ let ast;
460
+ try {
461
+ ast = csstree.parse(snippet, { context: 'mediaQueryList', positions: true });
462
+ }
463
+ catch (error) {
464
+ // css-tree's user-input parse failures (Tier-3) may surface when the
465
+ // caller hands a snippet that fails media-query grammar even though the
466
+ // outer grammar accepted it — return null so the outer diagnostic path
467
+ // owns the reporting. Rethrow anything else (Tier-1 programmer errors
468
+ // like `TypeError`) rather than silently hiding the bug.
469
+ if (!isCssTreeParseError(error))
470
+ throw error;
471
+ return null;
472
+ }
473
+ // `csstree.find` stops on the first match; walking the whole tree is wasted
474
+ // work once we have a hit. oxlint's `no-array-method-this-argument` mis-
475
+ // identifies this as an Array-method call because the API happens to be
476
+ // named `find` — suppress inline.
477
+ // eslint-disable-next-line unicorn/no-array-method-this-argument
478
+ const node = csstree.find(ast, child => child.type === 'GeneralEnclosed');
479
+ if (!node)
480
+ return null;
481
+ const offset = node.loc?.start.offset ?? 0;
482
+ const endOffset = node.loc?.end.offset ?? offset;
483
+ return { raw: snippet.slice(offset, endOffset), offset };
484
+ }
@@ -1,9 +1,5 @@
1
1
  import type { CustomSyntaxChecker } from '../types.js';
2
2
  /**
3
- * Validates a MIME type string according to the WHATWG MIME Sniffing specification.
4
- *
5
- * Optionally restricts to MIME types with no parameters.
6
- *
7
3
  * @see https://mimesniff.spec.whatwg.org/#valid-mime-type
8
4
  */
9
5
  export declare const checkMIMEType: CustomSyntaxChecker<{
@@ -9,10 +9,55 @@ const expects = (withoutParameters) => [
9
9
  },
10
10
  ];
11
11
  /**
12
- * Validates a MIME type string according to the WHATWG MIME Sniffing specification.
13
- *
14
- * Optionally restricts to MIME types with no parameters.
12
+ * [RFC 9110 §5.6.6](https://www.rfc-editor.org/rfc/rfc9110#name-parameters)
13
+ * requires the closing DQUOTE in `quoted-string`; the
14
+ * [WHATWG MIME Sniffing](https://mimesniff.spec.whatwg.org/#parse-a-mime-type)
15
+ * parser tolerates the missing terminator and returns a partial value (so
16
+ * `MIMEType.parse` cannot surface this conformance error). We do the
17
+ * structural scan ourselves and run it before invoking the parser.
15
18
  *
19
+ * **Limitation:** the scan assumes the opening DQUOTE immediately follows
20
+ * the `=` byte. RFC 9110 allows OWS (optional whitespace) around `=`, but
21
+ * WHATWG MIME Sniffing's tokenizer (and every nu-validator-known input
22
+ * today) closes that gap. If a future fixture exercises `; charset = "..."`
23
+ * style spacing, extend this scan to skip OWS before checking for `"`.
24
+ */
25
+ function findUnterminatedQuotedString(value) {
26
+ for (let i = 0; i < value.length;) {
27
+ if (value[i] !== ';') {
28
+ i++;
29
+ continue;
30
+ }
31
+ i++;
32
+ while (i < value.length && value[i] !== '=' && value[i] !== ';')
33
+ i++;
34
+ if (i >= value.length || value[i] === ';')
35
+ continue;
36
+ i++;
37
+ if (i >= value.length || value[i] !== '"')
38
+ continue;
39
+ const start = i;
40
+ i++;
41
+ let terminated = false;
42
+ while (i < value.length) {
43
+ if (value[i] === '\\') {
44
+ i += 2;
45
+ continue;
46
+ }
47
+ if (value[i] === '"') {
48
+ i++;
49
+ terminated = true;
50
+ break;
51
+ }
52
+ i++;
53
+ }
54
+ if (!terminated) {
55
+ return { offset: start };
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+ /**
16
61
  * @see https://mimesniff.spec.whatwg.org/#valid-mime-type
17
62
  */
18
63
  export const checkMIMEType = options => value => {
@@ -20,6 +65,13 @@ export const checkMIMEType = options => value => {
20
65
  if (!value) {
21
66
  return unmatched(value, 'empty-token', { expects: expects(withoutParameters) });
22
67
  }
68
+ const unterminated = findUnterminatedQuotedString(value);
69
+ if (unterminated) {
70
+ return new Token(value.slice(unterminated.offset), unterminated.offset, value).unmatched({
71
+ reason: 'syntax-error',
72
+ expects: expects(withoutParameters),
73
+ });
74
+ }
23
75
  const mimeType = MIMEType.parse(value);
24
76
  if (mimeType) {
25
77
  if (value.toLowerCase() === mimeType.essence) {
@@ -1,9 +1,7 @@
1
1
  import type { FormattedPrimitiveTypeCreator } from '../types.js';
2
2
  /**
3
- * Checks whether a string is a valid simple color.
4
- *
5
- * A valid simple color is exactly seven characters long:
6
- * a U+0023 NUMBER SIGN (#) followed by six ASCII hex digits.
3
+ * Not the same as the CSS `<color>` type — named colors, `rgb()`, `hsl()`,
4
+ * etc. are all invalid here. Used for `input[type=color]` value validation.
7
5
  *
8
6
  * @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-simple-colour
9
7
  */
@@ -1,8 +1,6 @@
1
1
  /**
2
- * Checks whether a string is a valid simple color.
3
- *
4
- * A valid simple color is exactly seven characters long:
5
- * a U+0023 NUMBER SIGN (#) followed by six ASCII hex digits.
2
+ * Not the same as the CSS `<color>` type — named colors, `rgb()`, `hsl()`,
3
+ * etc. are all invalid here. Used for `input[type=color]` value validation.
6
4
  *
7
5
  * @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-simple-colour
8
6
  */
@@ -3,14 +3,22 @@ import type { CustomSyntaxChecker } from '../types.js';
3
3
  * Validates a URL string (potentially surrounded by spaces) per the WHATWG
4
4
  * URL Standard. Accepts both absolute and relative URLs.
5
5
  *
6
- * Uses `new URL()` for structural parsing and adds strict checks matching
7
- * the nu-html-checker's galimatias StrictErrorHandler behavior:
8
- * - Illegal whitespace (tabs, newlines) in URL
9
- * - Malformed percent-encoding
10
- * - C0 control characters
11
- * - URLs that fail `new URL()` parsing (even with a dummy base)
6
+ * Uses `new URL()` for structural parsing and surfaces the validation errors
7
+ * that `new URL()` silently accepts but nu-validator (and the URL LS) report.
8
+ *
9
+ * Categories caught:
10
+ *
11
+ * - **invalid-URL-unit** (forbidden code points, malformed percent-encoding,
12
+ * unencoded space, tab/CR/LF)
13
+ * - **invalid-reverse-solidus** (`\` in a special-scheme URL)
14
+ * - **special-scheme-missing-following-solidus** (`http:foo`, `file:bar`)
15
+ * and `file-scheme-missing-following-solidus` / single-slash variants
16
+ * - **file-invalid-Windows-drive-letter** (`file:///C|/foo`)
17
+ * - **invalid-credentials** (`http://user:pass@example.com`)
18
+ * - **IPv4-non-decimal-part** (`http://192.0x00A80001`, hex/octal host labels)
19
+ * - any URL that fails `new URL()` parsing (even with a dummy base)
12
20
  *
13
21
  * @see https://html.spec.whatwg.org/multipage/urls-and-fetching.html#valid-url-potentially-surrounded-by-spaces
14
- * @see https://url.spec.whatwg.org/#url-code-points
22
+ * @see https://url.spec.whatwg.org/#url-parsing
15
23
  */
16
24
  export declare const checkURL: CustomSyntaxChecker;