@tenphi/eslint-plugin-tasty 1.2.0 → 1.3.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.
- package/README.md +30 -1
- package/dist/configs.js +3 -1
- package/dist/configs.js.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/parsers/state-key-parser.js +25 -1
- package/dist/parsers/state-key-parser.js.map +1 -1
- package/dist/rules/no-runtime-styles-mutation.js +33 -24
- package/dist/rules/no-runtime-styles-mutation.js.map +1 -1
- package/dist/rules/no-style-spread.js +50 -0
- package/dist/rules/no-style-spread.js.map +1 -0
- package/dist/rules/prefer-state-negation.js +65 -0
- package/dist/rules/prefer-state-negation.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -193,8 +193,11 @@ for typing shared signatures.
|
|
|
193
193
|
| `tasty/prefer-shorthand-property` | warn | Use Tasty shorthand instead of native CSS properties (`backgroundColor` → `fill`, `paddingBlock` → `blockPadding`, etc.). In an extension layer the rewrite is report-only and points at a token in the base component, and over a base you cannot edit it is skipped (see `ownedSources`) |
|
|
194
194
|
| `tasty/no-raw-color-values` | warn | Raw hex/rgb/`okhsl`/`okhst`/`oklch`/named colors instead of `#color` tokens |
|
|
195
195
|
| `tasty/no-raw-transition-duration` | warn | Hardcoded `transition` duration (`fill 0.2s`) in Tasty styles or a local `tasty()` component's `transition` prop. Suggests a configured duration token or the implicit per-name timing; zero and delays are left alone |
|
|
196
|
+
| `tasty/no-runtime-styles-mutation` | warn | JavaScript variables, calls, conditionals, computed keys, or interpolated templates in Tasty style values; use states and tokens instead |
|
|
197
|
+
| `tasty/no-style-spread` | warn | Object or array spreads inside runtime Tasty styles; suppress an intentional spread on its line with an ESLint comment and a reason |
|
|
196
198
|
| `tasty/consistent-token-usage` | warn | Raw px values when custom units or tokens exist |
|
|
197
199
|
| `tasty/prefer-auto-calc` | warn | `calc(...)` instead of Tasty auto-calc `(...)` |
|
|
200
|
+
| `tasty/prefer-state-negation` | warn | Top-level `:not(...)` in state keys instead of the `!` prefix (autofixable; nested CSS selectors stay intact) |
|
|
198
201
|
| `tasty/prefer-custom-property-syntax` | warn | `var(--prop)` / `$x-color` / `transparent` / `currentColor` instead of `$prop` / `#color` / `#clear` / `#current` |
|
|
199
202
|
| `tasty/prefer-hide` | warn | `display: 'none'` instead of `hide: true` |
|
|
200
203
|
| `tasty/prefer-directional-shorthand` | warn | 4-value `margin`/`padding`/`inset`/`radius`/`fade`/`border` with placeholder positions instead of directional form |
|
|
@@ -208,7 +211,33 @@ for typing shared signatures.
|
|
|
208
211
|
| `tasty/valid-state-definition` | warn | Invalid state definition values in `configure()` or `tasty.config` |
|
|
209
212
|
| `tasty/no-unknown-state-alias` | warn | Unknown `@name` state aliases |
|
|
210
213
|
| `tasty/no-styles-prop` | warn | Direct `styles` prop usage |
|
|
211
|
-
|
|
214
|
+
|
|
215
|
+
### Static values and intentional spreads
|
|
216
|
+
|
|
217
|
+
The recommended rules flag JavaScript-computed style values, including
|
|
218
|
+
identifiers, conditionals, function calls, computed keys, and template
|
|
219
|
+
interpolation. Use a Tasty state map with `mods` for discrete states, or a token
|
|
220
|
+
for a value that changes per instance. Literal CSS strings, token references,
|
|
221
|
+
and state maps are accepted.
|
|
222
|
+
|
|
223
|
+
Spreads in runtime style objects get a separate warning because they hide which
|
|
224
|
+
properties are defined and can make overrides hard to review. If a spread is
|
|
225
|
+
necessary, suppress just that line and explain why:
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
const Card = tasty({
|
|
229
|
+
styles: {
|
|
230
|
+
// eslint-disable-next-line tasty/no-style-spread -- generated defaults are shared by all cards
|
|
231
|
+
...sharedStyles,
|
|
232
|
+
fill: { '': '#surface', active: '#primary' },
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
`tastyStatic()` still rejects spreads and dynamic expressions as errors because
|
|
238
|
+
its build-time extractor cannot evaluate them. An ESLint suppression does not
|
|
239
|
+
make a spread valid there. To make warnings fail CI until addressed or explicitly
|
|
240
|
+
suppressed, run ESLint with `--max-warnings=0`.
|
|
212
241
|
|
|
213
242
|
### Optional motion duration rule
|
|
214
243
|
|
package/dist/configs.js
CHANGED
|
@@ -24,8 +24,11 @@ const recommended = {
|
|
|
24
24
|
"tasty/prefer-shorthand-property": "warn",
|
|
25
25
|
"tasty/no-raw-color-values": "warn",
|
|
26
26
|
"tasty/no-raw-transition-duration": "warn",
|
|
27
|
+
"tasty/no-runtime-styles-mutation": "warn",
|
|
28
|
+
"tasty/no-style-spread": "warn",
|
|
27
29
|
"tasty/consistent-token-usage": "warn",
|
|
28
30
|
"tasty/prefer-auto-calc": "warn",
|
|
31
|
+
"tasty/prefer-state-negation": "warn",
|
|
29
32
|
"tasty/prefer-custom-property-syntax": "warn",
|
|
30
33
|
"tasty/prefer-hide": "warn",
|
|
31
34
|
"tasty/prefer-directional-shorthand": "warn",
|
|
@@ -36,7 +39,6 @@ const strict = {
|
|
|
36
39
|
"tasty/valid-custom-property": "warn",
|
|
37
40
|
"tasty/no-unknown-state-alias": "warn",
|
|
38
41
|
"tasty/no-styles-prop": "warn",
|
|
39
|
-
"tasty/no-runtime-styles-mutation": "warn",
|
|
40
42
|
"tasty/valid-state-definition": "warn"
|
|
41
43
|
};
|
|
42
44
|
|
package/dist/configs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"configs.js","names":[],"sources":["../src/configs.ts"],"sourcesContent":["import type { TSESLint } from '@typescript-eslint/utils';\n\nexport const recommended: TSESLint.SharedConfig.RulesRecord = {\n 'tasty/known-property': 'warn',\n 'tasty/valid-value': 'error',\n 'tasty/valid-color-token': 'error',\n 'tasty/valid-custom-unit': 'error',\n 'tasty/valid-boolean-property': 'error',\n 'tasty/valid-state-key': 'error',\n 'tasty/valid-styles-structure': 'error',\n 'tasty/no-nested-state-map': 'error',\n 'tasty/no-important': 'error',\n 'tasty/valid-sub-element': 'error',\n 'tasty/valid-directional-modifier': 'error',\n 'tasty/valid-radius-shape': 'error',\n 'tasty/no-nested-selector': 'error',\n 'tasty/static-no-dynamic-values': 'error',\n 'tasty/static-valid-selector': 'error',\n 'tasty/valid-preset': 'error',\n 'tasty/valid-recipe': 'error',\n 'tasty/valid-transition': 'warn',\n 'tasty/require-default-state': 'error',\n 'tasty/no-own-at-root': 'warn',\n 'tasty/valid-default-state-order': 'warn',\n 'tasty/prefer-shorthand-property': 'warn',\n 'tasty/no-raw-color-values': 'warn',\n 'tasty/no-raw-transition-duration': 'warn',\n 'tasty/consistent-token-usage': 'warn',\n 'tasty/prefer-auto-calc': 'warn',\n 'tasty/prefer-custom-property-syntax': 'warn',\n 'tasty/prefer-hide': 'warn',\n 'tasty/prefer-directional-shorthand': 'warn',\n 'tasty/prefer-longhand-property': 'error',\n};\n\nexport const strict: TSESLint.SharedConfig.RulesRecord = {\n ...recommended,\n 'tasty/valid-custom-property': 'warn',\n 'tasty/no-unknown-state-alias': 'warn',\n 'tasty/no-styles-prop': 'warn',\n 'tasty/
|
|
1
|
+
{"version":3,"file":"configs.js","names":[],"sources":["../src/configs.ts"],"sourcesContent":["import type { TSESLint } from '@typescript-eslint/utils';\n\nexport const recommended: TSESLint.SharedConfig.RulesRecord = {\n 'tasty/known-property': 'warn',\n 'tasty/valid-value': 'error',\n 'tasty/valid-color-token': 'error',\n 'tasty/valid-custom-unit': 'error',\n 'tasty/valid-boolean-property': 'error',\n 'tasty/valid-state-key': 'error',\n 'tasty/valid-styles-structure': 'error',\n 'tasty/no-nested-state-map': 'error',\n 'tasty/no-important': 'error',\n 'tasty/valid-sub-element': 'error',\n 'tasty/valid-directional-modifier': 'error',\n 'tasty/valid-radius-shape': 'error',\n 'tasty/no-nested-selector': 'error',\n 'tasty/static-no-dynamic-values': 'error',\n 'tasty/static-valid-selector': 'error',\n 'tasty/valid-preset': 'error',\n 'tasty/valid-recipe': 'error',\n 'tasty/valid-transition': 'warn',\n 'tasty/require-default-state': 'error',\n 'tasty/no-own-at-root': 'warn',\n 'tasty/valid-default-state-order': 'warn',\n 'tasty/prefer-shorthand-property': 'warn',\n 'tasty/no-raw-color-values': 'warn',\n 'tasty/no-raw-transition-duration': 'warn',\n 'tasty/no-runtime-styles-mutation': 'warn',\n 'tasty/no-style-spread': 'warn',\n 'tasty/consistent-token-usage': 'warn',\n 'tasty/prefer-auto-calc': 'warn',\n 'tasty/prefer-state-negation': 'warn',\n 'tasty/prefer-custom-property-syntax': 'warn',\n 'tasty/prefer-hide': 'warn',\n 'tasty/prefer-directional-shorthand': 'warn',\n 'tasty/prefer-longhand-property': 'error',\n};\n\nexport const strict: TSESLint.SharedConfig.RulesRecord = {\n ...recommended,\n 'tasty/valid-custom-property': 'warn',\n 'tasty/no-unknown-state-alias': 'warn',\n 'tasty/no-styles-prop': 'warn',\n 'tasty/valid-state-definition': 'warn',\n};\n"],"mappings":";AAEA,MAAa,cAAiD;CAC5D,wBAAwB;CACxB,qBAAqB;CACrB,2BAA2B;CAC3B,2BAA2B;CAC3B,gCAAgC;CAChC,yBAAyB;CACzB,gCAAgC;CAChC,6BAA6B;CAC7B,sBAAsB;CACtB,2BAA2B;CAC3B,oCAAoC;CACpC,4BAA4B;CAC5B,4BAA4B;CAC5B,kCAAkC;CAClC,+BAA+B;CAC/B,sBAAsB;CACtB,sBAAsB;CACtB,0BAA0B;CAC1B,+BAA+B;CAC/B,wBAAwB;CACxB,mCAAmC;CACnC,mCAAmC;CACnC,6BAA6B;CAC7B,oCAAoC;CACpC,oCAAoC;CACpC,yBAAyB;CACzB,gCAAgC;CAChC,0BAA0B;CAC1B,+BAA+B;CAC/B,uCAAuC;CACvC,qBAAqB;CACrB,sCAAsC;CACtC,kCAAkC;CACnC;AAED,MAAa,SAA4C;CACvD,GAAG;CACH,+BAA+B;CAC/B,gCAAgC;CAChC,wBAAwB;CACxB,gCAAgC;CACjC"}
|
package/dist/index.js
CHANGED
|
@@ -26,10 +26,12 @@ import no_raw_motion_duration_default from "./rules/no-raw-motion-duration.js";
|
|
|
26
26
|
import no_styles_prop_default from "./rules/no-styles-prop.js";
|
|
27
27
|
import consistent_token_usage_default from "./rules/consistent-token-usage.js";
|
|
28
28
|
import no_runtime_styles_mutation_default from "./rules/no-runtime-styles-mutation.js";
|
|
29
|
+
import no_style_spread_default from "./rules/no-style-spread.js";
|
|
29
30
|
import valid_state_definition_default from "./rules/valid-state-definition.js";
|
|
30
31
|
import no_own_at_root_default from "./rules/no-own-at-root.js";
|
|
31
32
|
import valid_default_state_order_default from "./rules/valid-default-state-order.js";
|
|
32
33
|
import prefer_auto_calc_default from "./rules/prefer-auto-calc.js";
|
|
34
|
+
import prefer_state_negation_default from "./rules/prefer-state-negation.js";
|
|
33
35
|
import prefer_custom_property_syntax_default from "./rules/prefer-custom-property-syntax.js";
|
|
34
36
|
import prefer_hide_default from "./rules/prefer-hide.js";
|
|
35
37
|
import prefer_directional_shorthand_default from "./rules/prefer-directional-shorthand.js";
|
|
@@ -67,10 +69,12 @@ const ruleMap = {
|
|
|
67
69
|
"no-styles-prop": no_styles_prop_default,
|
|
68
70
|
"consistent-token-usage": consistent_token_usage_default,
|
|
69
71
|
"no-runtime-styles-mutation": no_runtime_styles_mutation_default,
|
|
72
|
+
"no-style-spread": no_style_spread_default,
|
|
70
73
|
"valid-state-definition": valid_state_definition_default,
|
|
71
74
|
"no-own-at-root": no_own_at_root_default,
|
|
72
75
|
"valid-default-state-order": valid_default_state_order_default,
|
|
73
76
|
"prefer-auto-calc": prefer_auto_calc_default,
|
|
77
|
+
"prefer-state-negation": prefer_state_negation_default,
|
|
74
78
|
"prefer-custom-property-syntax": prefer_custom_property_syntax_default,
|
|
75
79
|
"prefer-hide": prefer_hide_default,
|
|
76
80
|
"prefer-directional-shorthand": prefer_directional_shorthand_default,
|
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.noUnknownStateAlias","rules.noRawColorValues","rules.noRawTransitionDuration","rules.noRawMotionDuration","rules.noStylesProp","rules.consistentTokenUsage","rules.noRuntimeStylesMutation","rules.validStateDefinition","rules.noOwnAtRoot","rules.validDefaultStateOrder","rules.preferAutoCalc","rules.preferCustomPropertySyntax","rules.preferHide","rules.preferDirectionalShorthand","rules.preferLonghandProperty"],"sources":["../src/index.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport 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-unknown-state-alias': rules.noUnknownStateAlias,\n 'no-raw-color-values': rules.noRawColorValues,\n 'no-raw-transition-duration': rules.noRawTransitionDuration,\n 'no-raw-motion-duration': rules.noRawMotionDuration,\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 'no-own-at-root': rules.noOwnAtRoot,\n 'valid-default-state-order': rules.validDefaultStateOrder,\n 'prefer-auto-calc': rules.preferAutoCalc,\n 'prefer-custom-property-syntax': rules.preferCustomPropertySyntax,\n 'prefer-hide': rules.preferHide,\n 'prefer-directional-shorthand': rules.preferDirectionalShorthand,\n 'prefer-longhand-property': rules.preferLonghandProperty,\n};\n\n// Read from package.json so `meta.version` cannot drift from the released\n// version, as the previous hardcoded literal had. `../package.json` resolves to\n// the package root from both `src/` and the unbundled `dist/`.\nconst { version } = createRequire(import.meta.url)('../package.json') as {\n version: string;\n};\n\nconst plugin = {\n meta: {\n name: '@tenphi/eslint-plugin-tasty',\n version,\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 {\n TastyValidationConfig,\n ResolvedConfig,\n StyleFunctionConfig,\n} from './types.js';\n"],"mappings":"
|
|
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.noUnknownStateAlias","rules.noRawColorValues","rules.noRawTransitionDuration","rules.noRawMotionDuration","rules.noStylesProp","rules.consistentTokenUsage","rules.noRuntimeStylesMutation","rules.noStyleSpread","rules.validStateDefinition","rules.noOwnAtRoot","rules.validDefaultStateOrder","rules.preferAutoCalc","rules.preferStateNegation","rules.preferCustomPropertySyntax","rules.preferHide","rules.preferDirectionalShorthand","rules.preferLonghandProperty"],"sources":["../src/index.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport 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-unknown-state-alias': rules.noUnknownStateAlias,\n 'no-raw-color-values': rules.noRawColorValues,\n 'no-raw-transition-duration': rules.noRawTransitionDuration,\n 'no-raw-motion-duration': rules.noRawMotionDuration,\n 'no-styles-prop': rules.noStylesProp,\n 'consistent-token-usage': rules.consistentTokenUsage,\n 'no-runtime-styles-mutation': rules.noRuntimeStylesMutation,\n 'no-style-spread': rules.noStyleSpread,\n 'valid-state-definition': rules.validStateDefinition,\n 'no-own-at-root': rules.noOwnAtRoot,\n 'valid-default-state-order': rules.validDefaultStateOrder,\n 'prefer-auto-calc': rules.preferAutoCalc,\n 'prefer-state-negation': rules.preferStateNegation,\n 'prefer-custom-property-syntax': rules.preferCustomPropertySyntax,\n 'prefer-hide': rules.preferHide,\n 'prefer-directional-shorthand': rules.preferDirectionalShorthand,\n 'prefer-longhand-property': rules.preferLonghandProperty,\n};\n\n// Read from package.json so `meta.version` cannot drift from the released\n// version, as the previous hardcoded literal had. `../package.json` resolves to\n// the package root from both `src/` and the unbundled `dist/`.\nconst { version } = createRequire(import.meta.url)('../package.json') as {\n version: string;\n};\n\nconst plugin = {\n meta: {\n name: '@tenphi/eslint-plugin-tasty',\n version,\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 {\n TastyValidationConfig,\n ResolvedConfig,\n StyleFunctionConfig,\n} from './types.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAKA,MAAM,UAAkE;CACtE,kBAAkBA;CAClB,eAAeC;CACf,qBAAqBC;CACrB,qBAAqBC;CACrB,mBAAmBC;CACnB,0BAA0BC;CAC1B,sBAAsBC;CACtB,yBAAyBC;CACzB,gBAAgBC;CAChB,gBAAgBC;CAChB,0BAA0BC;CAC1B,8BAA8BC;CAC9B,sBAAsBC;CACtB,gBAAgBC;CAChB,qBAAqBC;CACrB,uBAAuBC;CACvB,4BAA4BC;CAC5B,yBAAyBC;CACzB,6BAA6BC;CAC7B,oBAAoBC;CACpB,yBAAyBC;CACzB,0BAA0BC;CAC1B,uBAAuBC;CACvB,8BAA8BC;CAC9B,0BAA0BC;CAC1B,kBAAkBC;CAClB,0BAA0BC;CAC1B,8BAA8BC;CAC9B,mBAAmBC;CACnB,0BAA0BC;CAC1B,kBAAkBC;CAClB,6BAA6BC;CAC7B,oBAAoBC;CACpB,yBAAyBC;CACzB,iCAAiCC;CACjC,eAAeC;CACf,gCAAgCC;CAChC,4BAA4BC;CAC7B;AAKD,MAAM,EAAE,YAAY,cAAc,OAAO,KAAK,IAAI,CAAC,kBAAkB;AAIrE,MAAM,SAAS;CACb,MAAM;EACJ,MAAM;EACN;EACD;CACD,OAAO;CACP,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,4 +1,5 @@
|
|
|
1
1
|
import { KNOWN_PSEUDO_CLASSES } from "../constants.js";
|
|
2
|
+
import { checkBracketBalance } from "./utils.js";
|
|
2
3
|
|
|
3
4
|
//#region src/parsers/state-key-parser.ts
|
|
4
5
|
/**
|
|
@@ -461,6 +462,29 @@ function findTopLevelComma(str) {
|
|
|
461
462
|
return -1;
|
|
462
463
|
}
|
|
463
464
|
/**
|
|
465
|
+
* Rewrite :not() state atoms, leaving raw CSS inside other functions alone.
|
|
466
|
+
* A selector list/compound must stay inside :is(): negating individual parts
|
|
467
|
+
* would change which elements match. Only a single pseudo can be unwrapped.
|
|
468
|
+
*/
|
|
469
|
+
function getStateNegationEdits(stateKey) {
|
|
470
|
+
if (!stateKey.includes(":not(")) return [];
|
|
471
|
+
if (checkBracketBalance(stateKey)) return [];
|
|
472
|
+
if (parseStateKey(stateKey).errors.length > 0) return [];
|
|
473
|
+
const { tokens } = tokenize(stateKey);
|
|
474
|
+
return tokens.filter((token) => token.type === "STATE" && token.value.startsWith(":not(")).flatMap((token) => {
|
|
475
|
+
if (checkBracketBalance(token.value)) return [];
|
|
476
|
+
const inner = token.value.slice(5, -1).trim();
|
|
477
|
+
if (!inner) return [];
|
|
478
|
+
const { tokens: innerTokens, errors } = tokenize(inner);
|
|
479
|
+
const singlePseudo = errors.length === 0 && innerTokens.length === 1 && innerTokens[0].type === "STATE" && innerTokens[0].value === inner && inner.startsWith(":") && parseStateKey(inner).errors.length === 0 && !inner.startsWith(":not(") && !inner.startsWith(":is(") && !inner.startsWith(":where(") && !(inner.startsWith(":has(") && /[>+~]\s*\)$/.test(inner));
|
|
480
|
+
return [{
|
|
481
|
+
start: token.offset,
|
|
482
|
+
end: token.offset + token.length,
|
|
483
|
+
replacement: singlePseudo ? `!${inner}` : `!:is(${inner})`
|
|
484
|
+
}];
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
464
488
|
* Validate a state key string and return detailed errors.
|
|
465
489
|
*/
|
|
466
490
|
function parseStateKey(stateKey, opts = {}) {
|
|
@@ -507,5 +531,5 @@ function validateStateDefinition(value, opts = {}) {
|
|
|
507
531
|
}
|
|
508
532
|
|
|
509
533
|
//#endregion
|
|
510
|
-
export { parseStateKey, validateStateDefinition };
|
|
534
|
+
export { getStateNegationEdits, parseStateKey, validateStateDefinition };
|
|
511
535
|
//# sourceMappingURL=state-key-parser.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state-key-parser.js","names":[],"sources":["../../src/parsers/state-key-parser.ts"],"sourcesContent":["import { 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): {\n tokens: Token[];\n errors: StateKeyError[];\n} {\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(/^(\\S+)\\s*(<=|>=|<|>|=)\\s*(.+)$/);\n if (simpleMatch) {\n const dim = simpleMatch[1];\n if (DIMENSION_FULL.has(dim) || DIMENSION_SHORTHANDS.has(dim)) {\n return;\n }\n }\n\n // Reversed: 768px > width\n const reversedMatch = condition.match(/^(.+?)\\s*(<=|>=|<|>|=)\\s*(\\S+)$/);\n if (reversedMatch) {\n const dim = reversedMatch[3];\n if (DIMENSION_FULL.has(dim) || DIMENSION_SHORTHANDS.has(dim)) {\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 trimmed = stateKey.trim();\n\n // The standalone `_` key is the map-wide fallback floor. It always applies\n // and must be used on its own — it carries no state logic to validate.\n if (trimmed === '_') {\n return { errors: [], hasOwn: false, referencedAliases: [] };\n }\n\n // A `_` combined with state logic (`_ & hovered`, `_ | x`) is a misuse: the\n // fallback floor can only be used on its own. Tasty ignores such keys at\n // runtime and warns; flag them here so the mistake surfaces in the editor.\n if (isMisusedFallbackKey(trimmed)) {\n return {\n errors: [\n {\n message:\n 'The fallback floor \"_\" cannot be combined with other state ' +\n 'logic. Use \"_\" on its own as a map-wide fallback floor.',\n offset: 0,\n length: stateKey.length,\n },\n ],\n hasOwn: false,\n referencedAliases: [],\n };\n }\n\n const { tokens, errors: tokenErrors } = tokenize(trimmed);\n const validator = new StateKeyValidator(tokens, tokenErrors, opts);\n return validator.validate();\n}\n\n/**\n * Detect a misused standalone `_` fallback key: a key that is not exactly\n * `_` but contains `_` as a standalone atom among state-logic operators\n * (`&`, `|`, `^`, `,`) or grouping parens. A `_` embedded in a longer token\n * (e.g. a modifier named `_private`) is not a misuse.\n */\nfunction isMisusedFallbackKey(stateKey: string): boolean {\n if (stateKey === '_' || !stateKey.includes('_')) return false;\n return /(?:^|[\\s&|^,(])_(?:[\\s&|^,)]|$)/.test(stateKey);\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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"}
|
|
1
|
+
{"version":3,"file":"state-key-parser.js","names":[],"sources":["../../src/parsers/state-key-parser.ts"],"sourcesContent":["import { KNOWN_PSEUDO_CLASSES } from '../constants.js';\nimport type { StringEdit } from '../fix-utils.js';\nimport { checkBracketBalance } from './utils.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): {\n tokens: Token[];\n errors: StateKeyError[];\n} {\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(/^(\\S+)\\s*(<=|>=|<|>|=)\\s*(.+)$/);\n if (simpleMatch) {\n const dim = simpleMatch[1];\n if (DIMENSION_FULL.has(dim) || DIMENSION_SHORTHANDS.has(dim)) {\n return;\n }\n }\n\n // Reversed: 768px > width\n const reversedMatch = condition.match(/^(.+?)\\s*(<=|>=|<|>|=)\\s*(\\S+)$/);\n if (reversedMatch) {\n const dim = reversedMatch[3];\n if (DIMENSION_FULL.has(dim) || DIMENSION_SHORTHANDS.has(dim)) {\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 * Rewrite :not() state atoms, leaving raw CSS inside other functions alone.\n * A selector list/compound must stay inside :is(): negating individual parts\n * would change which elements match. Only a single pseudo can be unwrapped.\n */\nexport function getStateNegationEdits(stateKey: string): StringEdit[] {\n if (!stateKey.includes(':not(')) return [];\n if (checkBracketBalance(stateKey)) return [];\n if (parseStateKey(stateKey).errors.length > 0) return [];\n\n const { tokens } = tokenize(stateKey);\n return tokens\n .filter(\n (token) => token.type === 'STATE' && token.value.startsWith(':not('),\n )\n .flatMap((token) => {\n if (checkBracketBalance(token.value)) return [];\n const inner = token.value.slice(5, -1).trim();\n if (!inner) return [];\n\n const { tokens: innerTokens, errors } = tokenize(inner);\n const singlePseudo =\n errors.length === 0 &&\n innerTokens.length === 1 &&\n innerTokens[0].type === 'STATE' &&\n innerTokens[0].value === inner &&\n inner.startsWith(':') &&\n parseStateKey(inner).errors.length === 0 &&\n !inner.startsWith(':not(') &&\n // Keep forgiving selector-list wrappers, even for invalid branches.\n !inner.startsWith(':is(') &&\n !inner.startsWith(':where(') &&\n // Promoting :has() to a state atom enables Tasty's trailing-combinator\n // completion. Preserve the original raw CSS instead in that case.\n !(inner.startsWith(':has(') && /[>+~]\\s*\\)$/.test(inner));\n\n return [\n {\n start: token.offset,\n end: token.offset + token.length,\n replacement: singlePseudo ? `!${inner}` : `!:is(${inner})`,\n },\n ];\n });\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 trimmed = stateKey.trim();\n\n // The standalone `_` key is the map-wide fallback floor. It always applies\n // and must be used on its own — it carries no state logic to validate.\n if (trimmed === '_') {\n return { errors: [], hasOwn: false, referencedAliases: [] };\n }\n\n // A `_` combined with state logic (`_ & hovered`, `_ | x`) is a misuse: the\n // fallback floor can only be used on its own. Tasty ignores such keys at\n // runtime and warns; flag them here so the mistake surfaces in the editor.\n if (isMisusedFallbackKey(trimmed)) {\n return {\n errors: [\n {\n message:\n 'The fallback floor \"_\" cannot be combined with other state ' +\n 'logic. Use \"_\" on its own as a map-wide fallback floor.',\n offset: 0,\n length: stateKey.length,\n },\n ],\n hasOwn: false,\n referencedAliases: [],\n };\n }\n\n const { tokens, errors: tokenErrors } = tokenize(trimmed);\n const validator = new StateKeyValidator(tokens, tokenErrors, opts);\n return validator.validate();\n}\n\n/**\n * Detect a misused standalone `_` fallback key: a key that is not exactly\n * `_` but contains `_` as a standalone atom among state-logic operators\n * (`&`, `|`, `^`, `,`) or grouping parens. A `_` embedded in a longer token\n * (e.g. a modifier named `_private`) is not a misuse.\n */\nfunction isMisusedFallbackKey(stateKey: string): boolean {\n if (stateKey === '_' || !stateKey.includes('_')) return false;\n return /(?:^|[\\s&|^,(])_(?:[\\s&|^,)]|$)/.test(stateKey);\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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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRule } from "../create-rule.js";
|
|
2
|
-
import {
|
|
2
|
+
import { SPECIAL_STYLE_KEYS } from "../constants.js";
|
|
3
|
+
import { getKeyName, isStaticValue, unwrapExpression } from "../utils.js";
|
|
3
4
|
import { TastyContext, styleObjectListeners } from "../context.js";
|
|
4
5
|
|
|
5
6
|
//#region src/rules/no-runtime-styles-mutation.ts
|
|
@@ -7,39 +8,47 @@ var no_runtime_styles_mutation_default = createRule({
|
|
|
7
8
|
name: "no-runtime-styles-mutation",
|
|
8
9
|
meta: {
|
|
9
10
|
type: "suggestion",
|
|
10
|
-
docs: { description: "Warn when style objects contain
|
|
11
|
-
messages: {
|
|
11
|
+
docs: { description: "Warn when style objects contain JavaScript-computed values" },
|
|
12
|
+
messages: {
|
|
13
|
+
dynamicStyleValue: "Style value for '{{property}}' should be static. Use states with mods, tokens, or styleProps for dynamic behavior.",
|
|
14
|
+
dynamicStyleKey: "Style key for '{{property}}' should be static. Define states as literal keys instead of computing them in JavaScript."
|
|
15
|
+
},
|
|
12
16
|
schema: []
|
|
13
17
|
},
|
|
14
18
|
defaultOptions: [],
|
|
15
19
|
create(context) {
|
|
16
20
|
const ctx = new TastyContext(context);
|
|
17
|
-
function
|
|
21
|
+
function checkValue(node, property, isStateMap = false) {
|
|
22
|
+
const value = unwrapExpression(node);
|
|
23
|
+
if (value.type === "ObjectExpression") checkProperties(value, property, isStateMap);
|
|
24
|
+
else if (value.type === "ArrayExpression") {
|
|
25
|
+
for (const element of value.elements) if (element && element.type !== "SpreadElement") checkValue(element, property);
|
|
26
|
+
} else if (!isStaticValue(value)) context.report({
|
|
27
|
+
node: value,
|
|
28
|
+
messageId: "dynamicStyleValue",
|
|
29
|
+
data: { property }
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function checkProperties(node, parentProperty = "(style)", isStateMap = false) {
|
|
18
33
|
for (const prop of node.properties) {
|
|
19
|
-
if (prop.type === "SpreadElement")
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (prop.type !== "Property") continue;
|
|
28
|
-
const key = !prop.computed && prop.key.type === "Identifier" ? prop.key.name : parentProperty;
|
|
29
|
-
if (!prop.computed && prop.key.type === "Identifier" && /^[A-Z]/.test(prop.key.name)) {
|
|
30
|
-
if (prop.value.type === "ObjectExpression") checkProperties(prop.value, key);
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
if (!prop.computed && prop.key.type === "Identifier" && prop.key.name.startsWith("@")) continue;
|
|
34
|
-
if (!isStaticValue(prop.value)) context.report({
|
|
35
|
-
node: prop.value,
|
|
36
|
-
messageId: "dynamicStyleValue",
|
|
37
|
-
data: { property: key ?? "(unknown)" }
|
|
34
|
+
if (prop.type === "SpreadElement") continue;
|
|
35
|
+
const key = !prop.computed ? getKeyName(prop.key) : null;
|
|
36
|
+
if (key && SPECIAL_STYLE_KEYS.has(key)) continue;
|
|
37
|
+
const property = isStateMap || prop.computed ? parentProperty : key ?? parentProperty;
|
|
38
|
+
if (prop.computed) context.report({
|
|
39
|
+
node: prop.key,
|
|
40
|
+
messageId: "dynamicStyleKey",
|
|
41
|
+
data: { property }
|
|
38
42
|
});
|
|
43
|
+
const value = unwrapExpression(prop.value);
|
|
44
|
+
const childIsStateMap = value.type === "ObjectExpression" && ctx.isStateMap(value, prop);
|
|
45
|
+
checkValue(prop.value, property, childIsStateMap);
|
|
39
46
|
}
|
|
40
47
|
}
|
|
41
48
|
function handleStyleObject(node) {
|
|
42
|
-
|
|
49
|
+
const styleCtx = ctx.getStyleContext(node);
|
|
50
|
+
if (!styleCtx || styleCtx.isStaticCall) return;
|
|
51
|
+
if (ctx.getRootStyleObject(node) !== node) return;
|
|
43
52
|
checkProperties(node);
|
|
44
53
|
}
|
|
45
54
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-runtime-styles-mutation.js","names":[],"sources":["../../src/rules/no-runtime-styles-mutation.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { isStaticValue } from '../utils.js';\n\ntype MessageIds = 'dynamicStyleValue';\n\nexport default createRule<[], MessageIds>({\n name: 'no-runtime-styles-mutation',\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Warn when style objects contain
|
|
1
|
+
{"version":3,"file":"no-runtime-styles-mutation.js","names":[],"sources":["../../src/rules/no-runtime-styles-mutation.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { SPECIAL_STYLE_KEYS } from '../constants.js';\nimport { getKeyName, isStaticValue, unwrapExpression } from '../utils.js';\n\ntype MessageIds = 'dynamicStyleValue' | 'dynamicStyleKey';\n\nexport default createRule<[], MessageIds>({\n name: 'no-runtime-styles-mutation',\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Warn when style objects contain JavaScript-computed values',\n },\n messages: {\n dynamicStyleValue:\n \"Style value for '{{property}}' should be static. Use states with mods, tokens, or styleProps for dynamic behavior.\",\n dynamicStyleKey:\n \"Style key for '{{property}}' should be static. Define states as literal keys instead of computing them in JavaScript.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkValue(\n node: TSESTree.Node,\n property: string,\n isStateMap = false,\n ): void {\n const value = unwrapExpression(node);\n\n if (value.type === 'ObjectExpression') {\n checkProperties(value, property, isStateMap);\n } else if (value.type === 'ArrayExpression') {\n for (const element of value.elements) {\n // A separate rule reports spreads, including array spreads.\n if (element && element.type !== 'SpreadElement') {\n checkValue(element, property);\n }\n }\n } else if (!isStaticValue(value)) {\n context.report({\n node: value,\n messageId: 'dynamicStyleValue',\n data: { property },\n });\n }\n }\n\n function checkProperties(\n node: TSESTree.ObjectExpression,\n parentProperty = '(style)',\n isStateMap = false,\n ): void {\n for (const prop of node.properties) {\n // Spreads have their own warning and explicit per-line suppression.\n if (prop.type === 'SpreadElement') continue;\n\n // At-rule definitions have their own shape and may legitimately be\n // assembled as JavaScript objects (for example, shared keyframes).\n const key = !prop.computed ? getKeyName(prop.key) : null;\n if (key && SPECIAL_STYLE_KEYS.has(key)) continue;\n\n const property =\n isStateMap || prop.computed\n ? parentProperty\n : (key ?? parentProperty);\n\n if (prop.computed) {\n context.report({\n node: prop.key,\n messageId: 'dynamicStyleKey',\n data: { property },\n });\n }\n\n const value = unwrapExpression(prop.value);\n const childIsStateMap =\n value.type === 'ObjectExpression' && ctx.isStateMap(value, prop);\n checkValue(prop.value, property, childIsStateMap);\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression): void {\n const styleCtx = ctx.getStyleContext(node);\n // tastyStatic has a stricter build-time error for dynamic values.\n if (!styleCtx || styleCtx.isStaticCall) return;\n // Walk the full tree once; sub-element objects also match AST selectors.\n if (ctx.getRootStyleObject(node) !== node) return;\n\n checkProperties(node);\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAQA,yCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,8DACd;EACD,UAAU;GACR,mBACE;GACF,iBACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,WACP,MACA,UACA,aAAa,OACP;GACN,MAAM,QAAQ,iBAAiB,KAAK;AAEpC,OAAI,MAAM,SAAS,mBACjB,iBAAgB,OAAO,UAAU,WAAW;YACnC,MAAM,SAAS,mBACxB;SAAK,MAAM,WAAW,MAAM,SAE1B,KAAI,WAAW,QAAQ,SAAS,gBAC9B,YAAW,SAAS,SAAS;cAGxB,CAAC,cAAc,MAAM,CAC9B,SAAQ,OAAO;IACb,MAAM;IACN,WAAW;IACX,MAAM,EAAE,UAAU;IACnB,CAAC;;EAIN,SAAS,gBACP,MACA,iBAAiB,WACjB,aAAa,OACP;AACN,QAAK,MAAM,QAAQ,KAAK,YAAY;AAElC,QAAI,KAAK,SAAS,gBAAiB;IAInC,MAAM,MAAM,CAAC,KAAK,WAAW,WAAW,KAAK,IAAI,GAAG;AACpD,QAAI,OAAO,mBAAmB,IAAI,IAAI,CAAE;IAExC,MAAM,WACJ,cAAc,KAAK,WACf,iBACC,OAAO;AAEd,QAAI,KAAK,SACP,SAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACX,MAAM,EAAE,UAAU;KACnB,CAAC;IAGJ,MAAM,QAAQ,iBAAiB,KAAK,MAAM;IAC1C,MAAM,kBACJ,MAAM,SAAS,sBAAsB,IAAI,WAAW,OAAO,KAAK;AAClE,eAAW,KAAK,OAAO,UAAU,gBAAgB;;;EAIrD,SAAS,kBAAkB,MAAuC;GAChE,MAAM,WAAW,IAAI,gBAAgB,KAAK;AAE1C,OAAI,CAAC,YAAY,SAAS,aAAc;AAExC,OAAI,IAAI,mBAAmB,KAAK,KAAK,KAAM;AAE3C,mBAAgB,KAAK;;AAGvB,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createRule } from "../create-rule.js";
|
|
2
|
+
import { unwrapExpression } from "../utils.js";
|
|
3
|
+
import { TastyContext, styleObjectListeners } from "../context.js";
|
|
4
|
+
|
|
5
|
+
//#region src/rules/no-style-spread.ts
|
|
6
|
+
var no_style_spread_default = createRule({
|
|
7
|
+
name: "no-style-spread",
|
|
8
|
+
meta: {
|
|
9
|
+
type: "suggestion",
|
|
10
|
+
docs: { description: "Warn about spreads inside Tasty style objects" },
|
|
11
|
+
messages: { styleSpread: "List Tasty style properties explicitly instead of spreading. If this spread is intentional, suppress this warning on this line with an ESLint comment and a reason." },
|
|
12
|
+
schema: []
|
|
13
|
+
},
|
|
14
|
+
defaultOptions: [],
|
|
15
|
+
create(context) {
|
|
16
|
+
const ctx = new TastyContext(context);
|
|
17
|
+
function checkValue(node) {
|
|
18
|
+
const value = unwrapExpression(node);
|
|
19
|
+
if (value.type === "ObjectExpression") for (const prop of value.properties) if (prop.type === "SpreadElement") context.report({
|
|
20
|
+
node: prop,
|
|
21
|
+
messageId: "styleSpread"
|
|
22
|
+
});
|
|
23
|
+
else checkValue(prop.value);
|
|
24
|
+
else if (value.type === "ArrayExpression") for (const element of value.elements) {
|
|
25
|
+
if (!element) continue;
|
|
26
|
+
if (element.type === "SpreadElement") context.report({
|
|
27
|
+
node: element,
|
|
28
|
+
messageId: "styleSpread"
|
|
29
|
+
});
|
|
30
|
+
else checkValue(element);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function handleStyleObject(node) {
|
|
34
|
+
const styleCtx = ctx.getStyleContext(node);
|
|
35
|
+
if (!styleCtx || styleCtx.isStaticCall) return;
|
|
36
|
+
if (ctx.getRootStyleObject(node) !== node) return;
|
|
37
|
+
checkValue(node);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
ImportDeclaration(node) {
|
|
41
|
+
ctx.trackImport(node);
|
|
42
|
+
},
|
|
43
|
+
...styleObjectListeners(handleStyleObject)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
//#endregion
|
|
49
|
+
export { no_style_spread_default as default };
|
|
50
|
+
//# sourceMappingURL=no-style-spread.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-style-spread.js","names":[],"sources":["../../src/rules/no-style-spread.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { unwrapExpression } from '../utils.js';\n\ntype MessageIds = 'styleSpread';\n\nexport default createRule<[], MessageIds>({\n name: 'no-style-spread',\n meta: {\n type: 'suggestion',\n docs: {\n description: 'Warn about spreads inside Tasty style objects',\n },\n messages: {\n styleSpread:\n 'List Tasty style properties explicitly instead of spreading. If this spread is intentional, suppress this warning on this line with an ESLint comment and a reason.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkValue(node: TSESTree.Node): void {\n const value = unwrapExpression(node);\n\n if (value.type === 'ObjectExpression') {\n for (const prop of value.properties) {\n if (prop.type === 'SpreadElement') {\n context.report({ node: prop, messageId: 'styleSpread' });\n } else {\n checkValue(prop.value);\n }\n }\n } else if (value.type === 'ArrayExpression') {\n for (const element of value.elements) {\n if (!element) continue;\n if (element.type === 'SpreadElement') {\n context.report({ node: element, messageId: 'styleSpread' });\n } else {\n checkValue(element);\n }\n }\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression): void {\n const styleCtx = ctx.getStyleContext(node);\n // tastyStatic rejects spreads during extraction, so its existing error\n // gives the useful diagnostic without a second warning.\n if (!styleCtx || styleCtx.isStaticCall) return;\n if (ctx.getRootStyleObject(node) !== node) return;\n\n checkValue(node);\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;AAOA,8BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,iDACd;EACD,UAAU,EACR,aACE,uKACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,WAAW,MAA2B;GAC7C,MAAM,QAAQ,iBAAiB,KAAK;AAEpC,OAAI,MAAM,SAAS,mBACjB,MAAK,MAAM,QAAQ,MAAM,WACvB,KAAI,KAAK,SAAS,gBAChB,SAAQ,OAAO;IAAE,MAAM;IAAM,WAAW;IAAe,CAAC;OAExD,YAAW,KAAK,MAAM;YAGjB,MAAM,SAAS,kBACxB,MAAK,MAAM,WAAW,MAAM,UAAU;AACpC,QAAI,CAAC,QAAS;AACd,QAAI,QAAQ,SAAS,gBACnB,SAAQ,OAAO;KAAE,MAAM;KAAS,WAAW;KAAe,CAAC;QAE3D,YAAW,QAAQ;;;EAM3B,SAAS,kBAAkB,MAAuC;GAChE,MAAM,WAAW,IAAI,gBAAgB,KAAK;AAG1C,OAAI,CAAC,YAAY,SAAS,aAAc;AACxC,OAAI,IAAI,mBAAmB,KAAK,KAAK,KAAM;AAE3C,cAAW,KAAK;;AAGlB,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createRule } from "../create-rule.js";
|
|
2
|
+
import { getKeyName, getStringValue } from "../utils.js";
|
|
3
|
+
import { TastyContext, styleObjectListeners } from "../context.js";
|
|
4
|
+
import { getStateNegationEdits } from "../parsers/state-key-parser.js";
|
|
5
|
+
import { replaceInStringValue } from "../fix-utils.js";
|
|
6
|
+
|
|
7
|
+
//#region src/rules/prefer-state-negation.ts
|
|
8
|
+
function applyEdits(value, edits) {
|
|
9
|
+
return edits.reduceRight((result, edit) => result.slice(0, edit.start) + edit.replacement + result.slice(edit.end), value);
|
|
10
|
+
}
|
|
11
|
+
var prefer_state_negation_default = createRule({
|
|
12
|
+
name: "prefer-state-negation",
|
|
13
|
+
meta: {
|
|
14
|
+
type: "suggestion",
|
|
15
|
+
fixable: "code",
|
|
16
|
+
docs: { description: "Prefer the ! prefix over top-level :not() in state keys" },
|
|
17
|
+
messages: { preferStateNegation: "Use the '!' prefix instead of top-level :not() in state keys." },
|
|
18
|
+
schema: []
|
|
19
|
+
},
|
|
20
|
+
defaultOptions: [],
|
|
21
|
+
create(context) {
|
|
22
|
+
const ctx = new TastyContext(context);
|
|
23
|
+
function handleStyleObject(node) {
|
|
24
|
+
if (!ctx.isStyleObject(node)) return;
|
|
25
|
+
for (const prop of node.properties) {
|
|
26
|
+
if (prop.type !== "Property" || prop.computed) continue;
|
|
27
|
+
const key = getKeyName(prop.key);
|
|
28
|
+
if (key === null || /^[A-Z@&]/.test(key) || prop.value.type !== "ObjectExpression") continue;
|
|
29
|
+
const stateMap = prop.value;
|
|
30
|
+
const hasUnknownKeys = stateMap.properties.some((stateProp) => stateProp.type !== "Property" || stateProp.computed && getStringValue(stateProp.key) === null);
|
|
31
|
+
for (const stateProp of stateMap.properties) {
|
|
32
|
+
if (stateProp.type !== "Property") continue;
|
|
33
|
+
const stateKey = !stateProp.computed ? getKeyName(stateProp.key) : getStringValue(stateProp.key);
|
|
34
|
+
if (stateKey === null) continue;
|
|
35
|
+
const edits = getStateNegationEdits(stateKey);
|
|
36
|
+
if (edits.length === 0) continue;
|
|
37
|
+
context.report({
|
|
38
|
+
node: stateProp.key,
|
|
39
|
+
messageId: "preferStateNegation",
|
|
40
|
+
fix(fixer) {
|
|
41
|
+
if (hasUnknownKeys) return null;
|
|
42
|
+
const replacement = applyEdits(stateKey, edits);
|
|
43
|
+
if (stateMap.properties.some((other) => {
|
|
44
|
+
if (other === stateProp || other.type !== "Property") return false;
|
|
45
|
+
const otherKey = other.computed ? getStringValue(other.key) : getKeyName(other.key);
|
|
46
|
+
return otherKey !== null && applyEdits(otherKey, getStateNegationEdits(otherKey)) === replacement;
|
|
47
|
+
})) return null;
|
|
48
|
+
return replaceInStringValue(fixer, stateProp.key, edits, context.sourceCode);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
ImportDeclaration(node) {
|
|
56
|
+
ctx.trackImport(node);
|
|
57
|
+
},
|
|
58
|
+
...styleObjectListeners(handleStyleObject)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
export { prefer_state_negation_default as default };
|
|
65
|
+
//# sourceMappingURL=prefer-state-negation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefer-state-negation.js","names":[],"sources":["../../src/rules/prefer-state-negation.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { getStateNegationEdits } from '../parsers/state-key-parser.js';\nimport { replaceInStringValue } from '../fix-utils.js';\nimport type { StringEdit } from '../fix-utils.js';\n\nfunction applyEdits(value: string, edits: StringEdit[]): string {\n return edits.reduceRight(\n (result, edit) =>\n result.slice(0, edit.start) + edit.replacement + result.slice(edit.end),\n value,\n );\n}\n\nexport default createRule<[], 'preferStateNegation'>({\n name: 'prefer-state-negation',\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description: 'Prefer the ! prefix over top-level :not() in state keys',\n },\n messages: {\n preferStateNegation:\n \"Use the '!' prefix instead of top-level :not() in state keys.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n const key = getKeyName(prop.key);\n if (\n key === null ||\n /^[A-Z@&]/.test(key) ||\n prop.value.type !== 'ObjectExpression'\n )\n continue;\n\n const stateMap = prop.value;\n // Unknown keys can collide with the replacement. Overwriting one can\n // also move a state's authored priority to an earlier object slot.\n const hasUnknownKeys = stateMap.properties.some(\n (stateProp) =>\n stateProp.type !== 'Property' ||\n (stateProp.computed && getStringValue(stateProp.key) === null),\n );\n\n for (const stateProp of stateMap.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateKey = !stateProp.computed\n ? getKeyName(stateProp.key)\n : getStringValue(stateProp.key);\n if (stateKey === null) continue;\n\n const edits = getStateNegationEdits(stateKey);\n if (edits.length === 0) continue;\n\n // Fix a whole key together so repeated :not() atoms never compete.\n context.report({\n node: stateProp.key,\n messageId: 'preferStateNegation',\n fix(fixer) {\n if (hasUnknownKeys) return null;\n const replacement = applyEdits(stateKey, edits);\n // Never overwrite another entry when two spellings coexist.\n const collision = stateMap.properties.some((other) => {\n if (other === stateProp || other.type !== 'Property')\n return false;\n const otherKey = other.computed\n ? getStringValue(other.key)\n : getKeyName(other.key);\n return (\n otherKey !== null &&\n applyEdits(otherKey, getStateNegationEdits(otherKey)) ===\n replacement\n );\n });\n if (collision) return null;\n return replaceInStringValue(\n fixer,\n stateProp.key,\n edits,\n context.sourceCode,\n );\n },\n });\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;;AAQA,SAAS,WAAW,OAAe,OAA6B;AAC9D,QAAO,MAAM,aACV,QAAQ,SACP,OAAO,MAAM,GAAG,KAAK,MAAM,GAAG,KAAK,cAAc,OAAO,MAAM,KAAK,IAAI,EACzE,MACD;;AAGH,oCAAe,WAAsC;CACnD,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aAAa,2DACd;EACD,UAAU,EACR,qBACE,iEACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAC/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QACE,QAAQ,QACR,WAAW,KAAK,IAAI,IACpB,KAAK,MAAM,SAAS,mBAEpB;IAEF,MAAM,WAAW,KAAK;IAGtB,MAAM,iBAAiB,SAAS,WAAW,MACxC,cACC,UAAU,SAAS,cAClB,UAAU,YAAY,eAAe,UAAU,IAAI,KAAK,KAC5D;AAED,SAAK,MAAM,aAAa,SAAS,YAAY;AAC3C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,WAAW,CAAC,UAAU,WACxB,WAAW,UAAU,IAAI,GACzB,eAAe,UAAU,IAAI;AACjC,SAAI,aAAa,KAAM;KAEvB,MAAM,QAAQ,sBAAsB,SAAS;AAC7C,SAAI,MAAM,WAAW,EAAG;AAGxB,aAAQ,OAAO;MACb,MAAM,UAAU;MAChB,WAAW;MACX,IAAI,OAAO;AACT,WAAI,eAAgB,QAAO;OAC3B,MAAM,cAAc,WAAW,UAAU,MAAM;AAc/C,WAZkB,SAAS,WAAW,MAAM,UAAU;AACpD,YAAI,UAAU,aAAa,MAAM,SAAS,WACxC,QAAO;QACT,MAAM,WAAW,MAAM,WACnB,eAAe,MAAM,IAAI,GACzB,WAAW,MAAM,IAAI;AACzB,eACE,aAAa,QACb,WAAW,UAAU,sBAAsB,SAAS,CAAC,KACnD;SAEJ,CACa,QAAO;AACtB,cAAO,qBACL,OACA,UAAU,KACV,OACA,QAAQ,WACT;;MAEJ,CAAC;;;;AAKR,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|