@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,584 @@
1
+ # Token System
2
+
3
+ ## Overview
4
+
5
+ The `@markuplint/types` package includes a token system that enables **position-tracked validation** of HTML attribute values. When markuplint validates structured string values -- such as datetime strings, color codes, or space/comma-separated lists -- it needs to report errors at the exact character position where a problem occurs, not just that the entire value is invalid.
6
+
7
+ ### The Problem
8
+
9
+ Consider validating the attribute value `"red, , blue"` as a comma-separated list. A naive validator might report "invalid value" for the whole string. But the actual problem is the empty item between the two commas at offset 5. The token system solves this by breaking the input into discrete tokens, each carrying its offset, line, and column within the original string. When validation fails on a specific token, the error message points to the precise location.
10
+
11
+ ### How It Fits Into the Check Pipeline
12
+
13
+ The typical validation flow is:
14
+
15
+ 1. An attribute value string enters the type-checking pipeline.
16
+ 2. The string is **tokenized** into a `TokenCollection` (either by constructor parsing or `fromPatterns`).
17
+ 3. Structural checks run against the collection (e.g., no consecutive commas, no leading separator).
18
+ 4. Individual tokens are validated against expected patterns using `eachCheck` or per-token type checks.
19
+ 5. If any check fails, an `UnmatchedResult` is returned carrying the exact `offset`, `line`, and `column`.
20
+
21
+ ```mermaid
22
+ sequenceDiagram
23
+ participant Input as Attribute Value
24
+ participant TC as TokenCollection
25
+ participant Check as Validator
26
+ participant Result as Result
27
+
28
+ Input->>TC: Tokenize string
29
+ TC->>TC: Assign offset/line/column to each Token
30
+ TC->>Check: Structural check (check())
31
+ Check-->>Result: UnmatchedResult (if structure invalid)
32
+ TC->>Check: Per-token validation (eachCheck())
33
+ Check-->>Result: UnmatchedResult with position (if token invalid)
34
+ Check-->>Result: MatchedResult (if all tokens valid)
35
+ ```
36
+
37
+ ## Token Class
38
+
39
+ **Source:** `src/token/token.ts`
40
+
41
+ The `Token` class represents a single parsed fragment of a string value. Each token knows its string value, its type (whitespace, comma, or identifier), and its byte offset within the original input.
42
+
43
+ ### Type Numbers
44
+
45
+ Token types are numeric constants borrowed from the [csstree tokenizer](https://github.com/csstree/csstree/blob/master/lib/tokenizer/types.js):
46
+
47
+ | Constant | Value | Description |
48
+ | ------------------ | ----- | -------------------------------------------------------- |
49
+ | `Token.Ident` | `1` | An identifier or general content token |
50
+ | `Token.WhiteSpace` | `13` | One or more ASCII whitespace characters |
51
+ | `Token.Comma` | `18` | A comma character (only when comma is a known separator) |
52
+
53
+ The type is determined automatically by `Token.getType()` based on the first character of the token value and the configured separators. If the first character is ASCII whitespace, the type is `WhiteSpace`. If it matches a configured separator (e.g., `,`), it receives the corresponding type. Otherwise, it defaults to `Ident`.
54
+
55
+ ### Static Properties
56
+
57
+ #### `Token.whitespace`
58
+
59
+ A readonly array of ASCII whitespace characters as defined by the [WHATWG Infra Standard](https://infra.spec.whatwg.org/#ascii-whitespace):
60
+
61
+ ```ts
62
+ static readonly whitespace: ReadonlyArray<string> = [
63
+ '\u0009', // TAB
64
+ '\u000A', // LF
65
+ '\u000C', // FF
66
+ '\u000D', // CR
67
+ '\u0020', // SPACE
68
+ ];
69
+ ```
70
+
71
+ ### Static Methods
72
+
73
+ #### `Token.getType(value, separators?)`
74
+
75
+ Determines the token type number from the first character of `value`.
76
+
77
+ ```ts
78
+ Token.getType(' hello'); // => 13 (WhiteSpace)
79
+ Token.getType(',', [',']); // => 18 (Comma)
80
+ Token.getType('blue'); // => 1 (Ident)
81
+ ```
82
+
83
+ #### `Token.getPosition(value, offset)`
84
+
85
+ Calculates the 1-based line and column at the given character offset within a string.
86
+
87
+ ```ts
88
+ Token.getPosition('abc\ndef', 5);
89
+ // => { line: 2, column: 2 }
90
+ ```
91
+
92
+ #### `Token.shiftLocation(token, offset)`
93
+
94
+ Computes a new position by adding `offset` to a token's existing offset, then recalculating the line and column from the original string. This is used to pinpoint errors within a token's value (e.g., the third character of a five-character token).
95
+
96
+ ```ts
97
+ // Given a token at offset 10 in the original string:
98
+ Token.shiftLocation(token, 3);
99
+ // => { offset: 13, line: ..., column: ... }
100
+ ```
101
+
102
+ ### Constructor
103
+
104
+ ```ts
105
+ new Token(value: string, offset: number, originalValue: string, separators?: readonly string[])
106
+ ```
107
+
108
+ | Parameter | Description |
109
+ | --------------- | ---------------------------------------------------------- |
110
+ | `value` | The token's string content |
111
+ | `offset` | The character offset within the original string |
112
+ | `originalValue` | The complete original string this token was extracted from |
113
+ | `separators` | Optional separator characters used for type detection |
114
+
115
+ ### Instance Properties
116
+
117
+ | Property | Type | Description |
118
+ | --------------- | -------- | --------------------------------------------------- |
119
+ | `value` | `string` | The token's string content |
120
+ | `type` | `number` | The token type (`Ident`, `WhiteSpace`, or `Comma`) |
121
+ | `offset` | `number` | The 0-based character offset in the original string |
122
+ | `originalValue` | `string` | The full original string this token was parsed from |
123
+ | `length` | `number` | The character length of `value` (computed getter) |
124
+
125
+ ### Instance Methods
126
+
127
+ #### `matches(value, caseInsensitive?)`
128
+
129
+ Checks whether the token **exactly** matches the given value. Accepts a string (exact match), a `RegExp` (test), a type number (type check), or an array of any of these (logical OR).
130
+
131
+ ```ts
132
+ token.matches('red'); // exact string match
133
+ token.matches(/^\d{4}$/); // regex match
134
+ token.matches(Token.WhiteSpace); // type match
135
+ token.matches(['T', ' ']); // matches either 'T' or ' '
136
+ ```
137
+
138
+ #### `includes(value, caseInsensitive?)`
139
+
140
+ Like `matches`, but uses substring containment (`String.includes`) instead of exact equality for string values. For RegExp and type numbers, behavior is identical to `matches`.
141
+
142
+ #### `unmatched(options?)`
143
+
144
+ Creates an `UnmatchedResult` positioned at this token's location. This is the primary way validation errors are generated with accurate position information.
145
+
146
+ ```ts
147
+ token.unmatched({
148
+ reason: 'unexpected-token',
149
+ expects: [{ type: 'common', value: 'hyphen' }],
150
+ partName: 'datetime',
151
+ });
152
+ // => { matched: false, raw: token.value, offset: token.offset, line: ..., column: ..., ... }
153
+ ```
154
+
155
+ #### `toNumber()`
156
+
157
+ Parses the token value as a floating-point number (returns `0` if parsing fails).
158
+
159
+ #### `clone()`
160
+
161
+ Returns a new `Token` with the same value, offset, and original value.
162
+
163
+ #### `toJSON()`
164
+
165
+ Returns a plain object `{ type, value, offset }` suitable for serialization and test assertions.
166
+
167
+ ## TokenCollection
168
+
169
+ **Source:** `src/token/token-collection.ts`
170
+
171
+ `TokenCollection` extends `Array<Token>` with parsing, structural validation, and query capabilities. It is the primary entry point for tokenizing a string value.
172
+
173
+ ### Class Diagram
174
+
175
+ ```mermaid
176
+ classDiagram
177
+ class Token {
178
+ +number type
179
+ +string value
180
+ +number offset
181
+ +string originalValue
182
+ +number length
183
+ +matches(value, caseInsensitive?) boolean
184
+ +includes(value, caseInsensitive?) boolean
185
+ +unmatched(options?) UnmatchedResult
186
+ +toNumber() number
187
+ +clone() Token
188
+ +toJSON() object
189
+ }
190
+ class Token {
191
+ +WhiteSpace$ = 13
192
+ +Comma$ = 18
193
+ +Ident$ = 1
194
+ +whitespace$ ReadonlyArray~string~
195
+ +getType(value, separators?)$ number
196
+ +getPosition(value, offset)$ object
197
+ +shiftLocation(token, offset)$ object
198
+ }
199
+ class TokenCollection {
200
+ +boolean allowEmpty
201
+ +boolean caseInsensitive
202
+ +boolean disallowToSurroundBySpaces
203
+ +boolean ordered
204
+ +string separator
205
+ +boolean unique
206
+ +string value
207
+ +check(options?) Result
208
+ +eachCheck(...callbacks) Result
209
+ +divide(position) Tuple
210
+ +chunk(split) TokenCollection[]
211
+ +search(value) Token?
212
+ +filter(callback) TokenCollection
213
+ +headAndTail() HeadAndTail
214
+ +getIdentTokens() TokenCollection
215
+ +getConsecutiveToken(type) Token?
216
+ +getDuplicated() Token?
217
+ +has(value) boolean
218
+ +takeTurns(types, lastType?) object?
219
+ +toJSON() object[]
220
+ }
221
+ class TokenCollection {
222
+ +fromPatterns(value, patterns, options?)$ TokenCollection
223
+ }
224
+ TokenCollection *-- Token : contains
225
+ TokenCollection --|> Array~Token~ : extends
226
+ ```
227
+
228
+ ### Creating Collections
229
+
230
+ #### Constructor: `new TokenCollection(value, options?)`
231
+
232
+ Parses a string into tokens by splitting on whitespace and (optionally) commas. The tokenization respects the configured `separator` mode.
233
+
234
+ ```ts
235
+ // Space-separated (default)
236
+ const tokens = new TokenCollection('red green blue');
237
+ // => [Token('red'), Token(' '), Token('green'), Token(' '), Token('blue')]
238
+
239
+ // Comma-separated
240
+ const tokens = new TokenCollection('red, green, blue', { separator: 'comma' });
241
+ // => [Token('red'), Token(','), Token(' '), Token('green'), Token(','), Token(' '), Token('blue')]
242
+ ```
243
+
244
+ **Options** (`TokenCollectionOptions`):
245
+
246
+ | Option | Default | Description |
247
+ | ---------------------------- | --------- | ------------------------------------------ |
248
+ | `separator` | `'space'` | Separator mode: `'space'` or `'comma'` |
249
+ | `allowEmpty` | `true` | Whether an empty value is valid |
250
+ | `unique` | `false` | Whether duplicate tokens are disallowed |
251
+ | `ordered` | `false` | Whether token order matters |
252
+ | `caseInsensitive` | `true` | Whether comparisons ignore case |
253
+ | `disallowToSurroundBySpaces` | `false` | Whether spaces around values are forbidden |
254
+ | `specificSeparator` | -- | Additional custom separator character(s) |
255
+
256
+ #### Static: `TokenCollection.fromPatterns(value, patterns, options?)`
257
+
258
+ Creates a `TokenCollection` by matching a string against a sequence of regular expression patterns. Each pattern consumes part of the input in order, and remaining unmatched portions become extra tokens. This is the primary method for parsing structured formats like datetime strings.
259
+
260
+ ```ts
261
+ // Parse a timezone offset like "+09:30" or "+0930"
262
+ const patterns = [/\+|-/, /\d{2}/, /:?/, /\d{2}/];
263
+
264
+ TokenCollection.fromPatterns('+09:30', patterns).map(t => t.value);
265
+ // => ['+', '09', ':', '30']
266
+
267
+ TokenCollection.fromPatterns('+0930', patterns).map(t => t.value);
268
+ // => ['+', '09', '', '30']
269
+ ```
270
+
271
+ Each resulting token carries the correct cumulative offset from the original string, so position tracking is preserved through the pattern matching process.
272
+
273
+ ### Core Methods
274
+
275
+ #### `check(options?)`
276
+
277
+ Validates the **structural** integrity of the token collection based on its configuration. This checks for:
278
+
279
+ - Unexpected spaces (when `disallowToSurroundBySpaces` is set with non-space separator)
280
+ - Consecutive commas in comma-separated lists
281
+ - Leading/trailing commas
282
+ - Empty values (when `allowEmpty` is `false`)
283
+ - Duplicate values (when `unique` is `true`)
284
+
285
+ ```ts
286
+ const tokens = new TokenCollection('a,, b', { separator: 'comma' });
287
+ const result = tokens.check();
288
+ // result.matched === false
289
+ // result.reason === 'unexpected-comma'
290
+ // result.offset === 2 (points to the second comma)
291
+ ```
292
+
293
+ #### `eachCheck(...callbacks)`
294
+
295
+ The main sequential validation method. Applies a series of `TokenEachCheck` callback functions to consecutive tokens. See the detailed explanation below in [The eachCheck Pattern](#the-eachcheck-pattern).
296
+
297
+ #### `divide(position)`
298
+
299
+ Splits the collection into two `TokenCollection` instances at the given index.
300
+
301
+ ```ts
302
+ const tokens = new TokenCollection('a b c');
303
+ const [before, after] = tokens.divide(2);
304
+ // before: [Token('a'), Token(' ')]
305
+ // after: [Token('b'), Token(' '), Token('c')]
306
+ ```
307
+
308
+ #### `chunk(split)`
309
+
310
+ Splits the collection into groups of `split` tokens each.
311
+
312
+ ```ts
313
+ const tokens = new TokenCollection('a b c d');
314
+ const chunks = tokens.chunk(2);
315
+ // chunks[0]: [Token('a'), Token(' ')]
316
+ // chunks[1]: [Token('b'), Token(' ')]
317
+ // chunks[2]: [Token('c'), Token(' ')]
318
+ // chunks[3]: [Token('d')]
319
+ ```
320
+
321
+ #### `search(value)`
322
+
323
+ Returns the first token whose value contains (via `Token.includes`) the given value, or `null`.
324
+
325
+ ```ts
326
+ tokens.search('red'); // find token containing "red"
327
+ tokens.search(Token.Comma); // find the first comma token
328
+ tokens.search(/^\d+$/); // find the first all-digit token
329
+ ```
330
+
331
+ #### `headAndTail()`
332
+
333
+ Splits the collection into the first token (`head`) and the remaining tokens (`tail`). Returns `{ head: Token | null, tail: TokenCollection }`. This is the mechanism that powers `eachCheck` iteration.
334
+
335
+ #### `getIdentTokens()`
336
+
337
+ Returns a new `TokenCollection` containing only tokens with type `Ident` (type `1`), filtering out whitespace and separators.
338
+
339
+ #### `has(value)`
340
+
341
+ Returns `true` if any token in the collection matches the given value (via `Token.matches`).
342
+
343
+ #### `filter(callback)`
344
+
345
+ Overrides `Array.filter` to return a `TokenCollection` (not a plain array), preserving all collection options.
346
+
347
+ #### `getConsecutiveToken(tokenType)`
348
+
349
+ Finds the first occurrence of two consecutive tokens of the same type. Returns the second token of the pair, or `null`.
350
+
351
+ #### `getDuplicated()`
352
+
353
+ Finds the first duplicated value in the collection, respecting the `caseInsensitive` setting. Returns the duplicate token, or `null`.
354
+
355
+ #### `takeTurns(tokenNumbers, lastTokenNumber?)`
356
+
357
+ Verifies that tokens follow a repeating type pattern. For example, in a comma-separated list, the pattern `[Ident, Comma]` should repeat, ending with `Ident`. Returns an error object if the pattern is violated, or `null` if valid.
358
+
359
+ ### The eachCheck Pattern
360
+
361
+ `eachCheck` is the core validation pattern for structured token sequences. It consumes tokens one by one, passing each to a corresponding callback function. This is how markuplint validates formats where each position has specific requirements (e.g., a datetime string where position 0 is a year, position 1 is a hyphen, position 2 is a month, etc.).
362
+
363
+ **Signature:**
364
+
365
+ ```ts
366
+ eachCheck(...callbacks: readonly TokenEachCheck[]): Result
367
+ ```
368
+
369
+ **The `TokenEachCheck` callback type:**
370
+
371
+ ```ts
372
+ type TokenEachCheck = (head: Readonly<Token> | null, tail: TokenCollection) => Result | void;
373
+ ```
374
+
375
+ Each callback receives:
376
+
377
+ - `head` -- the current token to validate (or `null` if tokens are exhausted)
378
+ - `tail` -- the remaining tokens after `head`
379
+
380
+ The callback returns:
381
+
382
+ - `void` -- the token passed validation; continue to the next callback
383
+ - A `Result` with `matched: true` -- stop iteration, report success
384
+ - A `Result` with `matched: false` -- record the error and continue checking remaining callbacks (the first error is kept as the primary result)
385
+
386
+ **How it works internally:**
387
+
388
+ 1. The collection is split into `head` and `tail` via `headAndTail()`.
389
+ 2. The first callback receives the head token and tail collection.
390
+ 3. After the callback returns, the tail is split again for the next callback.
391
+ 4. A `passCount` score accumulates, used for ranking competing parse results.
392
+ 5. If any callback returns an unmatched result, it is stored. The first unmatched result becomes the final error.
393
+ 6. If all callbacks return `void`, the overall result is `matched`.
394
+
395
+ **Example -- validating a date string `"2024-03-15"`:**
396
+
397
+ ```ts
398
+ // From: src/whatwg/check-datetime/date-string.ts
399
+ const tokens = TokenCollection.fromPatterns(value, [
400
+ /[^-]*/, // YYYY
401
+ /\D?/, // -
402
+ /[^-]*/, // MM
403
+ /\D/, // -
404
+ /.\d*/, // DD
405
+ ]);
406
+
407
+ const res = tokens.eachCheck(
408
+ datetimeTokenCheck.year, // validates "2024"
409
+ datetimeTokenCheck.hyphen, // validates "-"
410
+ datetimeTokenCheck.month, // validates "03"
411
+ datetimeTokenCheck.hyphen, // validates "-"
412
+ datetimeTokenCheck.date, // validates "15"
413
+ datetimeTokenCheck.extra, // ensures no trailing content
414
+ );
415
+ ```
416
+
417
+ Each `datetimeTokenCheck` function validates its token and returns `void` on success or an `UnmatchedResult` (via `token.unmatched(...)`) on failure. Because each token carries its offset, the resulting error automatically points to the exact position in the original string.
418
+
419
+ ## Usage Examples
420
+
421
+ ### DateTime Validator
422
+
423
+ **Source files:**
424
+
425
+ - `src/whatwg/check-datetime/datetime-tokens.ts`
426
+ - `src/whatwg/check-datetime/date-string.ts`
427
+
428
+ The datetime validator demonstrates the full token pipeline for validating structured formats.
429
+
430
+ **Step 1: Tokenize with patterns**
431
+
432
+ The input string is split into tokens using `TokenCollection.fromPatterns`. Each regex captures one component of the datetime format:
433
+
434
+ ```ts
435
+ // src/whatwg/check-datetime/date-string.ts
436
+ const tokens = TokenCollection.fromPatterns(value, [
437
+ /[^-]*/, // Captures everything before the first hyphen (year)
438
+ /\D?/, // Captures the optional non-digit (hyphen separator)
439
+ /[^-]*/, // Captures everything before the second hyphen (month)
440
+ /\D/, // Captures the non-digit (hyphen separator)
441
+ /.\d*/, // Captures the remaining digits (day)
442
+ ]);
443
+ ```
444
+
445
+ For the input `"2024-03-15"`, this produces:
446
+
447
+ | Index | Value | Offset | Type |
448
+ | ----- | -------- | ------ | ----- |
449
+ | 0 | `"2024"` | 0 | Ident |
450
+ | 1 | `"-"` | 4 | Ident |
451
+ | 2 | `"03"` | 5 | Ident |
452
+ | 3 | `"-"` | 7 | Ident |
453
+ | 4 | `"15"` | 8 | Ident |
454
+
455
+ **Step 2: Validate each token with eachCheck**
456
+
457
+ Each callback checks its token against the WHATWG specification:
458
+
459
+ ```ts
460
+ const res = tokens.eachCheck(
461
+ datetimeTokenCheck.year, // Must be 4+ digits, value > 0
462
+ datetimeTokenCheck.hyphen, // Must be exactly "-"
463
+ datetimeTokenCheck.month, // Must be 2 digits, 1-12
464
+ datetimeTokenCheck.hyphen, // Must be exactly "-"
465
+ datetimeTokenCheck.date, // Must be 2 digits, 1-maxday
466
+ datetimeTokenCheck.extra, // Must be empty (no trailing content)
467
+ );
468
+ ```
469
+
470
+ **Step 3: Error position flows through**
471
+
472
+ If the input is `"2024-13-15"` (month `13` is invalid), the `month` callback calls:
473
+
474
+ ```ts
475
+ return month.unmatched({
476
+ reason: { type: 'out-of-range', gte: 1, lte: 12 },
477
+ expects: [],
478
+ partName: 'month',
479
+ });
480
+ ```
481
+
482
+ Since the month token has `offset: 5`, the resulting `UnmatchedResult` carries `offset: 5`, `line: 1`, `column: 6` -- pointing directly at `"13"` in the original string.
483
+
484
+ ### List Validation
485
+
486
+ **Source file:** `src/list.ts`
487
+
488
+ The list validator demonstrates how `TokenCollection` handles comma-separated and space-separated attribute values.
489
+
490
+ **Step 1: Create collection from the value**
491
+
492
+ ```ts
493
+ // src/list.ts
494
+ const tokens = new TokenCollection(value, type);
495
+ ```
496
+
497
+ The `type` parameter (a `List` definition) configures the separator mode, uniqueness, and other constraints. For example, validating `class="btn btn-primary btn"` with `{ separator: 'space', unique: true }`.
498
+
499
+ **Step 2: Structural check**
500
+
501
+ ```ts
502
+ const matches = tokens.check({ ref });
503
+ ```
504
+
505
+ This validates the list structure -- no consecutive commas, no empty items (if disallowed), no duplicates (if `unique` is set). If the structure is invalid, an error with the exact position is returned immediately.
506
+
507
+ **Step 3: Validate individual items**
508
+
509
+ ```ts
510
+ const identTokens = tokens.getIdentTokens();
511
+
512
+ for (const token of identTokens) {
513
+ const res = checkBase(token.value, type.token, defs, ref, cache);
514
+ if (!res.matched) {
515
+ const { offset, line, column } = Token.shiftLocation(token, res.offset);
516
+ return {
517
+ ...res,
518
+ partName: res.partName ?? 'the content of the list',
519
+ offset,
520
+ line,
521
+ column,
522
+ };
523
+ }
524
+ }
525
+ ```
526
+
527
+ Key points:
528
+
529
+ - `getIdentTokens()` strips out whitespace and separator tokens, leaving only the actual values.
530
+ - Each identifier token is validated against the list's `token` type definition using `checkBase`.
531
+ - If a token fails validation, `Token.shiftLocation` adjusts the error offset to be relative to the **original string**, not the individual token. This ensures the reported position is accurate within the full attribute value.
532
+
533
+ ## Diagrams
534
+
535
+ ### Tokenization Flow
536
+
537
+ ```mermaid
538
+ sequenceDiagram
539
+ participant V as "red, , blue"
540
+ participant TC as TokenCollection
541
+ participant T1 as Token("red", 0)
542
+ participant T2 as Token(",", 3)
543
+ participant T3 as Token(" ", 4)
544
+ participant T4 as Token(",", 5)
545
+ participant T5 as Token(" ", 6)
546
+ participant T6 as Token("blue", 7)
547
+
548
+ V->>TC: new TokenCollection(value, {separator: 'comma'})
549
+ TC->>T1: Create Token("red", offset=0, type=Ident)
550
+ TC->>T2: Create Token(",", offset=3, type=Comma)
551
+ TC->>T3: Create Token(" ", offset=4, type=WhiteSpace)
552
+ TC->>T4: Create Token(",", offset=5, type=Comma)
553
+ TC->>T5: Create Token(" ", offset=6, type=WhiteSpace)
554
+ TC->>T6: Create Token("blue", offset=7, type=Ident)
555
+
556
+ Note over TC: check() detects consecutive commas
557
+ TC-->>V: UnmatchedResult { offset: 5, reason: 'unexpected-comma' }
558
+ ```
559
+
560
+ ### eachCheck Validation Flow
561
+
562
+ ```mermaid
563
+ sequenceDiagram
564
+ participant TC as TokenCollection
565
+ participant CB1 as Callback 1 (year)
566
+ participant CB2 as Callback 2 (hyphen)
567
+ participant CB3 as Callback 3 (month)
568
+ participant R as Result
569
+
570
+ TC->>TC: headAndTail() => head="2024", tail=["-","03","-","15"]
571
+ TC->>CB1: year("2024", tail)
572
+ CB1-->>TC: void (pass)
573
+
574
+ TC->>TC: headAndTail() => head="-", tail=["03","-","15"]
575
+ TC->>CB2: hyphen("-", tail)
576
+ CB2-->>TC: void (pass)
577
+
578
+ TC->>TC: headAndTail() => head="03", tail=["-","15"]
579
+ TC->>CB3: month("03", tail)
580
+ CB3-->>TC: void (pass)
581
+
582
+ Note over TC: ... continues for remaining callbacks ...
583
+ TC-->>R: MatchedResult { matched: true }
584
+ ```