@tenphi/eslint-plugin-tasty 0.3.1 → 0.4.0

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 (72) hide show
  1. package/dist/config.js +100 -20
  2. package/dist/config.js.map +1 -1
  3. package/dist/configs.js +5 -4
  4. package/dist/configs.js.map +1 -1
  5. package/dist/constants.js +55 -1
  6. package/dist/constants.js.map +1 -1
  7. package/dist/context.js +37 -6
  8. package/dist/context.js.map +1 -1
  9. package/dist/index.js +3 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/parsers/state-key-parser.js +486 -0
  12. package/dist/parsers/state-key-parser.js.map +1 -0
  13. package/dist/parsers/utils.js +128 -0
  14. package/dist/parsers/utils.js.map +1 -0
  15. package/dist/parsers/value-parser.js +613 -0
  16. package/dist/parsers/value-parser.js.map +1 -0
  17. package/dist/rules/consistent-token-usage.js +20 -19
  18. package/dist/rules/consistent-token-usage.js.map +1 -1
  19. package/dist/rules/known-property.js +31 -30
  20. package/dist/rules/known-property.js.map +1 -1
  21. package/dist/rules/no-duplicate-state.js +12 -11
  22. package/dist/rules/no-duplicate-state.js.map +1 -1
  23. package/dist/rules/no-important.js +12 -11
  24. package/dist/rules/no-important.js.map +1 -1
  25. package/dist/rules/no-nested-selector.js +15 -14
  26. package/dist/rules/no-nested-selector.js.map +1 -1
  27. package/dist/rules/no-nested-state-map.js +19 -18
  28. package/dist/rules/no-nested-state-map.js.map +1 -1
  29. package/dist/rules/no-raw-color-values.js +15 -14
  30. package/dist/rules/no-raw-color-values.js.map +1 -1
  31. package/dist/rules/no-runtime-styles-mutation.js +6 -5
  32. package/dist/rules/no-runtime-styles-mutation.js.map +1 -1
  33. package/dist/rules/no-unknown-state-alias.js +12 -11
  34. package/dist/rules/no-unknown-state-alias.js.map +1 -1
  35. package/dist/rules/prefer-shorthand-property.js +19 -18
  36. package/dist/rules/prefer-shorthand-property.js.map +1 -1
  37. package/dist/rules/require-default-state.js +22 -21
  38. package/dist/rules/require-default-state.js.map +1 -1
  39. package/dist/rules/static-no-dynamic-values.js +7 -6
  40. package/dist/rules/static-no-dynamic-values.js.map +1 -1
  41. package/dist/rules/static-valid-selector.js +1 -1
  42. package/dist/rules/valid-boolean-property.js +19 -18
  43. package/dist/rules/valid-boolean-property.js.map +1 -1
  44. package/dist/rules/valid-color-token.js +33 -21
  45. package/dist/rules/valid-color-token.js.map +1 -1
  46. package/dist/rules/valid-custom-property.js +31 -19
  47. package/dist/rules/valid-custom-property.js.map +1 -1
  48. package/dist/rules/valid-custom-unit.js +12 -16
  49. package/dist/rules/valid-custom-unit.js.map +1 -1
  50. package/dist/rules/valid-directional-modifier.js +21 -20
  51. package/dist/rules/valid-directional-modifier.js.map +1 -1
  52. package/dist/rules/valid-preset.js +5 -2
  53. package/dist/rules/valid-preset.js.map +1 -1
  54. package/dist/rules/valid-radius-shape.js +19 -18
  55. package/dist/rules/valid-radius-shape.js.map +1 -1
  56. package/dist/rules/valid-recipe.js +5 -2
  57. package/dist/rules/valid-recipe.js.map +1 -1
  58. package/dist/rules/valid-state-definition.js +70 -0
  59. package/dist/rules/valid-state-definition.js.map +1 -0
  60. package/dist/rules/valid-state-key.js +39 -102
  61. package/dist/rules/valid-state-key.js.map +1 -1
  62. package/dist/rules/valid-styles-structure.js +39 -38
  63. package/dist/rules/valid-styles-structure.js.map +1 -1
  64. package/dist/rules/valid-sub-element.js +21 -20
  65. package/dist/rules/valid-sub-element.js.map +1 -1
  66. package/dist/rules/valid-transition.js +19 -18
  67. package/dist/rules/valid-transition.js.map +1 -1
  68. package/dist/rules/valid-value.js +115 -64
  69. package/dist/rules/valid-value.js.map +1 -1
  70. package/package.json +1 -8
  71. package/dist/parser.js +0 -46
  72. package/dist/parser.js.map +0 -1
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["rules.knownProperty","rules.validValue","rules.validColorToken","rules.validCustomUnit","rules.validStateKey","rules.validStylesStructure","rules.noNestedSelector","rules.validCustomProperty","rules.validPreset","rules.validRecipe","rules.validBooleanProperty","rules.validDirectionalModifier","rules.validRadiusShape","rules.noImportant","rules.validSubElement","rules.noNestedStateMap","rules.staticNoDynamicValues","rules.staticValidSelector","rules.preferShorthandProperty","rules.validTransition","rules.requireDefaultState","rules.noDuplicateState","rules.noUnknownStateAlias","rules.noRawColorValues","rules.noStylesProp","rules.consistentTokenUsage","rules.noRuntimeStylesMutation"],"sources":["../src/index.ts"],"sourcesContent":["import type { TSESLint } from '@typescript-eslint/utils';\nimport * as rules from './rules/index.js';\nimport { recommended, strict } from './configs.js';\n\nconst ruleMap: Record<string, TSESLint.RuleModule<string, unknown[]>> = {\n 'known-property': rules.knownProperty,\n 'valid-value': rules.validValue,\n 'valid-color-token': rules.validColorToken,\n 'valid-custom-unit': rules.validCustomUnit,\n 'valid-state-key': rules.validStateKey,\n 'valid-styles-structure': rules.validStylesStructure,\n 'no-nested-selector': rules.noNestedSelector,\n 'valid-custom-property': rules.validCustomProperty,\n 'valid-preset': rules.validPreset,\n 'valid-recipe': rules.validRecipe,\n 'valid-boolean-property': rules.validBooleanProperty,\n 'valid-directional-modifier': rules.validDirectionalModifier,\n 'valid-radius-shape': rules.validRadiusShape,\n 'no-important': rules.noImportant,\n 'valid-sub-element': rules.validSubElement,\n 'no-nested-state-map': rules.noNestedStateMap,\n 'static-no-dynamic-values': rules.staticNoDynamicValues,\n 'static-valid-selector': rules.staticValidSelector,\n 'prefer-shorthand-property': rules.preferShorthandProperty,\n 'valid-transition': rules.validTransition,\n 'require-default-state': rules.requireDefaultState,\n 'no-duplicate-state': rules.noDuplicateState,\n 'no-unknown-state-alias': rules.noUnknownStateAlias,\n 'no-raw-color-values': rules.noRawColorValues,\n 'no-styles-prop': rules.noStylesProp,\n 'consistent-token-usage': rules.consistentTokenUsage,\n 'no-runtime-styles-mutation': rules.noRuntimeStylesMutation,\n};\n\nconst plugin = {\n meta: {\n name: '@tenphi/eslint-plugin-tasty',\n version: '0.1.0',\n },\n rules: ruleMap,\n configs: {\n recommended: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: recommended,\n },\n strict: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: strict,\n },\n },\n} satisfies TSESLint.FlatConfig.Plugin & {\n configs: Record<string, TSESLint.FlatConfig.Config>;\n};\n\nexport default plugin;\n\nexport { recommended, strict } from './configs.js';\nexport type { TastyValidationConfig, ResolvedConfig } from './types.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,MAAM,SAAS;CACb,MAAM;EACJ,MAAM;EACN,SAAS;EACV;CACD,OAnCsE;EACtE,kBAAkBA;EAClB,eAAeC;EACf,qBAAqBC;EACrB,qBAAqBC;EACrB,mBAAmBC;EACnB,0BAA0BC;EAC1B,sBAAsBC;EACtB,yBAAyBC;EACzB,gBAAgBC;EAChB,gBAAgBC;EAChB,0BAA0BC;EAC1B,8BAA8BC;EAC9B,sBAAsBC;EACtB,gBAAgBC;EAChB,qBAAqBC;EACrB,uBAAuBC;EACvB,4BAA4BC;EAC5B,yBAAyBC;EACzB,6BAA6BC;EAC7B,oBAAoBC;EACpB,yBAAyBC;EACzB,sBAAsBC;EACtB,0BAA0BC;EAC1B,uBAAuBC;EACvB,kBAAkBC;EAClB,0BAA0BC;EAC1B,8BAA8BC;EAC/B;CAQC,SAAS;EACP,aAAa;GACX,SAAS,EACP,IAAI,QAAQ;AACV,WAAO;MAEV;GACD,OAAO;GACR;EACD,QAAQ;GACN,SAAS,EACP,IAAI,QAAQ;AACV,WAAO;MAEV;GACD,OAAO;GACR;EACF;CACF"}
1
+ {"version":3,"file":"index.js","names":["rules.knownProperty","rules.validValue","rules.validColorToken","rules.validCustomUnit","rules.validStateKey","rules.validStylesStructure","rules.noNestedSelector","rules.validCustomProperty","rules.validPreset","rules.validRecipe","rules.validBooleanProperty","rules.validDirectionalModifier","rules.validRadiusShape","rules.noImportant","rules.validSubElement","rules.noNestedStateMap","rules.staticNoDynamicValues","rules.staticValidSelector","rules.preferShorthandProperty","rules.validTransition","rules.requireDefaultState","rules.noDuplicateState","rules.noUnknownStateAlias","rules.noRawColorValues","rules.noStylesProp","rules.consistentTokenUsage","rules.noRuntimeStylesMutation","rules.validStateDefinition"],"sources":["../src/index.ts"],"sourcesContent":["import type { TSESLint } from '@typescript-eslint/utils';\nimport * as rules from './rules/index.js';\nimport { recommended, strict } from './configs.js';\n\nconst ruleMap: Record<string, TSESLint.RuleModule<string, unknown[]>> = {\n 'known-property': rules.knownProperty,\n 'valid-value': rules.validValue,\n 'valid-color-token': rules.validColorToken,\n 'valid-custom-unit': rules.validCustomUnit,\n 'valid-state-key': rules.validStateKey,\n 'valid-styles-structure': rules.validStylesStructure,\n 'no-nested-selector': rules.noNestedSelector,\n 'valid-custom-property': rules.validCustomProperty,\n 'valid-preset': rules.validPreset,\n 'valid-recipe': rules.validRecipe,\n 'valid-boolean-property': rules.validBooleanProperty,\n 'valid-directional-modifier': rules.validDirectionalModifier,\n 'valid-radius-shape': rules.validRadiusShape,\n 'no-important': rules.noImportant,\n 'valid-sub-element': rules.validSubElement,\n 'no-nested-state-map': rules.noNestedStateMap,\n 'static-no-dynamic-values': rules.staticNoDynamicValues,\n 'static-valid-selector': rules.staticValidSelector,\n 'prefer-shorthand-property': rules.preferShorthandProperty,\n 'valid-transition': rules.validTransition,\n 'require-default-state': rules.requireDefaultState,\n 'no-duplicate-state': rules.noDuplicateState,\n 'no-unknown-state-alias': rules.noUnknownStateAlias,\n 'no-raw-color-values': rules.noRawColorValues,\n 'no-styles-prop': rules.noStylesProp,\n 'consistent-token-usage': rules.consistentTokenUsage,\n 'no-runtime-styles-mutation': rules.noRuntimeStylesMutation,\n 'valid-state-definition': rules.validStateDefinition,\n};\n\nconst plugin = {\n meta: {\n name: '@tenphi/eslint-plugin-tasty',\n version: '0.1.0',\n },\n rules: ruleMap,\n configs: {\n recommended: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: recommended,\n },\n strict: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: strict,\n },\n },\n} satisfies TSESLint.FlatConfig.Plugin & {\n configs: Record<string, TSESLint.FlatConfig.Config>;\n};\n\nexport default plugin;\n\nexport { recommended, strict } from './configs.js';\nexport type { TastyValidationConfig, ResolvedConfig } from './types.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,MAAM,SAAS;CACb,MAAM;EACJ,MAAM;EACN,SAAS;EACV;CACD,OApCsE;EACtE,kBAAkBA;EAClB,eAAeC;EACf,qBAAqBC;EACrB,qBAAqBC;EACrB,mBAAmBC;EACnB,0BAA0BC;EAC1B,sBAAsBC;EACtB,yBAAyBC;EACzB,gBAAgBC;EAChB,gBAAgBC;EAChB,0BAA0BC;EAC1B,8BAA8BC;EAC9B,sBAAsBC;EACtB,gBAAgBC;EAChB,qBAAqBC;EACrB,uBAAuBC;EACvB,4BAA4BC;EAC5B,yBAAyBC;EACzB,6BAA6BC;EAC7B,oBAAoBC;EACpB,yBAAyBC;EACzB,sBAAsBC;EACtB,0BAA0BC;EAC1B,uBAAuBC;EACvB,kBAAkBC;EAClB,0BAA0BC;EAC1B,8BAA8BC;EAC9B,0BAA0BC;EAC3B;CAQC,SAAS;EACP,aAAa;GACX,SAAS,EACP,IAAI,QAAQ;AACV,WAAO;MAEV;GACD,OAAO;GACR;EACD,QAAQ;GACN,SAAS,EACP,IAAI,QAAQ;AACV,WAAO;MAEV;GACD,OAAO;GACR;EACF;CACF"}
@@ -0,0 +1,486 @@
1
+ import { KNOWN_PSEUDO_CLASSES } from "../constants.js";
2
+
3
+ //#region src/parsers/state-key-parser.ts
4
+ /**
5
+ * Pattern for tokenizing state notation.
6
+ * Matches operators, parentheses, @-prefixed states, value mods, boolean mods,
7
+ * pseudo-classes with functions (including :is/:has/:not/:where with nesting),
8
+ * class selectors, and attribute selectors.
9
+ */
10
+ const STATE_TOKEN_PATTERN = /([&|!^])|([()])|(@media:[a-z]+)|(@media\([^)]*\))|(@supports\([^()]*(?:\([^)]*\))?[^)]*\))|(@root\([^)]*\))|(@parent\([^)]*\))|(@own\([^)]*\))|(@\([^()]*(?:\([^)]*\))?[^)]*\))|(@starting)|(@[A-Za-z][A-Za-z0-9-]*)|([a-z][a-z0-9-]*(?:\^=|\$=|\*=|=)(?:"[^"]*"|'[^']*'|[^\s&|!^()]+))|([a-z][a-z0-9-]+)|(:(?:is|has|not|where)\([^()]*(?:\([^()]*(?:\([^)]*\))?[^)]*\))*[^)]*\))|(:[-a-z][a-z0-9-]*(?:\([^)]+\))?)|(\.[a-z][a-z0-9-]+)|(\[[^\]]+\])/gi;
11
+ function tokenize(stateKey) {
12
+ const tokens = [];
13
+ const errors = [];
14
+ const normalized = replaceCommasOutsideParens(stateKey);
15
+ const covered = /* @__PURE__ */ new Set();
16
+ STATE_TOKEN_PATTERN.lastIndex = 0;
17
+ let match;
18
+ while ((match = STATE_TOKEN_PATTERN.exec(normalized)) !== null) {
19
+ const fullMatch = match[0];
20
+ const offset = match.index;
21
+ for (let i = offset; i < offset + fullMatch.length; i++) covered.add(i);
22
+ if (match[1]) switch (fullMatch) {
23
+ case "&":
24
+ tokens.push({
25
+ type: "AND",
26
+ value: "&",
27
+ offset,
28
+ length: 1
29
+ });
30
+ break;
31
+ case "|":
32
+ tokens.push({
33
+ type: "OR",
34
+ value: "|",
35
+ offset,
36
+ length: 1
37
+ });
38
+ break;
39
+ case "!":
40
+ tokens.push({
41
+ type: "NOT",
42
+ value: "!",
43
+ offset,
44
+ length: 1
45
+ });
46
+ break;
47
+ case "^":
48
+ tokens.push({
49
+ type: "XOR",
50
+ value: "^",
51
+ offset,
52
+ length: 1
53
+ });
54
+ break;
55
+ }
56
+ else if (match[2]) if (fullMatch === "(") tokens.push({
57
+ type: "LPAREN",
58
+ value: "(",
59
+ offset,
60
+ length: 1
61
+ });
62
+ else tokens.push({
63
+ type: "RPAREN",
64
+ value: ")",
65
+ offset,
66
+ length: 1
67
+ });
68
+ else tokens.push({
69
+ type: "STATE",
70
+ value: fullMatch,
71
+ offset,
72
+ length: fullMatch.length
73
+ });
74
+ }
75
+ const uncovered = [];
76
+ for (let i = 0; i < stateKey.length; i++) {
77
+ const ch = stateKey[i];
78
+ if (ch === " " || ch === " " || ch === ",") continue;
79
+ if (!covered.has(i)) uncovered.push({
80
+ ch,
81
+ pos: i
82
+ });
83
+ }
84
+ if (uncovered.length > 0) {
85
+ const chars = [...new Set(uncovered.map((u) => u.ch))].join("");
86
+ errors.push({
87
+ message: `Unrecognized characters '${chars}' in state key '${stateKey}'.`,
88
+ offset: uncovered[0].pos,
89
+ length: 1
90
+ });
91
+ }
92
+ return {
93
+ tokens,
94
+ errors
95
+ };
96
+ }
97
+ function replaceCommasOutsideParens(str) {
98
+ let result = "";
99
+ let depth = 0;
100
+ for (const char of str) if (char === "(") {
101
+ depth++;
102
+ result += char;
103
+ } else if (char === ")") {
104
+ depth--;
105
+ result += char;
106
+ } else if (char === "," && depth === 0) result += "|";
107
+ else result += char;
108
+ return result;
109
+ }
110
+ const MAX_XOR_CHAIN_LENGTH = 4;
111
+ const DIMENSION_SHORTHANDS = new Set([
112
+ "w",
113
+ "h",
114
+ "is",
115
+ "bs"
116
+ ]);
117
+ const DIMENSION_FULL = new Set([
118
+ "width",
119
+ "height",
120
+ "inline-size",
121
+ "block-size"
122
+ ]);
123
+ var StateKeyValidator = class {
124
+ errors = [];
125
+ hasOwn = false;
126
+ referencedAliases = [];
127
+ tokens;
128
+ pos = 0;
129
+ opts;
130
+ insideOwn = false;
131
+ constructor(tokens, tokenErrors, opts) {
132
+ this.tokens = tokens;
133
+ this.errors = [...tokenErrors];
134
+ this.opts = opts;
135
+ }
136
+ validate() {
137
+ if (this.tokens.length > 0) this.parseExpression();
138
+ return {
139
+ errors: this.errors,
140
+ hasOwn: this.hasOwn,
141
+ referencedAliases: this.referencedAliases
142
+ };
143
+ }
144
+ current() {
145
+ return this.tokens[this.pos];
146
+ }
147
+ advance() {
148
+ return this.tokens[this.pos++];
149
+ }
150
+ match(type) {
151
+ if (this.current()?.type === type) {
152
+ this.advance();
153
+ return true;
154
+ }
155
+ return false;
156
+ }
157
+ parseExpression() {
158
+ this.parseAnd();
159
+ }
160
+ parseAnd() {
161
+ this.parseOr();
162
+ while (this.current()?.type === "AND") {
163
+ this.advance();
164
+ if (!this.current() || this.current()?.type === "AND") {
165
+ const prev = this.tokens[this.pos - 1];
166
+ this.errors.push({
167
+ message: "Expected expression after '&' operator.",
168
+ offset: prev.offset,
169
+ length: prev.length
170
+ });
171
+ return;
172
+ }
173
+ this.parseOr();
174
+ }
175
+ }
176
+ parseOr() {
177
+ this.parseXor();
178
+ while (this.current()?.type === "OR") {
179
+ this.advance();
180
+ if (!this.current() || this.current()?.type === "OR") {
181
+ const prev = this.tokens[this.pos - 1];
182
+ this.errors.push({
183
+ message: "Expected expression after '|' operator.",
184
+ offset: prev.offset,
185
+ length: prev.length
186
+ });
187
+ return;
188
+ }
189
+ this.parseXor();
190
+ }
191
+ }
192
+ parseXor() {
193
+ this.parseUnary();
194
+ let operandCount = 1;
195
+ while (this.current()?.type === "XOR") {
196
+ this.advance();
197
+ operandCount++;
198
+ if (operandCount > MAX_XOR_CHAIN_LENGTH) {
199
+ const prev = this.tokens[this.pos - 1];
200
+ this.errors.push({
201
+ message: `XOR chain with ${operandCount} operands produces ${Math.pow(2, operandCount - 1)} OR branches. Consider breaking into smaller expressions.`,
202
+ offset: prev.offset,
203
+ length: prev.length
204
+ });
205
+ }
206
+ this.parseUnary();
207
+ }
208
+ }
209
+ parseUnary() {
210
+ if (this.match("NOT")) {
211
+ if (!this.current() || this.current()?.type === "AND" || this.current()?.type === "OR" || this.current()?.type === "XOR") {
212
+ const prev = this.tokens[this.pos - 1];
213
+ this.errors.push({
214
+ message: "Expected expression after '!' operator.",
215
+ offset: prev.offset,
216
+ length: prev.length
217
+ });
218
+ return;
219
+ }
220
+ this.parseUnary();
221
+ return;
222
+ }
223
+ this.parsePrimary();
224
+ }
225
+ parsePrimary() {
226
+ if (this.match("LPAREN")) {
227
+ this.parseExpression();
228
+ if (!this.match("RPAREN")) this.errors.push({
229
+ message: "Missing closing ')' in state expression.",
230
+ offset: this.tokens[this.pos - 1]?.offset ?? 0,
231
+ length: 1
232
+ });
233
+ return;
234
+ }
235
+ const token = this.current();
236
+ if (token?.type === "STATE") {
237
+ this.advance();
238
+ this.validateStateToken(token);
239
+ return;
240
+ }
241
+ if (token) {
242
+ this.errors.push({
243
+ message: `Unexpected token '${token.value}'.`,
244
+ offset: token.offset,
245
+ length: token.length
246
+ });
247
+ this.advance();
248
+ }
249
+ }
250
+ validateStateToken(token) {
251
+ const value = token.value;
252
+ if (value === "@starting") return;
253
+ if (value.startsWith("@media:")) {
254
+ const mediaType = value.slice(7);
255
+ if (!new Set([
256
+ "print",
257
+ "screen",
258
+ "all",
259
+ "speech"
260
+ ]).has(mediaType)) this.errors.push({
261
+ message: `Unknown media type '${mediaType}'. Valid: print, screen, all, speech.`,
262
+ offset: token.offset,
263
+ length: token.length
264
+ });
265
+ return;
266
+ }
267
+ if (value.startsWith("@media(")) {
268
+ this.validateMediaQuery(value, token);
269
+ return;
270
+ }
271
+ if (value.startsWith("@supports(")) {
272
+ this.validateSupportsQuery(value, token);
273
+ return;
274
+ }
275
+ if (value.startsWith("@root(")) {
276
+ this.validateInnerStateExpression(value, 6, token);
277
+ return;
278
+ }
279
+ if (value.startsWith("@parent(")) {
280
+ this.validateParentState(value, token);
281
+ return;
282
+ }
283
+ if (value.startsWith("@own(")) {
284
+ this.hasOwn = true;
285
+ if (this.insideOwn) {
286
+ this.errors.push({
287
+ message: "Nested @own() is not allowed.",
288
+ offset: token.offset,
289
+ length: token.length
290
+ });
291
+ return;
292
+ }
293
+ const prevInsideOwn = this.insideOwn;
294
+ this.insideOwn = true;
295
+ this.validateInnerStateExpression(value, 5, token);
296
+ this.insideOwn = prevInsideOwn;
297
+ return;
298
+ }
299
+ if (value.startsWith("@(")) {
300
+ this.validateContainerQuery(value, token);
301
+ return;
302
+ }
303
+ if (value.startsWith("@") && /^@[A-Za-z][A-Za-z0-9-]*$/.test(value)) {
304
+ this.referencedAliases.push(value);
305
+ return;
306
+ }
307
+ if (value.startsWith(":")) {
308
+ this.validatePseudoClass(value, token);
309
+ return;
310
+ }
311
+ if (value.startsWith(".")) return;
312
+ if (value.startsWith("[")) {
313
+ this.validateAttributeSelector(value, token);
314
+ return;
315
+ }
316
+ if (value.includes("=")) {
317
+ this.validateValueModifier(value, token);
318
+ return;
319
+ }
320
+ if (/^[a-z][a-z0-9-]+$/i.test(value)) return;
321
+ this.errors.push({
322
+ message: `Unrecognized state token '${value}'.`,
323
+ offset: token.offset,
324
+ length: token.length
325
+ });
326
+ }
327
+ validateMediaQuery(raw, token) {
328
+ const content = raw.slice(7, -1);
329
+ if (!content.trim()) {
330
+ this.errors.push({
331
+ message: "Empty @media() query.",
332
+ offset: token.offset,
333
+ length: token.length
334
+ });
335
+ return;
336
+ }
337
+ const expanded = expandDimensionShorthands(content.trim());
338
+ if (expanded.includes(":") && !expanded.includes("<") && !expanded.includes(">") && !expanded.includes("=")) return;
339
+ if (!expanded.includes("<") && !expanded.includes(">") && !expanded.includes("=")) return;
340
+ this.validateDimensionCondition(expanded, token);
341
+ }
342
+ validateDimensionCondition(condition, token) {
343
+ const rangeMatch = condition.match(/^(.+?)\s*(<=|<)\s*(\S+)\s*(<=|<)\s*(.+)$/);
344
+ if (rangeMatch) {
345
+ const dim = rangeMatch[3];
346
+ if (!DIMENSION_FULL.has(dim) && !DIMENSION_SHORTHANDS.has(dim)) this.errors.push({
347
+ message: `Unknown dimension '${dim}' in media/container query. Valid: width, height, inline-size, block-size (or w, h, is, bs).`,
348
+ offset: token.offset,
349
+ length: token.length
350
+ });
351
+ return;
352
+ }
353
+ const simpleMatch = condition.match(/^(\S+)\s*(<=|>=|<|>|=)\s*(.+)$/);
354
+ if (simpleMatch) {
355
+ const dim = simpleMatch[1];
356
+ if (DIMENSION_FULL.has(dim) || DIMENSION_SHORTHANDS.has(dim)) return;
357
+ }
358
+ const reversedMatch = condition.match(/^(.+?)\s*(<=|>=|<|>|=)\s*(\S+)$/);
359
+ if (reversedMatch) {
360
+ const dim = reversedMatch[3];
361
+ if (DIMENSION_FULL.has(dim) || DIMENSION_SHORTHANDS.has(dim)) return;
362
+ }
363
+ }
364
+ validateSupportsQuery(raw, token) {
365
+ if (!raw.slice(10, -1).trim()) this.errors.push({
366
+ message: "Empty @supports() query.",
367
+ offset: token.offset,
368
+ length: token.length
369
+ });
370
+ }
371
+ validateInnerStateExpression(raw, prefixLen, _token) {
372
+ const content = raw.slice(prefixLen, -1);
373
+ if (!content.trim()) return;
374
+ const innerResult = parseStateKey(content, this.opts);
375
+ this.errors.push(...innerResult.errors);
376
+ if (innerResult.hasOwn) this.hasOwn = true;
377
+ this.referencedAliases.push(...innerResult.referencedAliases);
378
+ }
379
+ validateParentState(raw, token) {
380
+ const content = raw.slice(8, -1);
381
+ if (!content.trim()) {
382
+ this.errors.push({
383
+ message: "Empty @parent() state.",
384
+ offset: token.offset,
385
+ length: token.length
386
+ });
387
+ return;
388
+ }
389
+ let condition = content.trim();
390
+ const lastCommaIdx = condition.lastIndexOf(",");
391
+ if (lastCommaIdx !== -1) {
392
+ if (condition.slice(lastCommaIdx + 1).trim() === ">") condition = condition.slice(0, lastCommaIdx).trim();
393
+ }
394
+ const innerResult = parseStateKey(condition, this.opts);
395
+ this.errors.push(...innerResult.errors);
396
+ this.referencedAliases.push(...innerResult.referencedAliases);
397
+ }
398
+ validateContainerQuery(raw, token) {
399
+ const content = raw.slice(2, -1);
400
+ if (!content.trim()) {
401
+ this.errors.push({
402
+ message: "Empty container query.",
403
+ offset: token.offset,
404
+ length: token.length
405
+ });
406
+ return;
407
+ }
408
+ const commaIdx = findTopLevelComma(content);
409
+ let condition;
410
+ if (commaIdx !== -1) condition = content.slice(commaIdx + 1).trim();
411
+ else condition = content.trim();
412
+ if (condition.startsWith("$")) return;
413
+ if (/^[a-zA-Z][\w-]*\s*\(/.test(condition)) return;
414
+ const expanded = expandDimensionShorthands(condition);
415
+ if (expanded.includes("<") || expanded.includes(">") || expanded.includes("=")) this.validateDimensionCondition(expanded, token);
416
+ }
417
+ validatePseudoClass(value, token) {
418
+ if (/^:(is|has|not|where)\(/.exec(value)) return;
419
+ const funcMatch = /^(:[a-z-]+)\(/.exec(value);
420
+ if (funcMatch) {
421
+ const baseName = funcMatch[1];
422
+ if (!KNOWN_PSEUDO_CLASSES.has(baseName)) this.errors.push({
423
+ message: `Unknown pseudo-class '${baseName}'.`,
424
+ offset: token.offset,
425
+ length: token.length
426
+ });
427
+ return;
428
+ }
429
+ if (!KNOWN_PSEUDO_CLASSES.has(value)) this.errors.push({
430
+ message: `Unknown pseudo-class '${value}'.`,
431
+ offset: token.offset,
432
+ length: token.length
433
+ });
434
+ }
435
+ validateAttributeSelector(value, token) {
436
+ if (!value.startsWith("[") || !value.endsWith("]")) this.errors.push({
437
+ message: `Malformed attribute selector '${value}'.`,
438
+ offset: token.offset,
439
+ length: token.length
440
+ });
441
+ }
442
+ validateValueModifier(value, token) {
443
+ if (!value.match(/^([a-z][a-z0-9-]*)(\^=|\$=|\*=|=)(.+)$/i)) this.errors.push({
444
+ message: `Invalid value modifier syntax '${value}'.`,
445
+ offset: token.offset,
446
+ length: token.length
447
+ });
448
+ }
449
+ };
450
+ function expandDimensionShorthands(condition) {
451
+ return condition.replace(/\bw\b/g, "width").replace(/\bh\b/g, "height").replace(/\bis\b/g, "inline-size").replace(/\bbs\b/g, "block-size");
452
+ }
453
+ function findTopLevelComma(str) {
454
+ let depth = 0;
455
+ for (let i = 0; i < str.length; i++) {
456
+ const ch = str[i];
457
+ if (ch === "(") depth++;
458
+ else if (ch === ")") depth = Math.max(0, depth - 1);
459
+ else if (ch === "," && depth === 0) return i;
460
+ }
461
+ return -1;
462
+ }
463
+ /**
464
+ * Validate a state key string and return detailed errors.
465
+ */
466
+ function parseStateKey(stateKey, opts = {}) {
467
+ if (!stateKey || !stateKey.trim()) return {
468
+ errors: [],
469
+ hasOwn: false,
470
+ referencedAliases: []
471
+ };
472
+ const { tokens, errors: tokenErrors } = tokenize(stateKey.trim());
473
+ return new StateKeyValidator(tokens, tokenErrors, opts).validate();
474
+ }
475
+ /**
476
+ * Validate a state definition value (the RHS of a state alias).
477
+ * State definitions should be valid state expressions like
478
+ * '@media(w < 768px)', '@root(theme=dark)', etc.
479
+ */
480
+ function validateStateDefinition(value, opts = {}) {
481
+ return parseStateKey(value, opts);
482
+ }
483
+
484
+ //#endregion
485
+ export { parseStateKey, validateStateDefinition };
486
+ //# sourceMappingURL=state-key-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-key-parser.js","names":[],"sources":["../../src/parsers/state-key-parser.ts"],"sourcesContent":["import { BUILT_IN_STATE_PREFIXES, KNOWN_PSEUDO_CLASSES } from '../constants.js';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface StateKeyError {\n message: string;\n offset: number;\n length: number;\n}\n\nexport interface StateKeyResult {\n errors: StateKeyError[];\n hasOwn: boolean;\n referencedAliases: string[];\n}\n\nexport interface StateKeyParserOptions {\n knownAliases?: string[];\n}\n\n// ============================================================================\n// Tokenizer\n// ============================================================================\n\ntype TokenType = 'AND' | 'OR' | 'NOT' | 'XOR' | 'LPAREN' | 'RPAREN' | 'STATE';\n\ninterface Token {\n type: TokenType;\n value: string;\n offset: number;\n length: number;\n}\n\n/**\n * Pattern for tokenizing state notation.\n * Matches operators, parentheses, @-prefixed states, value mods, boolean mods,\n * pseudo-classes with functions (including :is/:has/:not/:where with nesting),\n * class selectors, and attribute selectors.\n */\nconst STATE_TOKEN_PATTERN =\n /([&|!^])|([()])|(@media:[a-z]+)|(@media\\([^)]*\\))|(@supports\\([^()]*(?:\\([^)]*\\))?[^)]*\\))|(@root\\([^)]*\\))|(@parent\\([^)]*\\))|(@own\\([^)]*\\))|(@\\([^()]*(?:\\([^)]*\\))?[^)]*\\))|(@starting)|(@[A-Za-z][A-Za-z0-9-]*)|([a-z][a-z0-9-]*(?:\\^=|\\$=|\\*=|=)(?:\"[^\"]*\"|'[^']*'|[^\\s&|!^()]+))|([a-z][a-z0-9-]+)|(:(?:is|has|not|where)\\([^()]*(?:\\([^()]*(?:\\([^)]*\\))?[^)]*\\))*[^)]*\\))|(:[-a-z][a-z0-9-]*(?:\\([^)]+\\))?)|(\\.[a-z][a-z0-9-]+)|(\\[[^\\]]+\\])/gi;\n\nfunction tokenize(stateKey: string): { tokens: Token[]; errors: StateKeyError[] } {\n const tokens: Token[] = [];\n const errors: StateKeyError[] = [];\n\n // Replace commas with | outside of parentheses\n const normalized = replaceCommasOutsideParens(stateKey);\n\n const covered = new Set<number>();\n\n STATE_TOKEN_PATTERN.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = STATE_TOKEN_PATTERN.exec(normalized)) !== null) {\n const fullMatch = match[0];\n const offset = match.index;\n\n for (let i = offset; i < offset + fullMatch.length; i++) {\n covered.add(i);\n }\n\n if (match[1]) {\n switch (fullMatch) {\n case '&':\n tokens.push({ type: 'AND', value: '&', offset, length: 1 });\n break;\n case '|':\n tokens.push({ type: 'OR', value: '|', offset, length: 1 });\n break;\n case '!':\n tokens.push({ type: 'NOT', value: '!', offset, length: 1 });\n break;\n case '^':\n tokens.push({ type: 'XOR', value: '^', offset, length: 1 });\n break;\n }\n } else if (match[2]) {\n if (fullMatch === '(') {\n tokens.push({ type: 'LPAREN', value: '(', offset, length: 1 });\n } else {\n tokens.push({ type: 'RPAREN', value: ')', offset, length: 1 });\n }\n } else {\n tokens.push({\n type: 'STATE',\n value: fullMatch,\n offset,\n length: fullMatch.length,\n });\n }\n }\n\n // Check for uncovered characters (unrecognized tokens)\n const uncovered: { ch: string; pos: number }[] = [];\n for (let i = 0; i < stateKey.length; i++) {\n const ch = stateKey[i];\n if (ch === ' ' || ch === '\\t' || ch === ',') continue;\n if (!covered.has(i)) {\n uncovered.push({ ch, pos: i });\n }\n }\n\n if (uncovered.length > 0) {\n const chars = [...new Set(uncovered.map((u) => u.ch))].join('');\n errors.push({\n message: `Unrecognized characters '${chars}' in state key '${stateKey}'.`,\n offset: uncovered[0].pos,\n length: 1,\n });\n }\n\n return { tokens, errors };\n}\n\nfunction replaceCommasOutsideParens(str: string): string {\n let result = '';\n let depth = 0;\n\n for (const char of str) {\n if (char === '(') {\n depth++;\n result += char;\n } else if (char === ')') {\n depth--;\n result += char;\n } else if (char === ',' && depth === 0) {\n result += '|';\n } else {\n result += char;\n }\n }\n\n return result;\n}\n\n// ============================================================================\n// Validator\n// ============================================================================\n\nconst MAX_XOR_CHAIN_LENGTH = 4;\n\nconst DIMENSION_SHORTHANDS = new Set(['w', 'h', 'is', 'bs']);\nconst DIMENSION_FULL = new Set([\n 'width',\n 'height',\n 'inline-size',\n 'block-size',\n]);\n\nclass StateKeyValidator {\n private errors: StateKeyError[] = [];\n private hasOwn = false;\n private referencedAliases: string[] = [];\n private tokens: Token[];\n private pos = 0;\n private opts: StateKeyParserOptions;\n private insideOwn = false;\n\n constructor(\n tokens: Token[],\n tokenErrors: StateKeyError[],\n opts: StateKeyParserOptions,\n ) {\n this.tokens = tokens;\n this.errors = [...tokenErrors];\n this.opts = opts;\n }\n\n validate(): StateKeyResult {\n if (this.tokens.length > 0) {\n this.parseExpression();\n }\n\n return {\n errors: this.errors,\n hasOwn: this.hasOwn,\n referencedAliases: this.referencedAliases,\n };\n }\n\n private current(): Token | undefined {\n return this.tokens[this.pos];\n }\n\n private advance(): Token | undefined {\n return this.tokens[this.pos++];\n }\n\n private match(type: TokenType): boolean {\n if (this.current()?.type === type) {\n this.advance();\n return true;\n }\n return false;\n }\n\n private parseExpression(): void {\n this.parseAnd();\n }\n\n private parseAnd(): void {\n this.parseOr();\n while (this.current()?.type === 'AND') {\n this.advance();\n if (!this.current() || this.current()?.type === 'AND') {\n const prev = this.tokens[this.pos - 1];\n this.errors.push({\n message: \"Expected expression after '&' operator.\",\n offset: prev.offset,\n length: prev.length,\n });\n return;\n }\n this.parseOr();\n }\n }\n\n private parseOr(): void {\n this.parseXor();\n while (this.current()?.type === 'OR') {\n this.advance();\n if (!this.current() || this.current()?.type === 'OR') {\n const prev = this.tokens[this.pos - 1];\n this.errors.push({\n message: \"Expected expression after '|' operator.\",\n offset: prev.offset,\n length: prev.length,\n });\n return;\n }\n this.parseXor();\n }\n }\n\n private parseXor(): void {\n this.parseUnary();\n let operandCount = 1;\n\n while (this.current()?.type === 'XOR') {\n this.advance();\n operandCount++;\n if (operandCount > MAX_XOR_CHAIN_LENGTH) {\n const prev = this.tokens[this.pos - 1];\n this.errors.push({\n message: `XOR chain with ${operandCount} operands produces ${Math.pow(2, operandCount - 1)} OR branches. Consider breaking into smaller expressions.`,\n offset: prev.offset,\n length: prev.length,\n });\n }\n this.parseUnary();\n }\n }\n\n private parseUnary(): void {\n if (this.match('NOT')) {\n if (\n !this.current() ||\n this.current()?.type === 'AND' ||\n this.current()?.type === 'OR' ||\n this.current()?.type === 'XOR'\n ) {\n const prev = this.tokens[this.pos - 1];\n this.errors.push({\n message: \"Expected expression after '!' operator.\",\n offset: prev.offset,\n length: prev.length,\n });\n return;\n }\n this.parseUnary();\n return;\n }\n this.parsePrimary();\n }\n\n private parsePrimary(): void {\n if (this.match('LPAREN')) {\n this.parseExpression();\n if (!this.match('RPAREN')) {\n this.errors.push({\n message: \"Missing closing ')' in state expression.\",\n offset: this.tokens[this.pos - 1]?.offset ?? 0,\n length: 1,\n });\n }\n return;\n }\n\n const token = this.current();\n if (token?.type === 'STATE') {\n this.advance();\n this.validateStateToken(token);\n return;\n }\n\n // Unexpected token or end\n if (token) {\n this.errors.push({\n message: `Unexpected token '${token.value}'.`,\n offset: token.offset,\n length: token.length,\n });\n this.advance();\n }\n }\n\n private validateStateToken(token: Token): void {\n const value = token.value;\n\n // @starting\n if (value === '@starting') return;\n\n // @media:type\n if (value.startsWith('@media:')) {\n const mediaType = value.slice(7);\n const validTypes = new Set(['print', 'screen', 'all', 'speech']);\n if (!validTypes.has(mediaType)) {\n this.errors.push({\n message: `Unknown media type '${mediaType}'. Valid: print, screen, all, speech.`,\n offset: token.offset,\n length: token.length,\n });\n }\n return;\n }\n\n // @media(...)\n if (value.startsWith('@media(')) {\n this.validateMediaQuery(value, token);\n return;\n }\n\n // @supports(...)\n if (value.startsWith('@supports(')) {\n this.validateSupportsQuery(value, token);\n return;\n }\n\n // @root(...)\n if (value.startsWith('@root(')) {\n this.validateInnerStateExpression(value, 6, token);\n return;\n }\n\n // @parent(...)\n if (value.startsWith('@parent(')) {\n this.validateParentState(value, token);\n return;\n }\n\n // @own(...)\n if (value.startsWith('@own(')) {\n this.hasOwn = true;\n if (this.insideOwn) {\n this.errors.push({\n message: 'Nested @own() is not allowed.',\n offset: token.offset,\n length: token.length,\n });\n return;\n }\n const prevInsideOwn = this.insideOwn;\n this.insideOwn = true;\n this.validateInnerStateExpression(value, 5, token);\n this.insideOwn = prevInsideOwn;\n return;\n }\n\n // @(...) container query\n if (value.startsWith('@(')) {\n this.validateContainerQuery(value, token);\n return;\n }\n\n // @alias predefined state\n if (value.startsWith('@') && /^@[A-Za-z][A-Za-z0-9-]*$/.test(value)) {\n this.referencedAliases.push(value);\n return;\n }\n\n // Pseudo-class/pseudo-element\n if (value.startsWith(':')) {\n this.validatePseudoClass(value, token);\n return;\n }\n\n // Class selector\n if (value.startsWith('.')) return;\n\n // Attribute selector\n if (value.startsWith('[')) {\n this.validateAttributeSelector(value, token);\n return;\n }\n\n // Value modifier (e.g., theme=danger)\n if (value.includes('=')) {\n this.validateValueModifier(value, token);\n return;\n }\n\n // Boolean modifier (e.g., hovered, disabled)\n if (/^[a-z][a-z0-9-]+$/i.test(value)) return;\n\n this.errors.push({\n message: `Unrecognized state token '${value}'.`,\n offset: token.offset,\n length: token.length,\n });\n }\n\n private validateMediaQuery(raw: string, token: Token): void {\n const content = raw.slice(7, -1);\n if (!content.trim()) {\n this.errors.push({\n message: 'Empty @media() query.',\n offset: token.offset,\n length: token.length,\n });\n return;\n }\n\n // Expand dimension shorthands for validation\n const expanded = expandDimensionShorthands(content.trim());\n\n // Feature query (contains ':' but no comparison operators)\n if (\n expanded.includes(':') &&\n !expanded.includes('<') &&\n !expanded.includes('>') &&\n !expanded.includes('=')\n ) {\n return;\n }\n\n // Boolean feature query (no comparison operators)\n if (\n !expanded.includes('<') &&\n !expanded.includes('>') &&\n !expanded.includes('=')\n ) {\n return;\n }\n\n // Dimension query — validate dimension names\n this.validateDimensionCondition(expanded, token);\n }\n\n private validateDimensionCondition(condition: string, token: Token): void {\n // Range syntax: 600px <= width < 1200px\n const rangeMatch = condition.match(\n /^(.+?)\\s*(<=|<)\\s*(\\S+)\\s*(<=|<)\\s*(.+)$/,\n );\n if (rangeMatch) {\n const dim = rangeMatch[3];\n if (!DIMENSION_FULL.has(dim) && !DIMENSION_SHORTHANDS.has(dim)) {\n this.errors.push({\n message: `Unknown dimension '${dim}' in media/container query. Valid: width, height, inline-size, block-size (or w, h, is, bs).`,\n offset: token.offset,\n length: token.length,\n });\n }\n return;\n }\n\n // Simple comparison: width < 768px\n const simpleMatch = condition.match(\n /^(\\S+)\\s*(<=|>=|<|>|=)\\s*(.+)$/,\n );\n if (simpleMatch) {\n const dim = simpleMatch[1];\n if (\n DIMENSION_FULL.has(dim) ||\n DIMENSION_SHORTHANDS.has(dim)\n ) {\n return;\n }\n }\n\n // Reversed: 768px > width\n const reversedMatch = condition.match(\n /^(.+?)\\s*(<=|>=|<|>|=)\\s*(\\S+)$/,\n );\n if (reversedMatch) {\n const dim = reversedMatch[3];\n if (\n DIMENSION_FULL.has(dim) ||\n DIMENSION_SHORTHANDS.has(dim)\n ) {\n return;\n }\n }\n }\n\n private validateSupportsQuery(raw: string, token: Token): void {\n const content = raw.slice(10, -1);\n if (!content.trim()) {\n this.errors.push({\n message: 'Empty @supports() query.',\n offset: token.offset,\n length: token.length,\n });\n }\n }\n\n private validateInnerStateExpression(\n raw: string,\n prefixLen: number,\n _token: Token,\n ): void {\n const content = raw.slice(prefixLen, -1);\n if (!content.trim()) return;\n\n const innerResult = parseStateKey(content, this.opts);\n this.errors.push(...innerResult.errors);\n if (innerResult.hasOwn) this.hasOwn = true;\n this.referencedAliases.push(...innerResult.referencedAliases);\n }\n\n private validateParentState(raw: string, token: Token): void {\n const content = raw.slice(8, -1);\n if (!content.trim()) {\n this.errors.push({\n message: 'Empty @parent() state.',\n offset: token.offset,\n length: token.length,\n });\n return;\n }\n\n let condition = content.trim();\n\n // Check for direct parent combinator: @parent(hovered, >)\n const lastCommaIdx = condition.lastIndexOf(',');\n if (lastCommaIdx !== -1) {\n const afterComma = condition.slice(lastCommaIdx + 1).trim();\n if (afterComma === '>') {\n condition = condition.slice(0, lastCommaIdx).trim();\n }\n }\n\n const innerResult = parseStateKey(condition, this.opts);\n this.errors.push(...innerResult.errors);\n this.referencedAliases.push(...innerResult.referencedAliases);\n }\n\n private validateContainerQuery(raw: string, token: Token): void {\n const content = raw.slice(2, -1);\n if (!content.trim()) {\n this.errors.push({\n message: 'Empty container query.',\n offset: token.offset,\n length: token.length,\n });\n return;\n }\n\n // Named container: @(layout, w < 600px)\n const commaIdx = findTopLevelComma(content);\n let condition: string;\n\n if (commaIdx !== -1) {\n condition = content.slice(commaIdx + 1).trim();\n } else {\n condition = content.trim();\n }\n\n // Style query: @($variant=primary) — skip\n if (condition.startsWith('$')) return;\n\n // Function-like: scroll-state(...) — skip\n if (/^[a-zA-Z][\\w-]*\\s*\\(/.test(condition)) return;\n\n // Dimension query\n const expanded = expandDimensionShorthands(condition);\n if (\n expanded.includes('<') ||\n expanded.includes('>') ||\n expanded.includes('=')\n ) {\n this.validateDimensionCondition(expanded, token);\n }\n }\n\n private validatePseudoClass(value: string, token: Token): void {\n // :is(), :has(), :not(), :where() — structural pseudo-classes\n const enhancedMatch = /^:(is|has|not|where)\\(/.exec(value);\n if (enhancedMatch) return;\n\n // Functional pseudo-classes like :nth-child(2n+1)\n const funcMatch = /^(:[a-z-]+)\\(/.exec(value);\n if (funcMatch) {\n const baseName = funcMatch[1];\n if (!KNOWN_PSEUDO_CLASSES.has(baseName)) {\n this.errors.push({\n message: `Unknown pseudo-class '${baseName}'.`,\n offset: token.offset,\n length: token.length,\n });\n }\n return;\n }\n\n // Simple pseudo-class: :hover, :focus, etc.\n if (!KNOWN_PSEUDO_CLASSES.has(value)) {\n this.errors.push({\n message: `Unknown pseudo-class '${value}'.`,\n offset: token.offset,\n length: token.length,\n });\n }\n }\n\n private validateAttributeSelector(value: string, token: Token): void {\n if (!value.startsWith('[') || !value.endsWith(']')) {\n this.errors.push({\n message: `Malformed attribute selector '${value}'.`,\n offset: token.offset,\n length: token.length,\n });\n }\n }\n\n private validateValueModifier(value: string, token: Token): void {\n const opMatch = value.match(/^([a-z][a-z0-9-]*)(\\^=|\\$=|\\*=|=)(.+)$/i);\n if (!opMatch) {\n this.errors.push({\n message: `Invalid value modifier syntax '${value}'.`,\n offset: token.offset,\n length: token.length,\n });\n }\n }\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction expandDimensionShorthands(condition: string): string {\n return condition\n .replace(/\\bw\\b/g, 'width')\n .replace(/\\bh\\b/g, 'height')\n .replace(/\\bis\\b/g, 'inline-size')\n .replace(/\\bbs\\b/g, 'block-size');\n}\n\nfunction findTopLevelComma(str: string): number {\n let depth = 0;\n for (let i = 0; i < str.length; i++) {\n const ch = str[i];\n if (ch === '(') depth++;\n else if (ch === ')') depth = Math.max(0, depth - 1);\n else if (ch === ',' && depth === 0) return i;\n }\n return -1;\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Validate a state key string and return detailed errors.\n */\nexport function parseStateKey(\n stateKey: string,\n opts: StateKeyParserOptions = {},\n): StateKeyResult {\n if (!stateKey || !stateKey.trim()) {\n return { errors: [], hasOwn: false, referencedAliases: [] };\n }\n\n const { tokens, errors: tokenErrors } = tokenize(stateKey.trim());\n const validator = new StateKeyValidator(tokens, tokenErrors, opts);\n return validator.validate();\n}\n\n/**\n * Validate a state definition value (the RHS of a state alias).\n * State definitions should be valid state expressions like\n * '@media(w < 768px)', '@root(theme=dark)', etc.\n */\nexport function validateStateDefinition(\n value: string,\n opts: StateKeyParserOptions = {},\n): StateKeyResult {\n return parseStateKey(value, opts);\n}\n"],"mappings":"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"}