@markuplint/types 5.0.0-rc.2 → 5.0.0-rc.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +83 -75
- package/lib/check-multi-types.d.ts +2 -9
- package/lib/check-multi-types.js +2 -9
- package/lib/css-defs.d.ts +0 -8
- package/lib/css-defs.js +0 -8
- package/lib/css-overrides.d.ts +2 -5
- package/lib/css-overrides.js +2 -5
- package/lib/css-syntax.d.ts +0 -11
- package/lib/css-syntax.js +6 -18
- package/lib/css-tokenizers.d.ts +0 -6
- package/lib/css-tokenizers.js +0 -6
- package/lib/debug.d.ts +0 -3
- package/lib/debug.js +0 -3
- package/lib/defs.d.ts +0 -8
- package/lib/defs.js +308 -37
- package/lib/directive.d.ts +0 -14
- package/lib/directive.js +0 -14
- package/lib/enum.d.ts +0 -11
- package/lib/enum.js +0 -11
- package/lib/keyword-type.d.ts +0 -13
- package/lib/keyword-type.js +0 -13
- package/lib/list.d.ts +0 -13
- package/lib/list.js +0 -13
- package/lib/match-result.d.ts +0 -21
- package/lib/match-result.js +0 -21
- package/lib/number.d.ts +0 -12
- package/lib/number.js +0 -12
- package/lib/primitive/is-float.d.ts +3 -4
- package/lib/primitive/is-float.js +3 -4
- package/lib/primitive/is-int.d.ts +0 -5
- package/lib/primitive/is-int.js +0 -5
- package/lib/primitive/is-non-zero-uint.d.ts +0 -6
- package/lib/primitive/is-non-zero-uint.js +0 -6
- package/lib/primitive/is-quantity.d.ts +0 -8
- package/lib/primitive/is-quantity.js +0 -8
- package/lib/primitive/is-uint.d.ts +0 -6
- package/lib/primitive/is-uint.js +0 -6
- package/lib/primitive/range.d.ts +0 -8
- package/lib/primitive/range.js +0 -8
- package/lib/primitive/split-unit.d.ts +0 -6
- package/lib/primitive/split-unit.js +0 -6
- package/lib/rfc/is-bcp-47.d.ts +24 -1
- package/lib/rfc/is-bcp-47.js +88 -3
- package/lib/token/token-collection.d.ts +1 -111
- package/lib/token/token-collection.js +0 -108
- package/lib/token/token.d.ts +1 -73
- package/lib/token/token.js +0 -72
- package/lib/token/types.d.ts +0 -9
- package/lib/types.schema.d.ts +1 -1
- package/lib/w3c/check-content-security-policy.d.ts +18 -0
- package/lib/w3c/check-content-security-policy.js +363 -0
- package/lib/w3c/check-serialized-permissions-policy.d.ts +0 -2
- package/lib/w3c/check-serialized-permissions-policy.js +0 -2
- package/lib/whatwg/check-autocomplete.d.ts +26 -9
- package/lib/whatwg/check-autocomplete.js +42 -15
- package/lib/whatwg/check-datetime/date-string-with-optional-time.d.ts +11 -0
- package/lib/whatwg/check-datetime/date-string-with-optional-time.js +16 -0
- package/lib/whatwg/check-datetime/date-string.d.ts +0 -2
- package/lib/whatwg/check-datetime/date-string.js +0 -2
- package/lib/whatwg/check-datetime/datetime-tokens.d.ts +0 -13
- package/lib/whatwg/check-datetime/datetime-tokens.js +0 -16
- package/lib/whatwg/check-datetime/duration-string.d.ts +0 -4
- package/lib/whatwg/check-datetime/duration-string.js +0 -4
- package/lib/whatwg/check-datetime/global-date-and-time-string.d.ts +0 -2
- package/lib/whatwg/check-datetime/global-date-and-time-string.js +4 -4
- package/lib/whatwg/check-datetime/index.d.ts +0 -5
- package/lib/whatwg/check-datetime/index.js +0 -5
- package/lib/whatwg/check-datetime/local-date-and-time-string.d.ts +0 -4
- package/lib/whatwg/check-datetime/local-date-and-time-string.js +0 -4
- package/lib/whatwg/check-datetime/month-string.d.ts +0 -2
- package/lib/whatwg/check-datetime/month-string.js +0 -2
- package/lib/whatwg/check-datetime/time-string.d.ts +0 -2
- package/lib/whatwg/check-datetime/time-string.js +0 -2
- package/lib/whatwg/check-datetime/time-zone-offset-string.d.ts +0 -8
- package/lib/whatwg/check-datetime/time-zone-offset-string.js +0 -8
- package/lib/whatwg/check-datetime/week-string.d.ts +0 -2
- package/lib/whatwg/check-datetime/week-string.js +0 -2
- package/lib/whatwg/check-datetime/year-string.d.ts +0 -2
- package/lib/whatwg/check-datetime/year-string.js +0 -2
- package/lib/whatwg/check-datetime/yearless-date-string.d.ts +0 -2
- package/lib/whatwg/check-datetime/yearless-date-string.js +0 -2
- package/lib/whatwg/check-email.js +0 -2
- package/lib/whatwg/check-http-equiv-content-type.d.ts +12 -0
- package/lib/whatwg/check-http-equiv-content-type.js +32 -0
- package/lib/whatwg/check-http-equiv-refresh.d.ts +21 -0
- package/lib/whatwg/check-http-equiv-refresh.js +106 -0
- package/lib/whatwg/check-media-query-list.d.ts +61 -0
- package/lib/whatwg/check-media-query-list.js +484 -0
- package/lib/whatwg/check-mime-type.d.ts +0 -4
- package/lib/whatwg/check-mime-type.js +55 -3
- package/lib/whatwg/check-simple-color.d.ts +2 -4
- package/lib/whatwg/check-simple-color.js +2 -4
- package/lib/whatwg/check-url.d.ts +15 -7
- package/lib/whatwg/check-url.js +333 -25
- package/lib/whatwg/is-abs-url.d.ts +0 -2
- package/lib/whatwg/is-abs-url.js +0 -2
- package/lib/whatwg/is-browser-context-name.d.ts +0 -2
- package/lib/whatwg/is-browser-context-name.js +0 -2
- package/lib/whatwg/is-itemprop-name.d.ts +0 -2
- package/lib/whatwg/is-itemprop-name.js +0 -2
- package/lib/whatwg/is-navigable-target-name.d.ts +0 -2
- package/lib/whatwg/is-navigable-target-name.js +0 -2
- package/package.json +6 -5
- package/types.schema.json +9 -0
- package/ARCHITECTURE.ja.md +0 -256
- package/ARCHITECTURE.md +0 -256
- package/SKILL.md +0 -100
- package/docs/check-pipeline.ja.md +0 -494
- package/docs/check-pipeline.md +0 -494
- package/docs/token-system.ja.md +0 -584
- package/docs/token-system.md +0 -584
- package/docs/type-system.ja.md +0 -623
- package/docs/type-system.md +0 -623
- package/docs/validators.ja.md +0 -536
- package/docs/validators.md +0 -536
|
@@ -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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* -
|
|
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-
|
|
22
|
+
* @see https://url.spec.whatwg.org/#url-parsing
|
|
15
23
|
*/
|
|
16
24
|
export declare const checkURL: CustomSyntaxChecker;
|