@markuplint/types 4.8.1 → 4.8.2

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 (112) hide show
  1. package/ARCHITECTURE.ja.md +252 -0
  2. package/ARCHITECTURE.md +252 -0
  3. package/CHANGELOG.md +3 -3
  4. package/README.md +37 -2
  5. package/SKILL.md +100 -0
  6. package/docs/check-pipeline.ja.md +494 -0
  7. package/docs/check-pipeline.md +494 -0
  8. package/docs/token-system.ja.md +584 -0
  9. package/docs/token-system.md +584 -0
  10. package/docs/type-system.ja.md +556 -0
  11. package/docs/type-system.md +556 -0
  12. package/docs/validators.ja.md +514 -0
  13. package/docs/validators.md +514 -0
  14. package/lib/check-base.d.ts +71 -0
  15. package/lib/check-base.js +71 -0
  16. package/lib/check-multi-types.d.ts +12 -1
  17. package/lib/check-multi-types.js +11 -0
  18. package/lib/check.d.ts +13 -0
  19. package/lib/check.js +13 -0
  20. package/lib/css-defs.d.ts +8 -0
  21. package/lib/css-defs.js +8 -0
  22. package/lib/css-overrides.d.ts +7 -0
  23. package/lib/css-overrides.js +7 -0
  24. package/lib/css-syntax.d.ts +11 -0
  25. package/lib/css-syntax.js +12 -1
  26. package/lib/css-tokenizers.d.ts +6 -0
  27. package/lib/css-tokenizers.js +6 -0
  28. package/lib/debug.d.ts +3 -0
  29. package/lib/debug.js +3 -0
  30. package/lib/defs.d.ts +8 -0
  31. package/lib/defs.js +8 -0
  32. package/lib/directive.d.ts +14 -0
  33. package/lib/directive.js +14 -0
  34. package/lib/enum.d.ts +11 -0
  35. package/lib/enum.js +11 -0
  36. package/lib/get-candidate.d.ts +11 -0
  37. package/lib/get-candidate.js +11 -0
  38. package/lib/index.d.ts +6 -0
  39. package/lib/index.js +6 -0
  40. package/lib/keyword-type.d.ts +13 -0
  41. package/lib/keyword-type.js +13 -0
  42. package/lib/list.d.ts +13 -0
  43. package/lib/list.js +13 -0
  44. package/lib/match-result.d.ts +22 -1
  45. package/lib/match-result.js +21 -0
  46. package/lib/number.d.ts +12 -0
  47. package/lib/number.js +12 -0
  48. package/lib/primitive/is-float.d.ts +4 -1
  49. package/lib/primitive/is-float.js +4 -1
  50. package/lib/primitive/is-int.d.ts +4 -1
  51. package/lib/primitive/is-int.js +4 -1
  52. package/lib/primitive/is-non-zero-uint.d.ts +3 -2
  53. package/lib/primitive/is-non-zero-uint.js +3 -2
  54. package/lib/primitive/is-quantity.d.ts +5 -3
  55. package/lib/primitive/is-quantity.js +5 -3
  56. package/lib/primitive/is-uint.d.ts +5 -1
  57. package/lib/primitive/is-uint.js +5 -1
  58. package/lib/primitive/range.d.ts +5 -4
  59. package/lib/primitive/range.js +5 -4
  60. package/lib/primitive/split-unit.d.ts +3 -2
  61. package/lib/primitive/split-unit.js +3 -2
  62. package/lib/rfc/is-bcp-47.d.ts +2 -0
  63. package/lib/rfc/is-bcp-47.js +2 -0
  64. package/lib/token/token-collection.d.ts +107 -2
  65. package/lib/token/token-collection.js +103 -3
  66. package/lib/token/token.d.ts +66 -2
  67. package/lib/token/token.js +66 -2
  68. package/lib/token/types.d.ts +9 -0
  69. package/lib/types.d.ts +107 -0
  70. package/lib/w3c/check-serialized-permissions-policy.d.ts +2 -0
  71. package/lib/w3c/check-serialized-permissions-policy.js +2 -0
  72. package/lib/whatwg/check-autocomplete.d.ts +5 -0
  73. package/lib/whatwg/check-autocomplete.js +6 -0
  74. package/lib/whatwg/check-datetime/date-string.d.ts +2 -0
  75. package/lib/whatwg/check-datetime/date-string.js +2 -0
  76. package/lib/whatwg/check-datetime/datetime-tokens.d.ts +13 -0
  77. package/lib/whatwg/check-datetime/datetime-tokens.js +13 -0
  78. package/lib/whatwg/check-datetime/duration-string.d.ts +7 -0
  79. package/lib/whatwg/check-datetime/duration-string.js +7 -0
  80. package/lib/whatwg/check-datetime/global-date-and-time-string.d.ts +2 -0
  81. package/lib/whatwg/check-datetime/global-date-and-time-string.js +2 -0
  82. package/lib/whatwg/check-datetime/index.d.ts +5 -0
  83. package/lib/whatwg/check-datetime/index.js +5 -0
  84. package/lib/whatwg/check-datetime/local-date-and-time-string.d.ts +4 -0
  85. package/lib/whatwg/check-datetime/local-date-and-time-string.js +4 -0
  86. package/lib/whatwg/check-datetime/month-string.d.ts +2 -0
  87. package/lib/whatwg/check-datetime/month-string.js +2 -0
  88. package/lib/whatwg/check-datetime/time-string.d.ts +2 -0
  89. package/lib/whatwg/check-datetime/time-string.js +2 -0
  90. package/lib/whatwg/check-datetime/time-zone-offset-string.d.ts +8 -0
  91. package/lib/whatwg/check-datetime/time-zone-offset-string.js +8 -0
  92. package/lib/whatwg/check-datetime/week-string.d.ts +2 -0
  93. package/lib/whatwg/check-datetime/week-string.js +2 -0
  94. package/lib/whatwg/check-datetime/year-string.d.ts +2 -0
  95. package/lib/whatwg/check-datetime/year-string.js +2 -0
  96. package/lib/whatwg/check-datetime/yearless-date-string.d.ts +2 -0
  97. package/lib/whatwg/check-datetime/yearless-date-string.js +2 -0
  98. package/lib/whatwg/check-link-type.d.ts +5 -1
  99. package/lib/whatwg/check-link-type.js +5 -1
  100. package/lib/whatwg/check-mime-type.d.ts +4 -1
  101. package/lib/whatwg/check-mime-type.js +4 -1
  102. package/lib/whatwg/is-abs-url.d.ts +2 -0
  103. package/lib/whatwg/is-abs-url.js +2 -0
  104. package/lib/whatwg/is-browser-context-name.d.ts +2 -2
  105. package/lib/whatwg/is-browser-context-name.js +2 -2
  106. package/lib/whatwg/is-custom-element-name.d.ts +1 -1
  107. package/lib/whatwg/is-custom-element-name.js +1 -1
  108. package/lib/whatwg/is-itemprop-name.d.ts +1 -0
  109. package/lib/whatwg/is-itemprop-name.js +1 -0
  110. package/lib/whatwg/is-navigable-target-name.d.ts +2 -0
  111. package/lib/whatwg/is-navigable-target-name.js +2 -0
  112. package/package.json +3 -3
@@ -0,0 +1,556 @@
1
+ # Type System
2
+
3
+ ## Overview
4
+
5
+ The `@markuplint/types` package provides a type system for validating HTML attribute values, CSS property values, and custom syntax definitions. It forms the foundation of markuplint's attribute value checking, supporting everything from simple enumerated attributes to complex CSS value definition syntax. The type system is designed to be extensible: built-in types cover HTML and CSS standards, while custom types can be registered for framework-specific or project-specific needs.
6
+
7
+ ## Type Union
8
+
9
+ The core of the type system is a five-member union type called `Type`, defined in `src/types.schema.ts`:
10
+
11
+ ```ts
12
+ // src/types.schema.ts
13
+ export type Type = KeywordDefinedType | List | Enum | Number | Directive;
14
+ ```
15
+
16
+ Every attribute value specification in markuplint resolves to one of these five forms. The dispatcher in `src/check-base.ts` identifies which variant is being used and routes to the corresponding checker:
17
+
18
+ ```ts
19
+ // src/check-base.ts
20
+ export function checkBase(value: string, type: ReadonlyDeep<Type>, defs: Defs, ref?: string, cache = true): Result {
21
+ if (isKeyword(type)) return checkKeywordType(value, type, defs, cache);
22
+ if (isList(type)) return checkList(value, type, defs, ref, cache);
23
+ if (isEnum(type)) return checkEnum(value, type, ref);
24
+ if (isNumber(type)) return checkNumber(value, type, ref);
25
+ if (isDirective(type)) return checkDirective(value, type, defs, ref, cache);
26
+ throw new Error('Unknown type');
27
+ }
28
+ ```
29
+
30
+ ### Summary Table
31
+
32
+ | Type | Representation | Discriminant | Purpose | Example |
33
+ | ---------------------- | -------------- | ------------------------------------ | -------------------------------------------------- | ---------------------------------------- |
34
+ | **KeywordDefinedType** | `string` | `typeof type === 'string'` | CSS syntax, extended types, HTML attr requirements | `"<color>"`, `"URL"`, `"Boolean"` |
35
+ | **List** | `object` | `'separator' in type` | Space- or comma-separated token sequences | `{ token: "DOMID", separator: "space" }` |
36
+ | **Enum** | `object` | `'enum' in type` | Fixed set of allowed string values | `{ enum: ["auto", "ltr", "rtl"] }` |
37
+ | **Number** | `object` | `type.type === 'float' \| 'integer'` | Numeric values with optional range constraints | `{ type: "integer", gte: 0 }` |
38
+ | **Directive** | `object` | `'directive' in type` | Composite attribute values with separators | `{ directive: [";"], token: "URL" }` |
39
+
40
+ ### KeywordDefinedType
41
+
42
+ A keyword type is a plain string that references a named type definition. It is itself a union of three sub-categories:
43
+
44
+ ```ts
45
+ // src/types.schema.ts
46
+ export type KeywordDefinedType = CssSyntax | ExtendedType | HtmlAttrRequirement;
47
+ ```
48
+
49
+ - **CssSyntax** -- CSS value definition syntax names sourced from css-tree (e.g., `"<color>"`, `"<'display'>"`, `"<length-percentage>"`). There are hundreds of these, covering all standard CSS properties and value types.
50
+ - **ExtendedType** -- Custom type identifiers defined in `src/defs.ts` and `src/css-defs.ts` (e.g., `"URL"`, `"DOMID"`, `"DateTime"`, `"<view-box>"`). These handle HTML-specific formats that are not expressible as pure CSS syntax.
51
+ - **HtmlAttrRequirement** -- Currently only `"Boolean"`, representing HTML boolean attributes.
52
+
53
+ When the type checker encounters a keyword string, it looks it up in the `Defs` registry. If found, it uses the registered checker. If not found, it falls back to css-tree's lexer for CSS syntax matching.
54
+
55
+ ### List
56
+
57
+ A `List` defines a token-separated sequence of values:
58
+
59
+ ```ts
60
+ // src/types.schema.ts
61
+ export interface List {
62
+ token: ExtendedType | Enum;
63
+ separator: 'space' | 'comma';
64
+ disallowToSurroundBySpaces?: boolean;
65
+ allowEmpty?: boolean;
66
+ ordered?: boolean;
67
+ unique?: boolean;
68
+ caseInsensitive?: boolean;
69
+ number?: ('zeroOrMore' | 'oneOrMore') | { min: number; max: number };
70
+ }
71
+ ```
72
+
73
+ This maps directly to the WHATWG specification concepts of [space-separated tokens](https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#space-separated-tokens) and [comma-separated tokens](https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#comma-separated-tokens).
74
+
75
+ **Example:** The `class` attribute uses a space-separated list of tokens:
76
+
77
+ ```json
78
+ {
79
+ "token": "NoEmptyAny",
80
+ "separator": "space",
81
+ "unique": true
82
+ }
83
+ ```
84
+
85
+ ### Enum
86
+
87
+ An `Enum` defines an [enumerated attribute](https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#enumerated-attribute):
88
+
89
+ ```ts
90
+ // src/types.schema.ts
91
+ export interface Enum {
92
+ enum: [string, ...string[]];
93
+ disallowToSurroundBySpaces?: boolean;
94
+ caseInsensitive?: boolean;
95
+ invalidValueDefault?: string;
96
+ missingValueDefault?: string;
97
+ sameStates?: { [k: string]: unknown };
98
+ }
99
+ ```
100
+
101
+ The `invalidValueDefault` and `missingValueDefault` fields model the WHATWG concept of invalid/missing value defaults. The `sameStates` field groups different keywords that map to the same internal state.
102
+
103
+ **Example:** The `dir` attribute:
104
+
105
+ ```json
106
+ {
107
+ "enum": ["ltr", "rtl", "auto"],
108
+ "caseInsensitive": true,
109
+ "missingValueDefault": "",
110
+ "invalidValueDefault": ""
111
+ }
112
+ ```
113
+
114
+ ### Number
115
+
116
+ A `Number` validates numeric attribute values with optional range constraints:
117
+
118
+ ```ts
119
+ // src/types.schema.ts
120
+ export interface Number {
121
+ type: 'float' | 'integer';
122
+ gt?: number;
123
+ gte?: number;
124
+ lt?: number;
125
+ lte?: number;
126
+ clampable?: boolean;
127
+ }
128
+ ```
129
+
130
+ The `gt`/`gte`/`lt`/`lte` fields define open or closed range boundaries. The `clampable` flag indicates whether out-of-range values are silently clamped by the browser (some HTML attributes do this).
131
+
132
+ **Example:** The `width` attribute on `<canvas>`:
133
+
134
+ ```json
135
+ {
136
+ "type": "integer",
137
+ "gte": 0
138
+ }
139
+ ```
140
+
141
+ ### Directive
142
+
143
+ A `Directive` handles composite attribute values where a separator string splits the value into individual tokens, each validated independently:
144
+
145
+ ```ts
146
+ // src/types.schema.ts
147
+ export interface Directive {
148
+ directive: [string, ...string[]];
149
+ token: Type;
150
+ ref?: string;
151
+ }
152
+ ```
153
+
154
+ The `directive` array lists one or more separator strings. Each segment produced by splitting on these separators is validated against the `token` type.
155
+
156
+ **Example:** A hypothetical attribute with semicolon-separated URLs:
157
+
158
+ ```json
159
+ {
160
+ "directive": [";"],
161
+ "token": "URL"
162
+ }
163
+ ```
164
+
165
+ ## Result Types
166
+
167
+ Every type check returns a `Result`, which is a discriminated union defined in `src/types.ts`:
168
+
169
+ ```ts
170
+ // src/types.ts
171
+ export type Result = UnmatchedResult | MatchedResult;
172
+ ```
173
+
174
+ ### MatchedResult
175
+
176
+ A successful validation returns this minimal object:
177
+
178
+ ```ts
179
+ // src/types.ts
180
+ export type MatchedResult = {
181
+ readonly matched: true;
182
+ };
183
+ ```
184
+
185
+ Created via the `matched()` factory in `src/match-result.ts`:
186
+
187
+ ```ts
188
+ // src/match-result.ts
189
+ export function matched(): MatchedResult {
190
+ return { matched: true };
191
+ }
192
+ ```
193
+
194
+ ### UnmatchedResult
195
+
196
+ A failed validation returns a detailed object:
197
+
198
+ ```ts
199
+ // src/types.ts
200
+ export type UnmatchedResult = {
201
+ readonly matched: false;
202
+ readonly ref: string | null;
203
+ readonly raw: string;
204
+ readonly length: number;
205
+ readonly offset: number;
206
+ readonly line: number;
207
+ readonly column: number;
208
+ readonly reason: UnmatchedResultReason;
209
+ readonly passCount?: number;
210
+ } & UnmatchedResultOptions;
211
+ ```
212
+
213
+ | Field | Description |
214
+ | ----------- | ------------------------------------------------------------- |
215
+ | `matched` | Always `false` |
216
+ | `ref` | Reference URL to the relevant specification, or `null` |
217
+ | `raw` | The raw string value that failed validation |
218
+ | `length` | The length of the raw value |
219
+ | `offset` | Character offset within the input where the mismatch occurred |
220
+ | `line` | Line number of the mismatch (1-based) |
221
+ | `column` | Column number of the mismatch (1-based) |
222
+ | `reason` | A reason code or structured object explaining the failure |
223
+ | `passCount` | Optional count of tokens that passed before the failure |
224
+
225
+ ### UnmatchedResultOptions
226
+
227
+ Additional metadata can be attached:
228
+
229
+ ```ts
230
+ // src/types.ts
231
+ export type UnmatchedResultOptions = {
232
+ readonly partName?: string;
233
+ readonly expects?: readonly Expect[];
234
+ readonly extra?: Expect;
235
+ readonly candidate?: string;
236
+ readonly fallbackTo?: string;
237
+ };
238
+ ```
239
+
240
+ - `partName` -- The name of the sub-part that failed (e.g., "width descriptor" in srcset)
241
+ - `expects` -- An array of `Expect` objects describing what the validator expected
242
+ - `candidate` -- A suggested correction (used for typo detection, e.g., a misspelled target name -> `"_blank"`)
243
+ - `fallbackTo` -- The value the browser would fall back to in the presence of this error
244
+
245
+ ### UnmatchedResultReason
246
+
247
+ The reason is either a string literal or a structured range violation:
248
+
249
+ ```ts
250
+ // src/types.ts
251
+ export type UnmatchedResultReason =
252
+ | 'syntax-error'
253
+ | 'typo'
254
+ | 'missing-token'
255
+ | 'missing-comma'
256
+ | 'unexpected-token'
257
+ | 'unexpected-space'
258
+ | 'unexpected-newline'
259
+ | 'unexpected-comma'
260
+ | 'empty-token'
261
+ | 'out-of-range'
262
+ | 'doesnt-exist-in-enum'
263
+ | 'duplicated'
264
+ | 'illegal-combination'
265
+ | 'illegal-order'
266
+ | 'extra-token'
267
+ | 'must-be-percent-encoded'
268
+ | 'must-be-serialized'
269
+ | {
270
+ readonly type: 'out-of-range';
271
+ readonly gt?: number;
272
+ readonly gte?: number;
273
+ readonly lt?: number;
274
+ readonly lte?: number;
275
+ }
276
+ | { readonly type: 'out-of-range-length-char'; readonly gte: number; readonly lte?: number }
277
+ | { readonly type: 'out-of-range-length-digit'; readonly gte: number; readonly lte?: number };
278
+ ```
279
+
280
+ ### Result Flow Diagram
281
+
282
+ ```mermaid
283
+ flowchart TD
284
+ A[Input Value + Type Definition] --> B{checkBase}
285
+ B -->|string| C[checkKeywordType]
286
+ B -->|separator| D[checkList]
287
+ B -->|enum| E[checkEnum]
288
+ B -->|type: float/integer| F[checkNumber]
289
+ B -->|directive| G[checkDirective]
290
+
291
+ C --> H{Lookup in Defs}
292
+ H -->|Found: CustomSyntax| I[Run is function]
293
+ H -->|Found: CustomCssSyntax| J[cssSyntaxMatch]
294
+ H -->|Not found| J
295
+
296
+ I --> K{Valid?}
297
+ J --> K
298
+ D --> K
299
+ E --> K
300
+ F --> K
301
+ G --> K
302
+
303
+ K -->|Yes| L["matched() -> MatchedResult"]
304
+ K -->|No| M["unmatched() -> UnmatchedResult"]
305
+
306
+ L --> N[Result]
307
+ M --> N
308
+ ```
309
+
310
+ ## Defs Registry
311
+
312
+ The `Defs` type maps type identifier strings to their validation implementations:
313
+
314
+ ```ts
315
+ // src/types.ts
316
+ export type Defs = Readonly<Record<string, CustomCssSyntax | CustomSyntax>>;
317
+ ```
318
+
319
+ Each entry is either a `CustomSyntax` (using an imperative `is` function) or a `CustomCssSyntax` (using CSS value definition syntax).
320
+
321
+ ### CustomSyntax
322
+
323
+ ```ts
324
+ // src/types.ts
325
+ export type CustomSyntax = {
326
+ readonly ref: string;
327
+ readonly expects?: readonly Expect[];
328
+ readonly is: CustomSyntaxCheck;
329
+ };
330
+ ```
331
+
332
+ **Example from `src/defs.ts`:**
333
+
334
+ ```ts
335
+ DOMID: {
336
+ ref: 'https://html.spec.whatwg.org/multipage/dom.html#global-attributes:concept-id',
337
+ expects: [{ type: 'format', value: 'ID' }],
338
+ is: value => {
339
+ const tokens = new TokenCollection(value);
340
+ const ws = tokens.search(Token.WhiteSpace);
341
+ if (ws) {
342
+ return ws.unmatched({ reason: 'unexpected-space' });
343
+ }
344
+ if (tokens.length === 0) {
345
+ return unmatched(value, 'empty-token');
346
+ }
347
+ return matched();
348
+ },
349
+ },
350
+ ```
351
+
352
+ ### CustomCssSyntax
353
+
354
+ ```ts
355
+ // src/types.ts
356
+ export type CustomCssSyntax = {
357
+ readonly ref: string;
358
+ readonly caseSensitive?: boolean;
359
+ readonly expects?: readonly Expect[];
360
+ readonly syntax: {
361
+ readonly apply: `<${string}>`;
362
+ readonly def: Readonly<Record<string, string | CssSyntaxTokenizer>>;
363
+ };
364
+ };
365
+ ```
366
+
367
+ **Example from `src/defs.ts`:**
368
+
369
+ ```ts
370
+ SourceSizeList: {
371
+ ref: 'https://html.spec.whatwg.org/multipage/images.html#sizes-attributes',
372
+ expects: [{ type: 'syntax', value: '<source-size-list>' }],
373
+ syntax: {
374
+ apply: '<source-size-list>',
375
+ def: {
376
+ 'source-size-list': '[ <source-size># , ]? <source-size-value>',
377
+ 'source-size': '<media-condition> <source-size-value> | auto',
378
+ 'source-size-value': '<length> | auto',
379
+ },
380
+ },
381
+ },
382
+ ```
383
+
384
+ ### Key Built-in Types
385
+
386
+ The `defs` object in `src/defs.ts` registers over 30 built-in types. Here are some notable entries:
387
+
388
+ | Type Identifier | Validation Method | Specification |
389
+ | ------------------- | -------------------------- | ---------------------- |
390
+ | `Any` | Always matches | -- |
391
+ | `NoEmptyAny` | Rejects empty strings | -- |
392
+ | `Number` | Floating-point check | -- |
393
+ | `Int` | Integer check | -- |
394
+ | `Uint` | Non-negative integer check | -- |
395
+ | `URL` | Always matches (see note) | WHATWG URL |
396
+ | `DOMID` | No whitespace, non-empty | HTML #id |
397
+ | `DateTime` | Full datetime parsing | WHATWG datetime |
398
+ | `BCP47` | RFC BCP 47 language tag | IETF BCP 47 |
399
+ | `CustomElementName` | Valid custom element name | WHATWG Custom Elements |
400
+ | `MIMEType` | MIME type parsing | MIME Sniffing |
401
+ | `SourceSizeList` | CSS syntax-based | HTML `<img sizes>` |
402
+ | `AutoComplete` | Complex multi-token | HTML autocomplete |
403
+
404
+ > **Note:** The `URL` type always matches because relative URLs accept almost any string. To validate URL format strictly, use `AbsoluteURL` or `HTTPSchemaURL` instead.
405
+
406
+ ### How the Registry is Used
407
+
408
+ When `check()` is called (the main entry point in `src/check.ts`), it merges the HTML definitions (`defs`) and CSS definitions (`cssDefs`) into a single registry:
409
+
410
+ ```ts
411
+ // src/check.ts
412
+ export function check(value: string, type: ReadonlyDeep<Type>, ref?: string, cache = true): Result {
413
+ return checkBase(value, type, { ...defs, ...cssDefs }, ref, cache);
414
+ }
415
+ ```
416
+
417
+ ## Schema Generation
418
+
419
+ The type system's type definitions are available both as TypeScript types and as a JSON Schema, enabling IDE autocompletion and configuration file validation.
420
+
421
+ ### Generation Flow
422
+
423
+ ```mermaid
424
+ flowchart LR
425
+ A["gen/specific-schema.json<br/>(List, Enum, Number, Directive)"] --> D["gen/types.ts<br/>(generator script)"]
426
+ B["css-tree lexer<br/>(CSS properties + types)"] --> D
427
+ C["src/defs.ts + src/css-defs.ts<br/>(extended types)"] --> D
428
+ E["src/css-tokenizers.ts<br/>(custom tokenizers)"] --> D
429
+ D --> F["types.schema.json<br/>(complete JSON Schema)"]
430
+ F -->|json-schema-to-typescript| G["src/types.schema.ts<br/>(TypeScript types)"]
431
+ ```
432
+
433
+ ### How It Works
434
+
435
+ 1. **`gen/specific-schema.json`** defines the JSON Schema for `List`, `Enum`, `Number`, and `Directive` -- the non-keyword type variants.
436
+
437
+ 2. **`gen/types.ts`** is the generator script. It:
438
+ - Reads all CSS property names and type names from css-tree's lexer
439
+ - Reads all extended type identifiers from `defs` and `cssDefs`
440
+ - Reads custom tokenizer names from `cssTokenizers`
441
+ - Combines everything into `types.schema.json`
442
+
443
+ 3. **`types.schema.json`** is the complete JSON Schema. Its `definitions` section contains:
444
+ - `css-syntax` -- All CSS property and type names as a string enum
445
+ - `extended-type` -- All custom type identifiers as a string enum
446
+ - `html-attr-requirement` -- Currently just `["Boolean"]`
447
+ - `keyword-defined-type` -- A `oneOf` of the three above
448
+ - `list`, `enum`, `number`, `directive` -- From `specific-schema.json`
449
+ - `type` -- A `oneOf` of all five type variants
450
+
451
+ 4. **`src/types.schema.ts`** is generated from `types.schema.json` using `json-schema-to-typescript`. This file exports the TypeScript types (`Type`, `List`, `Enum`, `Number`, `Directive`, `KeywordDefinedType`, `CssSyntax`, `ExtendedType`, `HtmlAttrRequirement`) that the rest of the codebase imports.
452
+
453
+ ### Purpose
454
+
455
+ - **Configuration validation:** The JSON Schema is referenced by markuplint's configuration schema, providing autocompletion and validation when users edit `.markuplintrc` files in their IDE.
456
+ - **Type safety:** The generated TypeScript types ensure that the codebase can only reference valid type identifiers at compile time.
457
+ - **Single source of truth:** CSS-tree's lexer database and the custom definitions in `defs.ts`/`cssDefs.ts` are the authoritative sources; the schema and TypeScript types are always derived from them.
458
+
459
+ ## CSS Definitions
460
+
461
+ The type system integrates deeply with CSS through three modules that extend css-tree's capabilities.
462
+
463
+ ### cssDefs (`src/css-defs.ts`)
464
+
465
+ The `cssDefs` registry provides type definitions for CSS and SVG attribute values that go beyond standard CSS property syntax. It follows the same `Defs` structure as the main `defs` registry.
466
+
467
+ Key categories:
468
+
469
+ | Category | Examples | Description |
470
+ | --------------------- | ---------------------------------------------------- | ------------------------------ |
471
+ | CSS Declaration Lists | `<css-declaration-list>` | For the `style` attribute |
472
+ | SVG Geometry | `<view-box>`, `<preserve-aspect-ratio>`, `<points>` | SVG coordinate/geometry types |
473
+ | SVG Painting | `<dasharray>`, `<color-matrix>` | SVG paint/filter-related types |
474
+ | SVG Animation | `<key-splines>`, `<key-times>`, `<begin-value-list>` | SMIL animation types |
475
+ | SVG Text | `<text-coordinate>`, `<list-of-lengths>` | SVG text positioning |
476
+
477
+ Most definitions use `CustomCssSyntax` with a `syntax` property that specifies css-tree grammar:
478
+
479
+ ```ts
480
+ // src/css-defs.ts
481
+ '<preserve-aspect-ratio>': {
482
+ ref: 'https://svgwg.org/svg2-draft/coords.html#PreserveAspectRatioAttribute',
483
+ syntax: {
484
+ apply: '<preserve-aspect-ratio>',
485
+ def: {
486
+ 'preserve-aspect-ratio': '<align> <meet-or-slice>?',
487
+ align: 'none | xMinYMin | xMidYMin | xMaxYMin | xMinYMid | xMidYMid | xMaxYMid | xMinYMax | xMidYMax | xMaxYMax',
488
+ 'meet-or-slice': 'meet | slice',
489
+ },
490
+ },
491
+ },
492
+ ```
493
+
494
+ Some entries use `CustomSyntax` with an `is` function when CSS syntax alone is insufficient (e.g., `<svg-font-size>` which is marked as TODO and currently always matches).
495
+
496
+ ### cssOverrides (`src/css-overrides.ts`)
497
+
498
+ The `cssOverrides` map provides alternative CSS syntax definitions that replace css-tree's built-in definitions for specific CSS value types. This is necessary for SVG attribute validation, where CSS syntax rules are more permissive than in stylesheets.
499
+
500
+ ```ts
501
+ // src/css-overrides.ts
502
+ export const cssOverrides: Record<string, string> = {
503
+ 'legacy-length-percentage': '<length> | <percentage> | <svg-length>',
504
+ 'legacy-angle': '<angle> | <zero> | <number>',
505
+ 'translate()': 'translate( <legacy-length-percentage> , <legacy-length-percentage>? ) | ...',
506
+ 'scale()': 'scale( [ <number> | <percentage> ]#{1,2} )',
507
+ 'rotate()': 'rotate( <legacy-angle> )',
508
+ 'skew()': 'skew( <legacy-angle> , <legacy-angle>? ) | ...',
509
+ };
510
+ ```
511
+
512
+ The overrides introduce `<legacy-length-percentage>` and `<legacy-angle>` aliases that accept unitless numbers (valid in SVG transform attributes but not in CSS stylesheets). These overrides are fed into css-tree's `fork()` function to create a custom lexer.
513
+
514
+ ### cssTokenizers (`src/css-tokenizers.ts`)
515
+
516
+ The `cssTokenizers` map provides custom token-level matching functions for CSS value types that require logic beyond what css-tree's grammar supports:
517
+
518
+ ```ts
519
+ // src/css-tokenizers.ts
520
+ export const cssTokenizers: Record<string, CssSyntaxTokenizer> = {
521
+ 'bcp-47'(token) {
522
+ if (!token) return 0;
523
+ return isBCP47()(token.value) ? 1 : 0;
524
+ },
525
+ };
526
+ ```
527
+
528
+ A `CssSyntaxTokenizer` is a function that receives:
529
+
530
+ - `token` -- The current CSS syntax token (or `null` at end of input)
531
+ - `getNextToken` -- A lookahead function for inspecting subsequent tokens
532
+ - `match` -- The `cssSyntaxMatch` function for recursive matching
533
+
534
+ It returns the number of tokens consumed (0 if no match). This allows integrating non-CSS validation logic (like BCP 47 language tag checking) into CSS syntax grammar rules.
535
+
536
+ ### Integration Flow
537
+
538
+ ```mermaid
539
+ flowchart TD
540
+ A[check function] --> B{Type is keyword?}
541
+ B -->|Yes| C{In defs or cssDefs?}
542
+ C -->|CustomSyntax| D["Run is() function"]
543
+ C -->|CustomCssSyntax| E[cssSyntaxMatch]
544
+ C -->|Not found| E
545
+
546
+ E --> F["css-tree fork()"]
547
+ F --> G[cssOverrides applied]
548
+ F --> H[cssTokenizers registered]
549
+ G --> I[Custom Lexer]
550
+ H --> I
551
+ I --> J[Match against grammar]
552
+ J --> K[Result]
553
+ D --> K
554
+ ```
555
+
556
+ The `cssSyntaxMatch` function in `src/css-syntax.ts` orchestrates this process: it creates a forked css-tree lexer with the overrides and custom tokenizers applied, then validates the value against the specified CSS syntax grammar.