@tenphi/eslint-plugin-tasty 1.7.2 → 1.8.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/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.noStateInSelector","rules.preferElementSelector","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.noStyleProp","rules.noClassNameProp","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 'no-state-in-selector': rules.noStateInSelector,\n 'prefer-element-selector': rules.preferElementSelector,\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 'no-style-prop': rules.noStyleProp,\n 'no-classname-prop': rules.noClassNameProp,\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 { TypeAwareJSXOptions } from './type-aware-jsx.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,wBAAwBC;CACxB,2BAA2BC;CAC3B,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,iBAAiBC;CACjB,qBAAqBC;CACrB,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
+ {"version":3,"file":"index.js","names":["rules.knownProperty","rules.validValue","rules.validColorToken","rules.validCustomUnit","rules.validStateKey","rules.validStylesStructure","rules.noNestedSelector","rules.noStateInSelector","rules.preferElementSelector","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.noStyleProp","rules.noClassNameProp","rules.consistentTokenUsage","rules.noRuntimeStylesMutation","rules.noStyleSpread","rules.validStateDefinition","rules.noOwnAtRoot","rules.noIgnoredPseudoElementState","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 'no-state-in-selector': rules.noStateInSelector,\n 'prefer-element-selector': rules.preferElementSelector,\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 'no-style-prop': rules.noStyleProp,\n 'no-classname-prop': rules.noClassNameProp,\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 'no-ignored-pseudo-element-state': rules.noIgnoredPseudoElementState,\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 { TypeAwareJSXOptions } from './type-aware-jsx.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,wBAAwBC;CACxB,2BAA2BC;CAC3B,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,iBAAiBC;CACjB,qBAAqBC;CACrB,0BAA0BC;CAC1B,8BAA8BC;CAC9B,mBAAmBC;CACnB,0BAA0BC;CAC1B,kBAAkBC;CAClB,mCAAmCC;CACnC,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"}
@@ -485,6 +485,29 @@ function getStateNegationEdits(stateKey) {
485
485
  });
486
486
  }
487
487
  /**
488
+ * The state atoms of a key, without its operators and grouping. The contents
489
+ * of `@own(...)` are listed atom by atom; `@root()`, `@parent()`, and at-rule
490
+ * conditions stay single atoms, since they describe other things.
491
+ */
492
+ function getStateAtoms(stateKey) {
493
+ const atoms = [];
494
+ for (const token of tokenize(stateKey.trim()).tokens) {
495
+ if (token.type !== "STATE") continue;
496
+ if (!token.value.startsWith("@own(")) {
497
+ atoms.push({
498
+ value: token.value,
499
+ own: false
500
+ });
501
+ continue;
502
+ }
503
+ for (const inner of tokenize(token.value.slice(5, -1)).tokens) if (inner.type === "STATE") atoms.push({
504
+ value: inner.value,
505
+ own: true
506
+ });
507
+ }
508
+ return atoms;
509
+ }
510
+ /**
488
511
  * Validate a state key string and return detailed errors.
489
512
  */
490
513
  function parseStateKey(stateKey, opts = {}) {
@@ -531,5 +554,5 @@ function validateStateDefinition(value, opts = {}) {
531
554
  }
532
555
 
533
556
  //#endregion
534
- export { getStateNegationEdits, parseStateKey, validateStateDefinition };
557
+ export { getStateAtoms, getStateNegationEdits, parseStateKey, validateStateDefinition };
535
558
  //# 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';\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":";;;;;;;;;;AA2CA,MAAM,sBACJ;AAEF,SAAS,SAAS,UAGhB;CACA,MAAM,SAAkB,EAAE;CAC1B,MAAM,SAA0B,EAAE;CAGlC,MAAM,aAAa,2BAA2B,SAAS;CAEvD,MAAM,0BAAU,IAAI,KAAa;AAEjC,qBAAoB,YAAY;CAChC,IAAI;AACJ,SAAQ,QAAQ,oBAAoB,KAAK,WAAW,MAAM,MAAM;EAC9D,MAAM,YAAY,MAAM;EACxB,MAAM,SAAS,MAAM;AAErB,OAAK,IAAI,IAAI,QAAQ,IAAI,SAAS,UAAU,QAAQ,IAClD,SAAQ,IAAI,EAAE;AAGhB,MAAI,MAAM,GACR,SAAQ,WAAR;GACE,KAAK;AACH,WAAO,KAAK;KAAE,MAAM;KAAO,OAAO;KAAK;KAAQ,QAAQ;KAAG,CAAC;AAC3D;GACF,KAAK;AACH,WAAO,KAAK;KAAE,MAAM;KAAM,OAAO;KAAK;KAAQ,QAAQ;KAAG,CAAC;AAC1D;GACF,KAAK;AACH,WAAO,KAAK;KAAE,MAAM;KAAO,OAAO;KAAK;KAAQ,QAAQ;KAAG,CAAC;AAC3D;GACF,KAAK;AACH,WAAO,KAAK;KAAE,MAAM;KAAO,OAAO;KAAK;KAAQ,QAAQ;KAAG,CAAC;AAC3D;;WAEK,MAAM,GACf,KAAI,cAAc,IAChB,QAAO,KAAK;GAAE,MAAM;GAAU,OAAO;GAAK;GAAQ,QAAQ;GAAG,CAAC;MAE9D,QAAO,KAAK;GAAE,MAAM;GAAU,OAAO;GAAK;GAAQ,QAAQ;GAAG,CAAC;MAGhE,QAAO,KAAK;GACV,MAAM;GACN,OAAO;GACP;GACA,QAAQ,UAAU;GACnB,CAAC;;CAKN,MAAM,YAA2C,EAAE;AACnD,MAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;EACxC,MAAM,KAAK,SAAS;AACpB,MAAI,OAAO,OAAO,OAAO,OAAQ,OAAO,IAAK;AAC7C,MAAI,CAAC,QAAQ,IAAI,EAAE,CACjB,WAAU,KAAK;GAAE;GAAI,KAAK;GAAG,CAAC;;AAIlC,KAAI,UAAU,SAAS,GAAG;EACxB,MAAM,QAAQ,CAAC,GAAG,IAAI,IAAI,UAAU,KAAK,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG;AAC/D,SAAO,KAAK;GACV,SAAS,4BAA4B,MAAM,kBAAkB,SAAS;GACtE,QAAQ,UAAU,GAAG;GACrB,QAAQ;GACT,CAAC;;AAGJ,QAAO;EAAE;EAAQ;EAAQ;;AAG3B,SAAS,2BAA2B,KAAqB;CACvD,IAAI,SAAS;CACb,IAAI,QAAQ;AAEZ,MAAK,MAAM,QAAQ,IACjB,KAAI,SAAS,KAAK;AAChB;AACA,YAAU;YACD,SAAS,KAAK;AACvB;AACA,YAAU;YACD,SAAS,OAAO,UAAU,EACnC,WAAU;KAEV,WAAU;AAId,QAAO;;AAOT,MAAM,uBAAuB;AAE7B,MAAM,uBAAuB,IAAI,IAAI;CAAC;CAAK;CAAK;CAAM;CAAK,CAAC;AAC5D,MAAM,iBAAiB,IAAI,IAAI;CAC7B;CACA;CACA;CACA;CACD,CAAC;AAEF,IAAM,oBAAN,MAAwB;CACtB,AAAQ,SAA0B,EAAE;CACpC,AAAQ,SAAS;CACjB,AAAQ,oBAA8B,EAAE;CACxC,AAAQ;CACR,AAAQ,MAAM;CACd,AAAQ;CACR,AAAQ,YAAY;CAEpB,YACE,QACA,aACA,MACA;AACA,OAAK,SAAS;AACd,OAAK,SAAS,CAAC,GAAG,YAAY;AAC9B,OAAK,OAAO;;CAGd,WAA2B;AACzB,MAAI,KAAK,OAAO,SAAS,EACvB,MAAK,iBAAiB;AAGxB,SAAO;GACL,QAAQ,KAAK;GACb,QAAQ,KAAK;GACb,mBAAmB,KAAK;GACzB;;CAGH,AAAQ,UAA6B;AACnC,SAAO,KAAK,OAAO,KAAK;;CAG1B,AAAQ,UAA6B;AACnC,SAAO,KAAK,OAAO,KAAK;;CAG1B,AAAQ,MAAM,MAA0B;AACtC,MAAI,KAAK,SAAS,EAAE,SAAS,MAAM;AACjC,QAAK,SAAS;AACd,UAAO;;AAET,SAAO;;CAGT,AAAQ,kBAAwB;AAC9B,OAAK,UAAU;;CAGjB,AAAQ,WAAiB;AACvB,OAAK,SAAS;AACd,SAAO,KAAK,SAAS,EAAE,SAAS,OAAO;AACrC,QAAK,SAAS;AACd,OAAI,CAAC,KAAK,SAAS,IAAI,KAAK,SAAS,EAAE,SAAS,OAAO;IACrD,MAAM,OAAO,KAAK,OAAO,KAAK,MAAM;AACpC,SAAK,OAAO,KAAK;KACf,SAAS;KACT,QAAQ,KAAK;KACb,QAAQ,KAAK;KACd,CAAC;AACF;;AAEF,QAAK,SAAS;;;CAIlB,AAAQ,UAAgB;AACtB,OAAK,UAAU;AACf,SAAO,KAAK,SAAS,EAAE,SAAS,MAAM;AACpC,QAAK,SAAS;AACd,OAAI,CAAC,KAAK,SAAS,IAAI,KAAK,SAAS,EAAE,SAAS,MAAM;IACpD,MAAM,OAAO,KAAK,OAAO,KAAK,MAAM;AACpC,SAAK,OAAO,KAAK;KACf,SAAS;KACT,QAAQ,KAAK;KACb,QAAQ,KAAK;KACd,CAAC;AACF;;AAEF,QAAK,UAAU;;;CAInB,AAAQ,WAAiB;AACvB,OAAK,YAAY;EACjB,IAAI,eAAe;AAEnB,SAAO,KAAK,SAAS,EAAE,SAAS,OAAO;AACrC,QAAK,SAAS;AACd;AACA,OAAI,eAAe,sBAAsB;IACvC,MAAM,OAAO,KAAK,OAAO,KAAK,MAAM;AACpC,SAAK,OAAO,KAAK;KACf,SAAS,kBAAkB,aAAa,qBAAqB,KAAK,IAAI,GAAG,eAAe,EAAE,CAAC;KAC3F,QAAQ,KAAK;KACb,QAAQ,KAAK;KACd,CAAC;;AAEJ,QAAK,YAAY;;;CAIrB,AAAQ,aAAmB;AACzB,MAAI,KAAK,MAAM,MAAM,EAAE;AACrB,OACE,CAAC,KAAK,SAAS,IACf,KAAK,SAAS,EAAE,SAAS,SACzB,KAAK,SAAS,EAAE,SAAS,QACzB,KAAK,SAAS,EAAE,SAAS,OACzB;IACA,MAAM,OAAO,KAAK,OAAO,KAAK,MAAM;AACpC,SAAK,OAAO,KAAK;KACf,SAAS;KACT,QAAQ,KAAK;KACb,QAAQ,KAAK;KACd,CAAC;AACF;;AAEF,QAAK,YAAY;AACjB;;AAEF,OAAK,cAAc;;CAGrB,AAAQ,eAAqB;AAC3B,MAAI,KAAK,MAAM,SAAS,EAAE;AACxB,QAAK,iBAAiB;AACtB,OAAI,CAAC,KAAK,MAAM,SAAS,CACvB,MAAK,OAAO,KAAK;IACf,SAAS;IACT,QAAQ,KAAK,OAAO,KAAK,MAAM,IAAI,UAAU;IAC7C,QAAQ;IACT,CAAC;AAEJ;;EAGF,MAAM,QAAQ,KAAK,SAAS;AAC5B,MAAI,OAAO,SAAS,SAAS;AAC3B,QAAK,SAAS;AACd,QAAK,mBAAmB,MAAM;AAC9B;;AAIF,MAAI,OAAO;AACT,QAAK,OAAO,KAAK;IACf,SAAS,qBAAqB,MAAM,MAAM;IAC1C,QAAQ,MAAM;IACd,QAAQ,MAAM;IACf,CAAC;AACF,QAAK,SAAS;;;CAIlB,AAAQ,mBAAmB,OAAoB;EAC7C,MAAM,QAAQ,MAAM;AAGpB,MAAI,UAAU,YAAa;AAG3B,MAAI,MAAM,WAAW,UAAU,EAAE;GAC/B,MAAM,YAAY,MAAM,MAAM,EAAE;AAEhC,OAAI,CADe,IAAI,IAAI;IAAC;IAAS;IAAU;IAAO;IAAS,CAAC,CAChD,IAAI,UAAU,CAC5B,MAAK,OAAO,KAAK;IACf,SAAS,uBAAuB,UAAU;IAC1C,QAAQ,MAAM;IACd,QAAQ,MAAM;IACf,CAAC;AAEJ;;AAIF,MAAI,MAAM,WAAW,UAAU,EAAE;AAC/B,QAAK,mBAAmB,OAAO,MAAM;AACrC;;AAIF,MAAI,MAAM,WAAW,aAAa,EAAE;AAClC,QAAK,sBAAsB,OAAO,MAAM;AACxC;;AAIF,MAAI,MAAM,WAAW,SAAS,EAAE;AAC9B,QAAK,6BAA6B,OAAO,GAAG,MAAM;AAClD;;AAIF,MAAI,MAAM,WAAW,WAAW,EAAE;AAChC,QAAK,oBAAoB,OAAO,MAAM;AACtC;;AAIF,MAAI,MAAM,WAAW,QAAQ,EAAE;AAC7B,QAAK,SAAS;AACd,OAAI,KAAK,WAAW;AAClB,SAAK,OAAO,KAAK;KACf,SAAS;KACT,QAAQ,MAAM;KACd,QAAQ,MAAM;KACf,CAAC;AACF;;GAEF,MAAM,gBAAgB,KAAK;AAC3B,QAAK,YAAY;AACjB,QAAK,6BAA6B,OAAO,GAAG,MAAM;AAClD,QAAK,YAAY;AACjB;;AAIF,MAAI,MAAM,WAAW,KAAK,EAAE;AAC1B,QAAK,uBAAuB,OAAO,MAAM;AACzC;;AAIF,MAAI,MAAM,WAAW,IAAI,IAAI,2BAA2B,KAAK,MAAM,EAAE;AACnE,QAAK,kBAAkB,KAAK,MAAM;AAClC;;AAIF,MAAI,MAAM,WAAW,IAAI,EAAE;AACzB,QAAK,oBAAoB,OAAO,MAAM;AACtC;;AAIF,MAAI,MAAM,WAAW,IAAI,CAAE;AAG3B,MAAI,MAAM,WAAW,IAAI,EAAE;AACzB,QAAK,0BAA0B,OAAO,MAAM;AAC5C;;AAIF,MAAI,MAAM,SAAS,IAAI,EAAE;AACvB,QAAK,sBAAsB,OAAO,MAAM;AACxC;;AAIF,MAAI,qBAAqB,KAAK,MAAM,CAAE;AAEtC,OAAK,OAAO,KAAK;GACf,SAAS,6BAA6B,MAAM;GAC5C,QAAQ,MAAM;GACd,QAAQ,MAAM;GACf,CAAC;;CAGJ,AAAQ,mBAAmB,KAAa,OAAoB;EAC1D,MAAM,UAAU,IAAI,MAAM,GAAG,GAAG;AAChC,MAAI,CAAC,QAAQ,MAAM,EAAE;AACnB,QAAK,OAAO,KAAK;IACf,SAAS;IACT,QAAQ,MAAM;IACd,QAAQ,MAAM;IACf,CAAC;AACF;;EAIF,MAAM,WAAW,0BAA0B,QAAQ,MAAM,CAAC;AAG1D,MACE,SAAS,SAAS,IAAI,IACtB,CAAC,SAAS,SAAS,IAAI,IACvB,CAAC,SAAS,SAAS,IAAI,IACvB,CAAC,SAAS,SAAS,IAAI,CAEvB;AAIF,MACE,CAAC,SAAS,SAAS,IAAI,IACvB,CAAC,SAAS,SAAS,IAAI,IACvB,CAAC,SAAS,SAAS,IAAI,CAEvB;AAIF,OAAK,2BAA2B,UAAU,MAAM;;CAGlD,AAAQ,2BAA2B,WAAmB,OAAoB;EAExE,MAAM,aAAa,UAAU,MAC3B,2CACD;AACD,MAAI,YAAY;GACd,MAAM,MAAM,WAAW;AACvB,OAAI,CAAC,eAAe,IAAI,IAAI,IAAI,CAAC,qBAAqB,IAAI,IAAI,CAC5D,MAAK,OAAO,KAAK;IACf,SAAS,sBAAsB,IAAI;IACnC,QAAQ,MAAM;IACd,QAAQ,MAAM;IACf,CAAC;AAEJ;;EAIF,MAAM,cAAc,UAAU,MAAM,iCAAiC;AACrE,MAAI,aAAa;GACf,MAAM,MAAM,YAAY;AACxB,OAAI,eAAe,IAAI,IAAI,IAAI,qBAAqB,IAAI,IAAI,CAC1D;;EAKJ,MAAM,gBAAgB,UAAU,MAAM,kCAAkC;AACxE,MAAI,eAAe;GACjB,MAAM,MAAM,cAAc;AAC1B,OAAI,eAAe,IAAI,IAAI,IAAI,qBAAqB,IAAI,IAAI,CAC1D;;;CAKN,AAAQ,sBAAsB,KAAa,OAAoB;AAE7D,MAAI,CADY,IAAI,MAAM,IAAI,GAAG,CACpB,MAAM,CACjB,MAAK,OAAO,KAAK;GACf,SAAS;GACT,QAAQ,MAAM;GACd,QAAQ,MAAM;GACf,CAAC;;CAIN,AAAQ,6BACN,KACA,WACA,QACM;EACN,MAAM,UAAU,IAAI,MAAM,WAAW,GAAG;AACxC,MAAI,CAAC,QAAQ,MAAM,CAAE;EAErB,MAAM,cAAc,cAAc,SAAS,KAAK,KAAK;AACrD,OAAK,OAAO,KAAK,GAAG,YAAY,OAAO;AACvC,MAAI,YAAY,OAAQ,MAAK,SAAS;AACtC,OAAK,kBAAkB,KAAK,GAAG,YAAY,kBAAkB;;CAG/D,AAAQ,oBAAoB,KAAa,OAAoB;EAC3D,MAAM,UAAU,IAAI,MAAM,GAAG,GAAG;AAChC,MAAI,CAAC,QAAQ,MAAM,EAAE;AACnB,QAAK,OAAO,KAAK;IACf,SAAS;IACT,QAAQ,MAAM;IACd,QAAQ,MAAM;IACf,CAAC;AACF;;EAGF,IAAI,YAAY,QAAQ,MAAM;EAG9B,MAAM,eAAe,UAAU,YAAY,IAAI;AAC/C,MAAI,iBAAiB,IAEnB;OADmB,UAAU,MAAM,eAAe,EAAE,CAAC,MAAM,KACxC,IACjB,aAAY,UAAU,MAAM,GAAG,aAAa,CAAC,MAAM;;EAIvD,MAAM,cAAc,cAAc,WAAW,KAAK,KAAK;AACvD,OAAK,OAAO,KAAK,GAAG,YAAY,OAAO;AACvC,OAAK,kBAAkB,KAAK,GAAG,YAAY,kBAAkB;;CAG/D,AAAQ,uBAAuB,KAAa,OAAoB;EAC9D,MAAM,UAAU,IAAI,MAAM,GAAG,GAAG;AAChC,MAAI,CAAC,QAAQ,MAAM,EAAE;AACnB,QAAK,OAAO,KAAK;IACf,SAAS;IACT,QAAQ,MAAM;IACd,QAAQ,MAAM;IACf,CAAC;AACF;;EAIF,MAAM,WAAW,kBAAkB,QAAQ;EAC3C,IAAI;AAEJ,MAAI,aAAa,GACf,aAAY,QAAQ,MAAM,WAAW,EAAE,CAAC,MAAM;MAE9C,aAAY,QAAQ,MAAM;AAI5B,MAAI,UAAU,WAAW,IAAI,CAAE;AAG/B,MAAI,uBAAuB,KAAK,UAAU,CAAE;EAG5C,MAAM,WAAW,0BAA0B,UAAU;AACrD,MACE,SAAS,SAAS,IAAI,IACtB,SAAS,SAAS,IAAI,IACtB,SAAS,SAAS,IAAI,CAEtB,MAAK,2BAA2B,UAAU,MAAM;;CAIpD,AAAQ,oBAAoB,OAAe,OAAoB;AAG7D,MADsB,yBAAyB,KAAK,MAAM,CACvC;EAGnB,MAAM,YAAY,gBAAgB,KAAK,MAAM;AAC7C,MAAI,WAAW;GACb,MAAM,WAAW,UAAU;AAC3B,OAAI,CAAC,qBAAqB,IAAI,SAAS,CACrC,MAAK,OAAO,KAAK;IACf,SAAS,yBAAyB,SAAS;IAC3C,QAAQ,MAAM;IACd,QAAQ,MAAM;IACf,CAAC;AAEJ;;AAIF,MAAI,CAAC,qBAAqB,IAAI,MAAM,CAClC,MAAK,OAAO,KAAK;GACf,SAAS,yBAAyB,MAAM;GACxC,QAAQ,MAAM;GACd,QAAQ,MAAM;GACf,CAAC;;CAIN,AAAQ,0BAA0B,OAAe,OAAoB;AACnE,MAAI,CAAC,MAAM,WAAW,IAAI,IAAI,CAAC,MAAM,SAAS,IAAI,CAChD,MAAK,OAAO,KAAK;GACf,SAAS,iCAAiC,MAAM;GAChD,QAAQ,MAAM;GACd,QAAQ,MAAM;GACf,CAAC;;CAIN,AAAQ,sBAAsB,OAAe,OAAoB;AAE/D,MAAI,CADY,MAAM,MAAM,0CAA0C,CAEpE,MAAK,OAAO,KAAK;GACf,SAAS,kCAAkC,MAAM;GACjD,QAAQ,MAAM;GACd,QAAQ,MAAM;GACf,CAAC;;;AASR,SAAS,0BAA0B,WAA2B;AAC5D,QAAO,UACJ,QAAQ,UAAU,QAAQ,CAC1B,QAAQ,UAAU,SAAS,CAC3B,QAAQ,WAAW,cAAc,CACjC,QAAQ,WAAW,aAAa;;AAGrC,SAAS,kBAAkB,KAAqB;CAC9C,IAAI,QAAQ;AACZ,MAAK,IAAI,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;EACnC,MAAM,KAAK,IAAI;AACf,MAAI,OAAO,IAAK;WACP,OAAO,IAAK,SAAQ,KAAK,IAAI,GAAG,QAAQ,EAAE;WAC1C,OAAO,OAAO,UAAU,EAAG,QAAO;;AAE7C,QAAO;;;;;;;AAYT,SAAgB,sBAAsB,UAAgC;AACpE,KAAI,CAAC,SAAS,SAAS,QAAQ,CAAE,QAAO,EAAE;AAC1C,KAAI,oBAAoB,SAAS,CAAE,QAAO,EAAE;AAC5C,KAAI,cAAc,SAAS,CAAC,OAAO,SAAS,EAAG,QAAO,EAAE;CAExD,MAAM,EAAE,WAAW,SAAS,SAAS;AACrC,QAAO,OACJ,QACE,UAAU,MAAM,SAAS,WAAW,MAAM,MAAM,WAAW,QAAQ,CACrE,CACA,SAAS,UAAU;AAClB,MAAI,oBAAoB,MAAM,MAAM,CAAE,QAAO,EAAE;EAC/C,MAAM,QAAQ,MAAM,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM;AAC7C,MAAI,CAAC,MAAO,QAAO,EAAE;EAErB,MAAM,EAAE,QAAQ,aAAa,WAAW,SAAS,MAAM;EACvD,MAAM,eACJ,OAAO,WAAW,KAClB,YAAY,WAAW,KACvB,YAAY,GAAG,SAAS,WACxB,YAAY,GAAG,UAAU,SACzB,MAAM,WAAW,IAAI,IACrB,cAAc,MAAM,CAAC,OAAO,WAAW,KACvC,CAAC,MAAM,WAAW,QAAQ,IAE1B,CAAC,MAAM,WAAW,OAAO,IACzB,CAAC,MAAM,WAAW,UAAU,IAG5B,EAAE,MAAM,WAAW,QAAQ,IAAI,cAAc,KAAK,MAAM;AAE1D,SAAO,CACL;GACE,OAAO,MAAM;GACb,KAAK,MAAM,SAAS,MAAM;GAC1B,aAAa,eAAe,IAAI,UAAU,QAAQ,MAAM;GACzD,CACF;GACD;;;;;AAMN,SAAgB,cACd,UACA,OAA8B,EAAE,EAChB;AAChB,KAAI,CAAC,YAAY,CAAC,SAAS,MAAM,CAC/B,QAAO;EAAE,QAAQ,EAAE;EAAE,QAAQ;EAAO,mBAAmB,EAAE;EAAE;CAG7D,MAAM,UAAU,SAAS,MAAM;AAI/B,KAAI,YAAY,IACd,QAAO;EAAE,QAAQ,EAAE;EAAE,QAAQ;EAAO,mBAAmB,EAAE;EAAE;AAM7D,KAAI,qBAAqB,QAAQ,CAC/B,QAAO;EACL,QAAQ,CACN;GACE,SACE;GAEF,QAAQ;GACR,QAAQ,SAAS;GAClB,CACF;EACD,QAAQ;EACR,mBAAmB,EAAE;EACtB;CAGH,MAAM,EAAE,QAAQ,QAAQ,gBAAgB,SAAS,QAAQ;AAEzD,QADkB,IAAI,kBAAkB,QAAQ,aAAa,KAAK,CACjD,UAAU;;;;;;;;AAS7B,SAAS,qBAAqB,UAA2B;AACvD,KAAI,aAAa,OAAO,CAAC,SAAS,SAAS,IAAI,CAAE,QAAO;AACxD,QAAO,kCAAkC,KAAK,SAAS;;;;;;;AAQzD,SAAgB,wBACd,OACA,OAA8B,EAAE,EAChB;AAChB,QAAO,cAAc,OAAO,KAAK"}
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\nexport interface StateAtom {\n /** The atom as written: `hovered`, `:hover`, `[open]`, `@root(...)`. */\n value: string;\n /** Whether the atom sits inside `@own(...)`. */\n own: boolean;\n}\n\n/**\n * The state atoms of a key, without its operators and grouping. The contents\n * of `@own(...)` are listed atom by atom; `@root()`, `@parent()`, and at-rule\n * conditions stay single atoms, since they describe other things.\n */\nexport function getStateAtoms(stateKey: string): StateAtom[] {\n const atoms: StateAtom[] = [];\n for (const token of tokenize(stateKey.trim()).tokens) {\n if (token.type !== 'STATE') continue;\n if (!token.value.startsWith('@own(')) {\n atoms.push({ value: token.value, own: false });\n continue;\n }\n for (const inner of tokenize(token.value.slice(5, -1)).tokens) {\n if (inner.type === 'STATE') atoms.push({ value: inner.value, own: true });\n }\n }\n return atoms;\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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"}
@@ -0,0 +1,85 @@
1
+ import selectorParser from "postcss-selector-parser";
2
+
3
+ //#region src/pseudo-elements.ts
4
+ /**
5
+ * Tasty's pseudo-element rules (tenphi/tasty#406). When a selector ends in a
6
+ * pseudo-element, the pseudo-element is the subject: its own states go after
7
+ * it, and a state testing what a pseudo-element has none of is dropped with a
8
+ * PSEUDO_ELEMENT_STATE_IGNORED warning. Like Tasty, these helpers use the CSS
9
+ * language alone, never what one browser supports.
10
+ */
11
+ const LEGACY_PSEUDO_ELEMENTS = new Set([
12
+ ":before",
13
+ ":after",
14
+ ":first-line",
15
+ ":first-letter"
16
+ ]);
17
+ const QUOTED_RE = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g;
18
+ const TREE_PSEUDO_RE = /:(?:root|empty|scope|has|(?:first|last|only)-(?:child|of-type)|nth-(?:last-)?(?:child|of-type))(?![\w-])/i;
19
+ /** Lowercase pseudo name with CSS escapes decoded (`:b\65 fore` → `:before`). */
20
+ function pseudoName(node) {
21
+ const name = node.value.toLowerCase();
22
+ if (!name.includes("\\")) return name;
23
+ const colons = name.startsWith("::") ? "::" : ":";
24
+ const identifier = selectorParser().astSync(name.slice(colons.length)).first?.first;
25
+ return identifier?.type === "tag" ? colons + identifier.value.toLowerCase() : name;
26
+ }
27
+ /** `::name`, or a legacy single-colon spelling such as `:before`. */
28
+ function isPseudoElement(node) {
29
+ if (node.type !== "pseudo") return false;
30
+ const name = pseudoName(node);
31
+ return name.startsWith("::") || LEGACY_PSEUDO_ELEMENTS.has(name);
32
+ }
33
+ /**
34
+ * Where a selector item ends in a pseudo-element: the first top-level
35
+ * pseudo-element in its last compound. Nodes before it select the
36
+ * originating element; pseudo-classes after the last chained one are the
37
+ * pseudo-element's own states. `null` when the item ends in an element.
38
+ */
39
+ function splitAtPseudoElement(item) {
40
+ const { nodes } = item;
41
+ let compound = 0;
42
+ nodes.forEach((node, index) => {
43
+ if (node.type === "combinator") compound = index + 1;
44
+ });
45
+ const first = nodes.findIndex((node, index) => index >= compound && isPseudoElement(node));
46
+ if (first === -1) return null;
47
+ let last = first;
48
+ nodes.forEach((node, index) => {
49
+ if (index > first && isPseudoElement(node)) last = index;
50
+ });
51
+ return {
52
+ compound,
53
+ first,
54
+ last
55
+ };
56
+ }
57
+ /**
58
+ * The items of a selector list that end in a pseudo-element, as written.
59
+ * `null` when the selector cannot be parsed.
60
+ */
61
+ function pseudoElementItems(selector) {
62
+ let root;
63
+ try {
64
+ root = selectorParser().astSync(selector);
65
+ } catch {
66
+ return null;
67
+ }
68
+ return root.nodes.filter((item) => splitAtPseudoElement(item) !== null).map((item) => item.toString().trim());
69
+ }
70
+ /**
71
+ * What a selector state (`:first-child`, `[open]`, `.active`, `:not([x])`)
72
+ * tests that a pseudo-element has none of, in the words of Tasty's warning,
73
+ * or `null` when a pseudo-element can be in that state.
74
+ */
75
+ function pseudoElementMismatch(selector) {
76
+ const text = selector.replace(QUOTED_RE, "\"\"");
77
+ if (TREE_PSEUDO_RE.test(text)) return "position in the DOM tree";
78
+ if (text.includes("[")) return "attributes";
79
+ if (text.includes(".") || text.includes("#")) return "classes or ids";
80
+ return null;
81
+ }
82
+
83
+ //#endregion
84
+ export { isPseudoElement, pseudoElementItems, pseudoElementMismatch, pseudoName, splitAtPseudoElement };
85
+ //# sourceMappingURL=pseudo-elements.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pseudo-elements.js","names":[],"sources":["../src/pseudo-elements.ts"],"sourcesContent":["import selectorParser from 'postcss-selector-parser';\n\n/**\n * Tasty's pseudo-element rules (tenphi/tasty#406). When a selector ends in a\n * pseudo-element, the pseudo-element is the subject: its own states go after\n * it, and a state testing what a pseudo-element has none of is dropped with a\n * PSEUDO_ELEMENT_STATE_IGNORED warning. Like Tasty, these helpers use the CSS\n * language alone, never what one browser supports.\n */\n\nconst LEGACY_PSEUDO_ELEMENTS = new Set([\n ':before',\n ':after',\n ':first-line',\n ':first-letter',\n]);\n\nconst QUOTED_RE = /\"(?:[^\"\\\\]|\\\\.)*\"|'(?:[^'\\\\]|\\\\.)*'/g;\nconst TREE_PSEUDO_RE =\n /:(?:root|empty|scope|has|(?:first|last|only)-(?:child|of-type)|nth-(?:last-)?(?:child|of-type))(?![\\w-])/i;\n\n/** Lowercase pseudo name with CSS escapes decoded (`:b\\65 fore` → `:before`). */\nexport function pseudoName(node: selectorParser.Pseudo): string {\n const name = node.value.toLowerCase();\n if (!name.includes('\\\\')) return name;\n // The parser decodes tag identifiers but preserves pseudo spelling.\n const colons = name.startsWith('::') ? '::' : ':';\n const identifier = selectorParser().astSync(name.slice(colons.length)).first\n ?.first;\n return identifier?.type === 'tag'\n ? colons + identifier.value.toLowerCase()\n : name;\n}\n\n/** `::name`, or a legacy single-colon spelling such as `:before`. */\nexport function isPseudoElement(node: selectorParser.Node): boolean {\n if (node.type !== 'pseudo') return false;\n const name = pseudoName(node);\n return name.startsWith('::') || LEGACY_PSEUDO_ELEMENTS.has(name);\n}\n\nexport interface PseudoElementSplit {\n /** Index of the first node of the last compound. */\n compound: number;\n /** Index of the pseudo-element the item ends in. */\n first: number;\n /** Index of the last pseudo-element chained after it (`::after::marker`). */\n last: number;\n}\n\n/**\n * Where a selector item ends in a pseudo-element: the first top-level\n * pseudo-element in its last compound. Nodes before it select the\n * originating element; pseudo-classes after the last chained one are the\n * pseudo-element's own states. `null` when the item ends in an element.\n */\nexport function splitAtPseudoElement(\n item: selectorParser.Selector,\n): PseudoElementSplit | null {\n const { nodes } = item;\n let compound = 0;\n nodes.forEach((node, index) => {\n if (node.type === 'combinator') compound = index + 1;\n });\n const first = nodes.findIndex(\n (node, index) => index >= compound && isPseudoElement(node),\n );\n if (first === -1) return null;\n let last = first;\n nodes.forEach((node, index) => {\n if (index > first && isPseudoElement(node)) last = index;\n });\n return { compound, first, last };\n}\n\n/**\n * The items of a selector list that end in a pseudo-element, as written.\n * `null` when the selector cannot be parsed.\n */\nexport function pseudoElementItems(selector: string): string[] | null {\n let root: selectorParser.Root;\n try {\n root = selectorParser().astSync(selector);\n } catch {\n return null;\n }\n return root.nodes\n .filter((item) => splitAtPseudoElement(item) !== null)\n .map((item) => item.toString().trim());\n}\n\n/**\n * What a selector state (`:first-child`, `[open]`, `.active`, `:not([x])`)\n * tests that a pseudo-element has none of, in the words of Tasty's warning,\n * or `null` when a pseudo-element can be in that state.\n */\nexport function pseudoElementMismatch(selector: string): string | null {\n const text = selector.replace(QUOTED_RE, '\"\"');\n if (TREE_PSEUDO_RE.test(text)) return 'position in the DOM tree';\n if (text.includes('[')) return 'attributes';\n if (text.includes('.') || text.includes('#')) return 'classes or ids';\n return null;\n}\n"],"mappings":";;;;;;;;;;AAUA,MAAM,yBAAyB,IAAI,IAAI;CACrC;CACA;CACA;CACA;CACD,CAAC;AAEF,MAAM,YAAY;AAClB,MAAM,iBACJ;;AAGF,SAAgB,WAAW,MAAqC;CAC9D,MAAM,OAAO,KAAK,MAAM,aAAa;AACrC,KAAI,CAAC,KAAK,SAAS,KAAK,CAAE,QAAO;CAEjC,MAAM,SAAS,KAAK,WAAW,KAAK,GAAG,OAAO;CAC9C,MAAM,aAAa,gBAAgB,CAAC,QAAQ,KAAK,MAAM,OAAO,OAAO,CAAC,CAAC,OACnE;AACJ,QAAO,YAAY,SAAS,QACxB,SAAS,WAAW,MAAM,aAAa,GACvC;;;AAIN,SAAgB,gBAAgB,MAAoC;AAClE,KAAI,KAAK,SAAS,SAAU,QAAO;CACnC,MAAM,OAAO,WAAW,KAAK;AAC7B,QAAO,KAAK,WAAW,KAAK,IAAI,uBAAuB,IAAI,KAAK;;;;;;;;AAkBlE,SAAgB,qBACd,MAC2B;CAC3B,MAAM,EAAE,UAAU;CAClB,IAAI,WAAW;AACf,OAAM,SAAS,MAAM,UAAU;AAC7B,MAAI,KAAK,SAAS,aAAc,YAAW,QAAQ;GACnD;CACF,MAAM,QAAQ,MAAM,WACjB,MAAM,UAAU,SAAS,YAAY,gBAAgB,KAAK,CAC5D;AACD,KAAI,UAAU,GAAI,QAAO;CACzB,IAAI,OAAO;AACX,OAAM,SAAS,MAAM,UAAU;AAC7B,MAAI,QAAQ,SAAS,gBAAgB,KAAK,CAAE,QAAO;GACnD;AACF,QAAO;EAAE;EAAU;EAAO;EAAM;;;;;;AAOlC,SAAgB,mBAAmB,UAAmC;CACpE,IAAI;AACJ,KAAI;AACF,SAAO,gBAAgB,CAAC,QAAQ,SAAS;SACnC;AACN,SAAO;;AAET,QAAO,KAAK,MACT,QAAQ,SAAS,qBAAqB,KAAK,KAAK,KAAK,CACrD,KAAK,SAAS,KAAK,UAAU,CAAC,MAAM,CAAC;;;;;;;AAQ1C,SAAgB,sBAAsB,UAAiC;CACrE,MAAM,OAAO,SAAS,QAAQ,WAAW,OAAK;AAC9C,KAAI,eAAe,KAAK,KAAK,CAAE,QAAO;AACtC,KAAI,KAAK,SAAS,IAAI,CAAE,QAAO;AAC/B,KAAI,KAAK,SAAS,IAAI,IAAI,KAAK,SAAS,IAAI,CAAE,QAAO;AACrD,QAAO"}
@@ -0,0 +1,155 @@
1
+ import { createRule } from "../create-rule.js";
2
+ import { getKeyName, getStringValue, unwrapExpression } from "../utils.js";
3
+ import { TastyContext, styleObjectListeners } from "../context.js";
4
+ import { getStateAtoms, parseStateKey } from "../parsers/state-key-parser.js";
5
+ import { pseudoElementItems, pseudoElementMismatch } from "../pseudo-elements.js";
6
+ import selectorParser from "postcss-selector-parser";
7
+
8
+ //#region src/rules/no-ignored-pseudo-element-state.ts
9
+ /**
10
+ * What a state atom tests that a pseudo-element has none of, or `null`.
11
+ * `@root()`, `@parent()`, and at-rule conditions describe other things, and
12
+ * `@alias` definitions are not known here.
13
+ */
14
+ function atomMismatch(atom) {
15
+ if (atom.startsWith("@")) return null;
16
+ if (/^[:.[]/.test(atom)) return pseudoElementMismatch(atom);
17
+ return "modifiers";
18
+ }
19
+ /** The first item of `selectors` that ends in a pseudo-element. */
20
+ function firstPseudoElementItem(selectors) {
21
+ for (const selector of selectors ?? []) {
22
+ const [item] = pseudoElementItems(selector) ?? [];
23
+ if (item) return item;
24
+ }
25
+ return null;
26
+ }
27
+ /** The items of a selector list, or `null` when it cannot be parsed. */
28
+ function selectorItems(selector) {
29
+ try {
30
+ return selectorParser().astSync(selector).nodes.map((item) => item.toString().trim());
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+ var no_ignored_pseudo_element_state_default = createRule({
36
+ name: "no-ignored-pseudo-element-state",
37
+ meta: {
38
+ type: "problem",
39
+ docs: { description: "Report states that Tasty ignores because they test a pseudo-element for attributes, modifiers, classes, or tree position" },
40
+ messages: { ignoredState: "Tasty ignores state '{{state}}' on '{{selector}}': a pseudo-element has no {{mismatch}}. To condition on the element the pseudo-element belongs to, write the condition in the selector before the pseudo-element." },
41
+ schema: []
42
+ },
43
+ defaultOptions: [],
44
+ create(context) {
45
+ const ctx = new TastyContext(context);
46
+ const visited = /* @__PURE__ */ new WeakSet();
47
+ /** The key and object of the property whose value is `node`. */
48
+ function ownerEntry(node) {
49
+ let wrapped = node;
50
+ while (wrapped.parent && unwrapExpression(wrapped.parent) === node) wrapped = wrapped.parent;
51
+ const prop = wrapped.parent;
52
+ if (prop?.type !== "Property" || prop.computed) return null;
53
+ if (prop.parent.type !== "ObjectExpression") return null;
54
+ const key = getKeyName(prop.key);
55
+ return key === null ? null : {
56
+ key,
57
+ object: prop.parent
58
+ };
59
+ }
60
+ /** The literal selector of a selector-mode call, such as useGlobalStyles. */
61
+ function baseSelector(node) {
62
+ if (!ctx.getStyleContext(node)?.isSelectorMode) return null;
63
+ const call = node.parent;
64
+ if (call?.type !== "CallExpression" || call.arguments[1] !== node) return null;
65
+ return getStringValue(unwrapExpression(call.arguments[0]));
66
+ }
67
+ function checkStateMap(map, own, root) {
68
+ for (const prop of map.properties) {
69
+ if (prop.type !== "Property") continue;
70
+ const state = prop.computed ? getStringValue(unwrapExpression(prop.key)) : getKeyName(prop.key);
71
+ if (state === null || state.trim() === "" || state.trim() === "_") continue;
72
+ if (parseStateKey(state).errors.length > 0) continue;
73
+ for (const atom of getStateAtoms(state)) {
74
+ const selector = atom.own ? own ?? root : root;
75
+ if (selector === null) continue;
76
+ const mismatch = atomMismatch(atom.value);
77
+ if (mismatch === null) continue;
78
+ context.report({
79
+ node: prop.key,
80
+ messageId: "ignoredState",
81
+ data: {
82
+ state,
83
+ selector,
84
+ mismatch
85
+ }
86
+ });
87
+ break;
88
+ }
89
+ }
90
+ }
91
+ /**
92
+ * `suffixes` are the selectors this object's styles apply to, relative
93
+ * to the root (`&`); `root` is the base selector item of a selector-mode
94
+ * call that ends in a pseudo-element, if there is one.
95
+ */
96
+ function checkStyleObject(node, suffixes, root) {
97
+ visited.add(node);
98
+ const own = firstPseudoElementItem(suffixes);
99
+ for (const prop of node.properties) {
100
+ if (prop.type !== "Property" || prop.computed) continue;
101
+ const key = getKeyName(prop.key);
102
+ const value = unwrapExpression(prop.value);
103
+ if (key === null || value.type !== "ObjectExpression") continue;
104
+ if (ctx.isSubElementKey(key)) checkStyleObject(value, subElementSuffixes(value, key, suffixes), root);
105
+ else if (ctx.isNestedSelectorKey(key)) checkStyleObject(value, extend(suffixes, key, true), root);
106
+ else if (!ctx.isSpecialKey(key)) checkStateMap(value, own, root);
107
+ }
108
+ }
109
+ function subElementSuffixes(node, name, parents) {
110
+ for (const prop of node.properties) {
111
+ if (prop.type !== "Property") continue;
112
+ if ((prop.computed ? getStringValue(unwrapExpression(prop.key)) : getKeyName(prop.key)) !== "$") continue;
113
+ const selector = getStringValue(unwrapExpression(prop.value));
114
+ return selector === null ? null : extend(parents, selector, false);
115
+ }
116
+ return [name];
117
+ }
118
+ /**
119
+ * Selectors relative to `parents`: `&` attaches to the owner, so
120
+ * `&::before` under `a` is `a::before`. Other items of a nested `&` key
121
+ * are descendants of the owner; other `$` items stand on their own.
122
+ */
123
+ function extend(parents, selector, nested) {
124
+ const items = selectorItems(selector);
125
+ if (items === null) return null;
126
+ const result = [];
127
+ for (const item of items) {
128
+ if (!item.startsWith("&") && !nested) {
129
+ result.push(item);
130
+ continue;
131
+ }
132
+ if (parents === null) return null;
133
+ for (const parent of parents) result.push(item.startsWith("&") ? parent + item.slice(1) : `${parent} ${item}`);
134
+ }
135
+ return result;
136
+ }
137
+ function handleStyleObject(node) {
138
+ if (visited.has(node) || !ctx.isStyleObject(node)) return;
139
+ const owner = ownerEntry(node);
140
+ if (owner && (ctx.isSubElementKey(owner.key) || ctx.isNestedSelectorKey(owner.key)) && ctx.isStyleObject(owner.object)) return;
141
+ const base = baseSelector(node);
142
+ checkStyleObject(node, ["&"], base === null ? null : firstPseudoElementItem([base]));
143
+ }
144
+ return {
145
+ ImportDeclaration(node) {
146
+ ctx.trackImport(node);
147
+ },
148
+ ...styleObjectListeners(handleStyleObject)
149
+ };
150
+ }
151
+ });
152
+
153
+ //#endregion
154
+ export { no_ignored_pseudo_element_state_default as default };
155
+ //# sourceMappingURL=no-ignored-pseudo-element-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-ignored-pseudo-element-state.js","names":[],"sources":["../../src/rules/no-ignored-pseudo-element-state.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport selectorParser from 'postcss-selector-parser';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getStateAtoms, parseStateKey } from '../parsers/state-key-parser.js';\nimport {\n pseudoElementItems,\n pseudoElementMismatch,\n} from '../pseudo-elements.js';\nimport { getKeyName, getStringValue, unwrapExpression } from '../utils.js';\n\n/**\n * What a state atom tests that a pseudo-element has none of, or `null`.\n * `@root()`, `@parent()`, and at-rule conditions describe other things, and\n * `@alias` definitions are not known here.\n */\nfunction atomMismatch(atom: string): string | null {\n if (atom.startsWith('@')) return null;\n if (/^[:.[]/.test(atom)) return pseudoElementMismatch(atom);\n // Boolean and value modifiers (`hovered`, `theme=dark`) are attributes.\n return 'modifiers';\n}\n\n/** The first item of `selectors` that ends in a pseudo-element. */\nfunction firstPseudoElementItem(selectors: string[] | null): string | null {\n for (const selector of selectors ?? []) {\n const [item] = pseudoElementItems(selector) ?? [];\n if (item) return item;\n }\n return null;\n}\n\n/** The items of a selector list, or `null` when it cannot be parsed. */\nfunction selectorItems(selector: string): string[] | null {\n try {\n return selectorParser()\n .astSync(selector)\n .nodes.map((item) => item.toString().trim());\n } catch {\n return null;\n }\n}\n\nexport default createRule<[], 'ignoredState'>({\n name: 'no-ignored-pseudo-element-state',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Report states that Tasty ignores because they test a pseudo-element for attributes, modifiers, classes, or tree position',\n },\n messages: {\n ignoredState:\n \"Tasty ignores state '{{state}}' on '{{selector}}': a pseudo-element has no {{mismatch}}. To condition on the element the pseudo-element belongs to, write the condition in the selector before the pseudo-element.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n const visited = new WeakSet<TSESTree.ObjectExpression>();\n\n /** The key and object of the property whose value is `node`. */\n function ownerEntry(\n node: TSESTree.ObjectExpression,\n ): { key: string; object: TSESTree.ObjectExpression } | null {\n let wrapped: TSESTree.Node = node;\n while (wrapped.parent && unwrapExpression(wrapped.parent) === node) {\n wrapped = wrapped.parent;\n }\n const prop = wrapped.parent;\n if (prop?.type !== 'Property' || prop.computed) return null;\n if (prop.parent.type !== 'ObjectExpression') return null;\n const key = getKeyName(prop.key);\n return key === null ? null : { key, object: prop.parent };\n }\n\n /** The literal selector of a selector-mode call, such as useGlobalStyles. */\n function baseSelector(node: TSESTree.ObjectExpression): string | null {\n if (!ctx.getStyleContext(node)?.isSelectorMode) return null;\n const call = node.parent;\n if (call?.type !== 'CallExpression' || call.arguments[1] !== node) {\n return null;\n }\n return getStringValue(unwrapExpression(call.arguments[0]));\n }\n\n function checkStateMap(\n map: TSESTree.ObjectExpression,\n own: string | null,\n root: string | null,\n ) {\n for (const prop of map.properties) {\n if (prop.type !== 'Property') continue;\n const state = prop.computed\n ? getStringValue(unwrapExpression(prop.key))\n : getKeyName(prop.key);\n if (state === null || state.trim() === '' || state.trim() === '_') {\n continue;\n }\n // Malformed keys are valid-state-key's to report.\n if (parseStateKey(state).errors.length > 0) continue;\n\n for (const atom of getStateAtoms(state)) {\n // Root states describe the root; @own() states the selected element.\n const selector = atom.own ? (own ?? root) : root;\n if (selector === null) continue;\n const mismatch = atomMismatch(atom.value);\n if (mismatch === null) continue;\n context.report({\n node: prop.key,\n messageId: 'ignoredState',\n data: { state, selector, mismatch },\n });\n break;\n }\n }\n }\n\n /**\n * `suffixes` are the selectors this object's styles apply to, relative\n * to the root (`&`); `root` is the base selector item of a selector-mode\n * call that ends in a pseudo-element, if there is one.\n */\n function checkStyleObject(\n node: TSESTree.ObjectExpression,\n suffixes: string[] | null,\n root: string | null,\n ) {\n visited.add(node);\n const own = firstPseudoElementItem(suffixes);\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n const key = getKeyName(prop.key);\n const value = unwrapExpression(prop.value);\n if (key === null || value.type !== 'ObjectExpression') continue;\n\n if (ctx.isSubElementKey(key)) {\n checkStyleObject(\n value,\n subElementSuffixes(value, key, suffixes),\n root,\n );\n } else if (ctx.isNestedSelectorKey(key)) {\n checkStyleObject(value, extend(suffixes, key, true), root);\n } else if (!ctx.isSpecialKey(key)) {\n checkStateMap(value, own, root);\n }\n }\n }\n\n function subElementSuffixes(\n node: TSESTree.ObjectExpression,\n name: string,\n parents: string[] | null,\n ): string[] | null {\n for (const prop of node.properties) {\n if (prop.type !== 'Property') continue;\n const key = prop.computed\n ? getStringValue(unwrapExpression(prop.key))\n : getKeyName(prop.key);\n if (key !== '$') continue;\n const selector = getStringValue(unwrapExpression(prop.value));\n return selector === null ? null : extend(parents, selector, false);\n }\n // Without `$`, the sub-element is its own [data-element] element.\n return [name];\n }\n\n /**\n * Selectors relative to `parents`: `&` attaches to the owner, so\n * `&::before` under `a` is `a::before`. Other items of a nested `&` key\n * are descendants of the owner; other `$` items stand on their own.\n */\n function extend(\n parents: string[] | null,\n selector: string,\n nested: boolean,\n ): string[] | null {\n const items = selectorItems(selector);\n if (items === null) return null;\n const result: string[] = [];\n for (const item of items) {\n if (!item.startsWith('&') && !nested) {\n result.push(item);\n continue;\n }\n if (parents === null) return null;\n for (const parent of parents) {\n result.push(\n item.startsWith('&') ? parent + item.slice(1) : `${parent} ${item}`,\n );\n }\n }\n return result;\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (visited.has(node) || !ctx.isStyleObject(node)) return;\n // Sub-elements and nested selectors are checked from their owner,\n // which knows the selector they extend.\n const owner = ownerEntry(node);\n if (\n owner &&\n (ctx.isSubElementKey(owner.key) ||\n ctx.isNestedSelectorKey(owner.key)) &&\n ctx.isStyleObject(owner.object)\n ) {\n return;\n }\n\n const base = baseSelector(node);\n const root = base === null ? null : firstPseudoElementItem([base]);\n checkStyleObject(node, ['&'], root);\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":"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"}
@@ -1,16 +1,35 @@
1
1
  import { createRule } from "../create-rule.js";
2
2
  import { getKeyName, getStringValue, unwrapExpression } from "../utils.js";
3
3
  import { TastyContext, styleObjectListeners } from "../context.js";
4
+ import { isPseudoElement, pseudoElementMismatch, pseudoName, splitAtPseudoElement } from "../pseudo-elements.js";
4
5
  import selectorParser from "postcss-selector-parser";
5
6
 
6
7
  //#region src/rules/no-state-in-selector.ts
7
8
  const STRUCTURAL_PSEUDOS = new Set([":is", ":where"]);
8
- const LEGACY_PSEUDO_ELEMENTS = new Set([
9
- ":before",
10
- ":after",
11
- ":first-line",
12
- ":first-letter"
13
- ]);
9
+ function isCondition(node) {
10
+ if (node.type === "attribute") return !(node.attribute === "data-element" && !node.namespace && node.operator === "=" && node.value);
11
+ if (node.type !== "pseudo") return false;
12
+ return !isPseudoElement(node) && !STRUCTURAL_PSEUDOS.has(pseudoName(node));
13
+ }
14
+ function findCondition(nodes) {
15
+ for (const node of nodes) {
16
+ if (isCondition(node)) return node.toString().trim();
17
+ if (selectorParser.isContainer(node)) {
18
+ const condition = findCondition(node.nodes);
19
+ if (condition !== null) return condition;
20
+ }
21
+ }
22
+ return null;
23
+ }
24
+ /** Whether @own() can express `node` on a pseudo-element. */
25
+ function isPseudoElementState(node) {
26
+ return node.type === "pseudo" && !pseudoElementMismatch(node.toString());
27
+ }
28
+ function firstItemCondition(item) {
29
+ const split = splitAtPseudoElement(item);
30
+ if (split === null) return findCondition(item.nodes);
31
+ return findCondition(item.nodes.slice(0, split.compound)) ?? findCondition(item.nodes.slice(split.last + 1).filter(isPseudoElementState));
32
+ }
14
33
  function firstStateCondition(selector) {
15
34
  let root;
16
35
  try {
@@ -18,23 +37,11 @@ function firstStateCondition(selector) {
18
37
  } catch {
19
38
  return null;
20
39
  }
21
- let condition = null;
22
- root.walk((node) => {
23
- if (node.type === "attribute") {
24
- if (node.attribute === "data-element" && !node.namespace && node.operator === "=" && node.value) return;
25
- } else if (node.type === "pseudo") {
26
- let name = node.value.toLowerCase();
27
- if (name.includes("\\")) {
28
- const colons = name.startsWith("::") ? "::" : ":";
29
- const identifier = selectorParser().astSync(name.slice(colons.length)).first?.first;
30
- if (identifier?.type === "tag") name = colons + identifier.value.toLowerCase();
31
- }
32
- if (name.startsWith("::") || LEGACY_PSEUDO_ELEMENTS.has(name) || STRUCTURAL_PSEUDOS.has(name)) return;
33
- } else return;
34
- condition = node.toString().trim();
35
- return false;
36
- });
37
- return condition;
40
+ for (const item of root.nodes) {
41
+ const condition = firstItemCondition(item);
42
+ if (condition !== null) return condition;
43
+ }
44
+ return null;
38
45
  }
39
46
  var no_state_in_selector_default = createRule({
40
47
  name: "no-state-in-selector",