@tenphi/eslint-plugin-tasty 0.11.4 → 1.0.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","names":[],"sources":["../src/context.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport type { RuleContext } from '@typescript-eslint/utils/ts-eslint';\nimport type { ResolvedConfig } from './types.js';\nimport { loadConfig } from './config.js';\nimport { DEFAULT_IMPORT_SOURCES } from './constants.js';\nimport { getKeyName } from './utils.js';\n\n/**\n * AST selectors that match ObjectExpressions in all known tasty style contexts:\n * call sites, variable declarations, satisfies/as expressions.\n */\nexport const STYLE_OBJECT_SELECTORS = [\n 'CallExpression ObjectExpression',\n 'VariableDeclarator > ObjectExpression',\n 'VariableDeclarator > TSSatisfiesExpression > ObjectExpression',\n 'VariableDeclarator > TSAsExpression > ObjectExpression',\n] as const;\n\n/**\n * Creates a record of ESLint listeners that all point to the same handler,\n * one entry per style-object AST selector.\n */\nexport function styleObjectListeners(\n handler: (node: TSESTree.ObjectExpression) => void,\n): Record<string, (node: TSESTree.ObjectExpression) => void> {\n const listeners: Record<string, (node: TSESTree.ObjectExpression) => void> =\n {};\n for (const selector of STYLE_OBJECT_SELECTORS) {\n listeners[selector] = handler;\n }\n return listeners;\n}\n\nexport interface TastyImport {\n localName: string;\n importedName: string;\n source: string;\n}\n\nconst TASTY_FUNCTION_NAMES = new Set([\n 'tasty',\n 'tastyStatic',\n 'useStyles',\n 'useGlobalStyles',\n 'configure',\n]);\n\n/**\n * Context tracker for a single file's lint pass.\n * Tracks which imports come from tasty and provides\n * helpers to determine if a node is in a tasty style context.\n */\nexport class TastyContext {\n readonly config: ResolvedConfig;\n private imports = new Map<string, TastyImport>();\n private importSources: Set<string>;\n\n constructor(\n private context: RuleContext<string, unknown[]>,\n config?: ResolvedConfig,\n ) {\n this.config = config ?? loadConfig(context.filename);\n this.importSources = new Set([\n ...DEFAULT_IMPORT_SOURCES,\n ...this.config.importSources,\n ]);\n }\n\n trackImport(node: TSESTree.ImportDeclaration): void {\n const source = node.source.value;\n if (!this.importSources.has(source)) return;\n\n for (const specifier of node.specifiers) {\n if (specifier.type === 'ImportSpecifier') {\n const importedName =\n specifier.imported.type === 'Identifier'\n ? specifier.imported.name\n : specifier.imported.value;\n if (TASTY_FUNCTION_NAMES.has(importedName)) {\n this.imports.set(specifier.local.name, {\n localName: specifier.local.name,\n importedName,\n source,\n });\n }\n }\n }\n }\n\n getImport(localName: string): TastyImport | undefined {\n return this.imports.get(localName);\n }\n\n isTastyCall(node: TSESTree.CallExpression): TastyImport | undefined {\n if (node.callee.type !== 'Identifier') return undefined;\n return this.imports.get(node.callee.name);\n }\n\n /**\n * Determines whether an object expression is a tasty style object\n * by walking up the AST to find a recognized call expression.\n */\n isStyleObject(node: TSESTree.ObjectExpression): boolean {\n return this.getStyleContext(node) !== null;\n }\n\n getStyleContext(node: TSESTree.Node): {\n type: 'tasty' | 'tastyStatic' | 'useStyles' | 'useGlobalStyles';\n isStaticCall: boolean;\n isSelectorMode: boolean;\n isExtending: boolean;\n } | null {\n // Sub-element objects inherit their parent style object's context\n if (node.type === 'ObjectExpression') {\n const parent = node.parent;\n if (parent?.type === 'Property' && !parent.computed) {\n const key = getKeyName(parent.key);\n if (key && /^[A-Z]/.test(key)) {\n const grandparent = parent.parent;\n if (grandparent?.type === 'ObjectExpression') {\n return this.getStyleContext(grandparent);\n }\n }\n }\n }\n\n let current: TSESTree.Node | undefined = node;\n\n while (current) {\n if (current.type === 'CallExpression') {\n const imp = this.isTastyCall(current);\n if (!imp) return null;\n\n const name = imp.importedName;\n const isStaticCall = name === 'tastyStatic';\n\n if (name === 'tasty') {\n return this.getTastyCallContext(current, node);\n }\n\n if (name === 'tastyStatic') {\n return this.getTastyStaticCallContext(current, node);\n }\n\n if (name === 'useStyles') {\n if (current.arguments[0] === node) {\n return {\n type: 'useStyles',\n isStaticCall: false,\n isSelectorMode: false,\n isExtending: false,\n };\n }\n }\n\n if (name === 'useGlobalStyles') {\n if (current.arguments[1] === node) {\n return {\n type: 'useGlobalStyles',\n isStaticCall,\n isSelectorMode: true,\n isExtending: false,\n };\n }\n }\n\n return null;\n }\n\n if (this.isStyleVariableDeclaration(current, node)) {\n return {\n type: 'tasty',\n isStaticCall: false,\n isSelectorMode: false,\n isExtending: false,\n };\n }\n\n current = current.parent;\n }\n\n return null;\n }\n\n private isStyleVariableDeclaration(\n current: TSESTree.Node,\n targetNode: TSESTree.Node,\n ): boolean {\n if (\n current.type !== 'VariableDeclarator' ||\n current.id.type !== 'Identifier'\n ) {\n return false;\n }\n\n let init: TSESTree.Node | null | undefined = current.init;\n while (\n init?.type === 'TSAsExpression' ||\n init?.type === 'TSSatisfiesExpression' ||\n init?.type === 'TSTypeAssertion' ||\n init?.type === 'TSNonNullExpression'\n ) {\n init = (init as TSESTree.TSAsExpression).expression;\n }\n\n if (init !== targetNode) return false;\n\n const name = current.id.name;\n\n if (/^[A-Z][A-Z0-9_]*$/.test(name)) return false;\n\n // Only `styles` and `*Styles` count. The singular `style` is conventionally\n // a DOM inline-style object (`el.style`, `CSSProperties`, `setStyle(el, …)`)\n // holding raw CSS longhands, not tasty syntax — matching it reported those\n // longhands as tasty violations. A `Styles` type annotation still opts a\n // differently-named variable in, below.\n if (name === 'styles' || name.endsWith('Styles')) return true;\n\n if (this.hasStylesTypeAnnotation(current)) return true;\n\n return false;\n }\n\n private hasStylesTypeAnnotation(node: TSESTree.VariableDeclarator): boolean {\n const annotation = node.id.typeAnnotation?.typeAnnotation;\n if (!annotation) return false;\n\n if (\n annotation.type === 'TSTypeReference' &&\n annotation.typeName.type === 'Identifier'\n ) {\n return /^Styles$/i.test(annotation.typeName.name);\n }\n\n return false;\n }\n\n private getTastyCallContext(\n call: TSESTree.CallExpression,\n targetNode: TSESTree.Node,\n ) {\n const args = call.arguments;\n\n // tasty({ styles: { ... } }) or tasty(Component, { styles: { ... } })\n const optionsArg =\n args.length >= 2 && args[0].type !== 'ObjectExpression'\n ? args[1]\n : args[0];\n\n if (\n optionsArg?.type === 'ObjectExpression' &&\n this.isInsideStylesProperty(optionsArg, targetNode)\n ) {\n return {\n type: 'tasty' as const,\n isStaticCall: false,\n isSelectorMode: false,\n isExtending: args.length >= 2 && args[0].type !== 'ObjectExpression',\n };\n }\n\n // Check if inside variants\n if (\n optionsArg?.type === 'ObjectExpression' &&\n this.isInsideVariantsProperty(optionsArg, targetNode)\n ) {\n return {\n type: 'tasty' as const,\n isStaticCall: false,\n isSelectorMode: false,\n isExtending: false,\n };\n }\n\n return null;\n }\n\n private getTastyStaticCallContext(\n call: TSESTree.CallExpression,\n targetNode: TSESTree.Node,\n ) {\n const args = call.arguments;\n\n // tastyStatic({ ... })\n if (args.length === 1 && args[0] === targetNode) {\n return {\n type: 'tastyStatic' as const,\n isStaticCall: true,\n isSelectorMode: false,\n isExtending: false,\n };\n }\n\n // tastyStatic(base, { ... }) or tastyStatic('selector', { ... })\n if (args.length === 2 && args[1] === targetNode) {\n const isSelectorMode = args[0].type === 'Literal';\n return {\n type: 'tastyStatic' as const,\n isStaticCall: true,\n isSelectorMode,\n isExtending: !isSelectorMode,\n };\n }\n\n return null;\n }\n\n private isInsideStylesProperty(\n optionsObj: TSESTree.ObjectExpression,\n targetNode: TSESTree.Node,\n ): boolean {\n for (const prop of optionsObj.properties) {\n if (\n prop.type === 'Property' &&\n prop.key.type === 'Identifier' &&\n prop.key.name === 'styles' &&\n prop.value === targetNode\n ) {\n return true;\n }\n }\n return false;\n }\n\n private isInsideVariantsProperty(\n optionsObj: TSESTree.ObjectExpression,\n targetNode: TSESTree.Node,\n ): boolean {\n for (const prop of optionsObj.properties) {\n if (\n prop.type === 'Property' &&\n prop.key.type === 'Identifier' &&\n prop.key.name === 'variants' &&\n prop.value.type === 'ObjectExpression'\n ) {\n for (const variantProp of prop.value.properties) {\n if (\n variantProp.type === 'Property' &&\n variantProp.value === targetNode\n ) {\n return true;\n }\n }\n }\n }\n return false;\n }\n\n /**\n * Checks if a property value node is a state mapping object\n * (i.e., an object where keys are state expressions and values are style values).\n */\n isStateMap(\n node: TSESTree.ObjectExpression,\n parentProperty: TSESTree.Property,\n ): boolean {\n const keyName = getKeyName(parentProperty.key);\n if (keyName === null) return false;\n\n // If the key starts with uppercase, it's a sub-element, not a state map\n if (/^[A-Z]/.test(keyName)) return false;\n\n // Special keys are not state maps\n if (keyName === '@keyframes' || keyName === '@properties') return false;\n\n // If the object has keys that look like state expressions, it's a state map.\n // A lone `_` (the fallback floor) or `''` (the default) is enough — both are\n // already covered by the string-literal / identifier branches below.\n return node.properties.some((prop) => {\n if (prop.type !== 'Property') return false;\n if (prop.key.type === 'Literal' && prop.key.value === '') return true;\n if (prop.key.type === 'Identifier') return true;\n if (prop.key.type === 'Literal' && typeof prop.key.value === 'string') {\n return true;\n }\n return false;\n });\n }\n\n /**\n * Checks if a key represents a sub-element (starts with uppercase).\n */\n isSubElementKey(key: string): boolean {\n return /^[A-Z]/.test(key);\n }\n\n /**\n * Checks if a key represents a nested selector (starts with &).\n */\n isNestedSelectorKey(key: string): boolean {\n return key.startsWith('&');\n }\n\n /**\n * Checks if a key is a custom CSS property definition ($name or $$name).\n */\n isCustomPropertyKey(key: string): boolean {\n return key.startsWith('$');\n }\n\n /**\n * Checks if a key is a color token definition (#name or ##name).\n */\n isColorTokenKey(key: string): boolean {\n return key.startsWith('#');\n }\n\n /**\n * Checks if a key is a special @ property (@keyframes, @properties).\n */\n isSpecialKey(key: string): boolean {\n return key.startsWith('@');\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"context.js","names":[],"sources":["../src/context.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport type { RuleContext } from '@typescript-eslint/utils/ts-eslint';\nimport type { ResolvedConfig } from './types.js';\nimport { loadConfig } from './config.js';\nimport { DEFAULT_IMPORT_SOURCES, SPECIAL_STYLE_KEYS } from './constants.js';\nimport { getKeyName } from './utils.js';\n\n/**\n * AST selectors that match ObjectExpressions in all known tasty style contexts:\n * call sites, variable declarations, satisfies/as expressions.\n */\nexport const STYLE_OBJECT_SELECTORS = [\n 'CallExpression ObjectExpression',\n 'VariableDeclarator > ObjectExpression',\n 'VariableDeclarator > TSSatisfiesExpression > ObjectExpression',\n 'VariableDeclarator > TSAsExpression > ObjectExpression',\n // `<Block styles={{…}} />`. Safe to match on the prop name alone: React's own\n // prop is `style`, singular, so a plural `styles` JSX prop is a Tasty\n // convention.\n \"JSXAttribute[name.name='styles'] > JSXExpressionContainer > ObjectExpression\",\n // A Storybook story's `args.styles`. Deliberately this specific rather than\n // matching any `styles` key: plenty of unrelated libraries take a `styles`\n // option object, and a bare key carries no evidence it is Tasty's. Story files\n // are where a lot of style code is authored and neither the call-site nor the\n // variable-name heuristic reaches them — the enclosing variable is named after\n // the story, and there is no Tasty call.\n // Superset of the `args.styles` shape; `isStylesPropertyValue` is the real\n // gate, so both `styles` and `'styles'` key forms are covered.\n 'Property > ObjectExpression > Property > ObjectExpression',\n] as const;\n\n/**\n * Creates a record of ESLint listeners that all point to the same handler,\n * one entry per style-object AST selector.\n */\nexport function styleObjectListeners(\n handler: (node: TSESTree.ObjectExpression) => void,\n): Record<string, (node: TSESTree.ObjectExpression) => void> {\n const listeners: Record<string, (node: TSESTree.ObjectExpression) => void> =\n {};\n\n // Selectors overlap — `tasty({ styles: {…} })` matches both\n // `CallExpression ObjectExpression` and the `styles` key selector — so the\n // handler must run at most once per object, or every rule double-reports.\n const visited = new WeakSet<TSESTree.ObjectExpression>();\n const once = (node: TSESTree.ObjectExpression) => {\n if (visited.has(node)) return;\n visited.add(node);\n handler(node);\n };\n\n for (const selector of STYLE_OBJECT_SELECTORS) {\n listeners[selector] = once;\n }\n\n return listeners;\n}\n\nexport interface TastyImport {\n localName: string;\n importedName: string;\n source: string;\n}\n\nconst TASTY_FUNCTION_NAMES = new Set([\n 'tasty',\n 'tastyStatic',\n 'useStyles',\n 'useGlobalStyles',\n 'configure',\n]);\n\n/**\n * Context tracker for a single file's lint pass.\n * Tracks which imports come from tasty and provides\n * helpers to determine if a node is in a tasty style context.\n */\nexport class TastyContext {\n readonly config: ResolvedConfig;\n private imports = new Map<string, TastyImport>();\n private importSources: Set<string>;\n\n constructor(\n private context: RuleContext<string, unknown[]>,\n config?: ResolvedConfig,\n ) {\n this.config = config ?? loadConfig(context.filename);\n this.importSources = new Set([\n ...DEFAULT_IMPORT_SOURCES,\n ...this.config.importSources,\n ]);\n }\n\n trackImport(node: TSESTree.ImportDeclaration): void {\n const source = node.source.value;\n if (!this.importSources.has(source)) return;\n\n for (const specifier of node.specifiers) {\n if (specifier.type === 'ImportSpecifier') {\n const importedName =\n specifier.imported.type === 'Identifier'\n ? specifier.imported.name\n : specifier.imported.value;\n if (TASTY_FUNCTION_NAMES.has(importedName)) {\n this.imports.set(specifier.local.name, {\n localName: specifier.local.name,\n importedName,\n source,\n });\n }\n }\n }\n }\n\n getImport(localName: string): TastyImport | undefined {\n return this.imports.get(localName);\n }\n\n isTastyCall(node: TSESTree.CallExpression): TastyImport | undefined {\n if (node.callee.type !== 'Identifier') return undefined;\n return this.imports.get(node.callee.name);\n }\n\n /**\n * Determines whether an object expression is a tasty style object\n * by walking up the AST to find a recognized call expression.\n */\n isStyleObject(node: TSESTree.ObjectExpression): boolean {\n return this.getStyleContext(node) !== null;\n }\n\n getStyleContext(node: TSESTree.Node): {\n type: 'tasty' | 'tastyStatic' | 'useStyles' | 'useGlobalStyles';\n isStaticCall: boolean;\n isSelectorMode: boolean;\n isExtending: boolean;\n } | null {\n // Sub-element objects inherit their parent style object's context\n if (node.type === 'ObjectExpression') {\n const parent = node.parent;\n if (parent?.type === 'Property' && !parent.computed) {\n const key = getKeyName(parent.key);\n if (key && /^[A-Z]/.test(key)) {\n const grandparent = parent.parent;\n if (grandparent?.type === 'ObjectExpression') {\n return this.getStyleContext(grandparent);\n }\n }\n }\n }\n\n // An object assigned to a `styles` key (or a `styles` JSX prop) is a style\n // object on its own terms — no enclosing tasty call or `styles`-ish variable\n // name required. Checked before the walk, which would otherwise climb past it\n // to an unrelated call or declaration and reject the whole thing.\n if (node.type === 'ObjectExpression' && this.isStylesPropertyValue(node)) {\n return {\n type: 'tasty',\n isStaticCall: false,\n isSelectorMode: false,\n isExtending: false,\n };\n }\n\n let current: TSESTree.Node | undefined = node;\n\n while (current) {\n if (current.type === 'CallExpression') {\n const imp = this.isTastyCall(current);\n if (!imp) return null;\n\n const name = imp.importedName;\n const isStaticCall = name === 'tastyStatic';\n\n if (name === 'tasty') {\n return this.getTastyCallContext(current, node);\n }\n\n if (name === 'tastyStatic') {\n return this.getTastyStaticCallContext(current, node);\n }\n\n if (name === 'useStyles') {\n if (current.arguments[0] === node) {\n return {\n type: 'useStyles',\n isStaticCall: false,\n isSelectorMode: false,\n isExtending: false,\n };\n }\n }\n\n if (name === 'useGlobalStyles') {\n if (current.arguments[1] === node) {\n return {\n type: 'useGlobalStyles',\n isStaticCall,\n isSelectorMode: true,\n isExtending: false,\n };\n }\n }\n\n return null;\n }\n\n if (this.isStyleVariableDeclaration(current, node)) {\n return {\n type: 'tasty',\n isStaticCall: false,\n isSelectorMode: false,\n isExtending: false,\n };\n }\n\n current = current.parent;\n }\n\n return null;\n }\n\n private isStyleVariableDeclaration(\n current: TSESTree.Node,\n targetNode: TSESTree.Node,\n ): boolean {\n if (\n current.type !== 'VariableDeclarator' ||\n current.id.type !== 'Identifier'\n ) {\n return false;\n }\n\n let init: TSESTree.Node | null | undefined = current.init;\n while (\n init?.type === 'TSAsExpression' ||\n init?.type === 'TSSatisfiesExpression' ||\n init?.type === 'TSTypeAssertion' ||\n init?.type === 'TSNonNullExpression'\n ) {\n init = (init as TSESTree.TSAsExpression).expression;\n }\n\n if (init !== targetNode) return false;\n\n const name = current.id.name;\n\n if (/^[A-Z][A-Z0-9_]*$/.test(name)) return false;\n\n // Only `styles` and `*Styles` count. The singular `style` is conventionally\n // a DOM inline-style object (`el.style`, `CSSProperties`, `setStyle(el, …)`)\n // holding raw CSS longhands, not tasty syntax — matching it reported those\n // longhands as tasty violations. A `Styles` type annotation still opts a\n // differently-named variable in, below.\n if (name === 'styles' || name.endsWith('Styles')) return true;\n\n if (this.hasStylesTypeAnnotation(current)) return true;\n\n return false;\n }\n\n private hasStylesTypeAnnotation(node: TSESTree.VariableDeclarator): boolean {\n const annotation = node.id.typeAnnotation?.typeAnnotation;\n if (!annotation) return false;\n\n if (\n annotation.type === 'TSTypeReference' &&\n annotation.typeName.type === 'Identifier'\n ) {\n return /^Styles$/i.test(annotation.typeName.name);\n }\n\n return false;\n }\n\n private getTastyCallContext(\n call: TSESTree.CallExpression,\n targetNode: TSESTree.Node,\n ) {\n const args = call.arguments;\n\n // tasty({ styles: { ... } }) or tasty(Component, { styles: { ... } })\n const optionsArg =\n args.length >= 2 && args[0].type !== 'ObjectExpression'\n ? args[1]\n : args[0];\n\n if (\n optionsArg?.type === 'ObjectExpression' &&\n this.isInsideStylesProperty(optionsArg, targetNode)\n ) {\n return {\n type: 'tasty' as const,\n isStaticCall: false,\n isSelectorMode: false,\n isExtending: args.length >= 2 && args[0].type !== 'ObjectExpression',\n };\n }\n\n // Check if inside variants\n if (\n optionsArg?.type === 'ObjectExpression' &&\n this.isInsideVariantsProperty(optionsArg, targetNode)\n ) {\n return {\n type: 'tasty' as const,\n isStaticCall: false,\n isSelectorMode: false,\n isExtending: false,\n };\n }\n\n return null;\n }\n\n private getTastyStaticCallContext(\n call: TSESTree.CallExpression,\n targetNode: TSESTree.Node,\n ) {\n const args = call.arguments;\n\n // tastyStatic({ ... })\n if (args.length === 1 && args[0] === targetNode) {\n return {\n type: 'tastyStatic' as const,\n isStaticCall: true,\n isSelectorMode: false,\n isExtending: false,\n };\n }\n\n // tastyStatic(base, { ... }) or tastyStatic('selector', { ... })\n if (args.length === 2 && args[1] === targetNode) {\n const isSelectorMode = args[0].type === 'Literal';\n return {\n type: 'tastyStatic' as const,\n isStaticCall: true,\n isSelectorMode,\n isExtending: !isSelectorMode,\n };\n }\n\n return null;\n }\n\n private isInsideStylesProperty(\n optionsObj: TSESTree.ObjectExpression,\n targetNode: TSESTree.Node,\n ): boolean {\n for (const prop of optionsObj.properties) {\n if (\n prop.type === 'Property' &&\n prop.key.type === 'Identifier' &&\n prop.key.name === 'styles' &&\n prop.value === targetNode\n ) {\n return true;\n }\n }\n return false;\n }\n\n private isInsideVariantsProperty(\n optionsObj: TSESTree.ObjectExpression,\n targetNode: TSESTree.Node,\n ): boolean {\n for (const prop of optionsObj.properties) {\n if (\n prop.type === 'Property' &&\n prop.key.type === 'Identifier' &&\n prop.key.name === 'variants' &&\n prop.value.type === 'ObjectExpression'\n ) {\n for (const variantProp of prop.value.properties) {\n if (\n variantProp.type === 'Property' &&\n variantProp.value === targetNode\n ) {\n return true;\n }\n }\n }\n }\n return false;\n }\n\n /**\n * Whether this object is a `styles` JSX prop value or a Storybook `args.styles`.\n *\n * Both are unambiguous enough to stand on their own without an import-tracked\n * call or a `styles`-ish variable name. A bare `styles` key elsewhere is not —\n * see the selector list for why.\n */\n isStylesPropertyValue(node: TSESTree.ObjectExpression): boolean {\n const parent = node.parent;\n\n if (parent?.type === 'Property' && !parent.computed) {\n if (getKeyName(parent.key) !== 'styles') return false;\n\n // Only inside `args: { … }`.\n const argsObject = parent.parent;\n const argsProperty =\n argsObject?.type === 'ObjectExpression' ? argsObject.parent : undefined;\n\n return (\n argsProperty?.type === 'Property' &&\n !argsProperty.computed &&\n getKeyName(argsProperty.key) === 'args'\n );\n }\n\n if (parent?.type === 'JSXExpressionContainer') {\n const attribute = parent.parent;\n\n return (\n attribute?.type === 'JSXAttribute' &&\n attribute.name.type === 'JSXIdentifier' &&\n attribute.name.name === 'styles'\n );\n }\n\n return false;\n }\n\n /**\n * Checks if a property value node is a state mapping object\n * (i.e., an object where keys are state expressions and values are style values).\n */\n isStateMap(\n node: TSESTree.ObjectExpression,\n parentProperty: TSESTree.Property,\n ): boolean {\n const keyName = getKeyName(parentProperty.key);\n if (keyName === null) return false;\n\n // If the key starts with uppercase, it's a sub-element, not a state map\n if (/^[A-Z]/.test(keyName)) return false;\n\n // Special keys are not state maps\n // At-rule blocks are not state maps — their object values are descriptor maps.\n if (SPECIAL_STYLE_KEYS.has(keyName)) return false;\n\n // If the object has keys that look like state expressions, it's a state map.\n // A lone `_` (the fallback floor) or `''` (the default) is enough — both are\n // already covered by the string-literal / identifier branches below.\n return node.properties.some((prop) => {\n if (prop.type !== 'Property') return false;\n if (prop.key.type === 'Literal' && prop.key.value === '') return true;\n if (prop.key.type === 'Identifier') return true;\n if (prop.key.type === 'Literal' && typeof prop.key.value === 'string') {\n return true;\n }\n return false;\n });\n }\n\n /**\n * Checks if a key represents a sub-element (starts with uppercase).\n */\n isSubElementKey(key: string): boolean {\n return /^[A-Z]/.test(key);\n }\n\n /**\n * Checks if a key represents a nested selector (starts with &).\n */\n isNestedSelectorKey(key: string): boolean {\n return key.startsWith('&');\n }\n\n /**\n * Checks if a key is a custom CSS property definition ($name or $$name).\n */\n isCustomPropertyKey(key: string): boolean {\n return key.startsWith('$');\n }\n\n /**\n * Checks if a key is a color token definition (#name or ##name).\n */\n isColorTokenKey(key: string): boolean {\n return key.startsWith('#');\n }\n\n /**\n * Checks if a key is a special @ property (@keyframes, @property, ...).\n */\n isSpecialKey(key: string): boolean {\n return key.startsWith('@');\n }\n}\n"],"mappings":";;;;;;;;;AAWA,MAAa,yBAAyB;CACpC;CACA;CACA;CACA;CAIA;CASA;CACD;;;;;AAMD,SAAgB,qBACd,SAC2D;CAC3D,MAAM,YACJ,EAAE;CAKJ,MAAM,0BAAU,IAAI,SAAoC;CACxD,MAAM,QAAQ,SAAoC;AAChD,MAAI,QAAQ,IAAI,KAAK,CAAE;AACvB,UAAQ,IAAI,KAAK;AACjB,UAAQ,KAAK;;AAGf,MAAK,MAAM,YAAY,uBACrB,WAAU,YAAY;AAGxB,QAAO;;AAST,MAAM,uBAAuB,IAAI,IAAI;CACnC;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;AAOF,IAAa,eAAb,MAA0B;CACxB,AAAS;CACT,AAAQ,0BAAU,IAAI,KAA0B;CAChD,AAAQ;CAER,YACE,AAAQ,SACR,QACA;EAFQ;AAGR,OAAK,SAAS,UAAU,WAAW,QAAQ,SAAS;AACpD,OAAK,gBAAgB,IAAI,IAAI,CAC3B,GAAG,wBACH,GAAG,KAAK,OAAO,cAChB,CAAC;;CAGJ,YAAY,MAAwC;EAClD,MAAM,SAAS,KAAK,OAAO;AAC3B,MAAI,CAAC,KAAK,cAAc,IAAI,OAAO,CAAE;AAErC,OAAK,MAAM,aAAa,KAAK,WAC3B,KAAI,UAAU,SAAS,mBAAmB;GACxC,MAAM,eACJ,UAAU,SAAS,SAAS,eACxB,UAAU,SAAS,OACnB,UAAU,SAAS;AACzB,OAAI,qBAAqB,IAAI,aAAa,CACxC,MAAK,QAAQ,IAAI,UAAU,MAAM,MAAM;IACrC,WAAW,UAAU,MAAM;IAC3B;IACA;IACD,CAAC;;;CAMV,UAAU,WAA4C;AACpD,SAAO,KAAK,QAAQ,IAAI,UAAU;;CAGpC,YAAY,MAAwD;AAClE,MAAI,KAAK,OAAO,SAAS,aAAc,QAAO;AAC9C,SAAO,KAAK,QAAQ,IAAI,KAAK,OAAO,KAAK;;;;;;CAO3C,cAAc,MAA0C;AACtD,SAAO,KAAK,gBAAgB,KAAK,KAAK;;CAGxC,gBAAgB,MAKP;AAEP,MAAI,KAAK,SAAS,oBAAoB;GACpC,MAAM,SAAS,KAAK;AACpB,OAAI,QAAQ,SAAS,cAAc,CAAC,OAAO,UAAU;IACnD,MAAM,MAAM,WAAW,OAAO,IAAI;AAClC,QAAI,OAAO,SAAS,KAAK,IAAI,EAAE;KAC7B,MAAM,cAAc,OAAO;AAC3B,SAAI,aAAa,SAAS,mBACxB,QAAO,KAAK,gBAAgB,YAAY;;;;AAUhD,MAAI,KAAK,SAAS,sBAAsB,KAAK,sBAAsB,KAAK,CACtE,QAAO;GACL,MAAM;GACN,cAAc;GACd,gBAAgB;GAChB,aAAa;GACd;EAGH,IAAI,UAAqC;AAEzC,SAAO,SAAS;AACd,OAAI,QAAQ,SAAS,kBAAkB;IACrC,MAAM,MAAM,KAAK,YAAY,QAAQ;AACrC,QAAI,CAAC,IAAK,QAAO;IAEjB,MAAM,OAAO,IAAI;IACjB,MAAM,eAAe,SAAS;AAE9B,QAAI,SAAS,QACX,QAAO,KAAK,oBAAoB,SAAS,KAAK;AAGhD,QAAI,SAAS,cACX,QAAO,KAAK,0BAA0B,SAAS,KAAK;AAGtD,QAAI,SAAS,aACX;SAAI,QAAQ,UAAU,OAAO,KAC3B,QAAO;MACL,MAAM;MACN,cAAc;MACd,gBAAgB;MAChB,aAAa;MACd;;AAIL,QAAI,SAAS,mBACX;SAAI,QAAQ,UAAU,OAAO,KAC3B,QAAO;MACL,MAAM;MACN;MACA,gBAAgB;MAChB,aAAa;MACd;;AAIL,WAAO;;AAGT,OAAI,KAAK,2BAA2B,SAAS,KAAK,CAChD,QAAO;IACL,MAAM;IACN,cAAc;IACd,gBAAgB;IAChB,aAAa;IACd;AAGH,aAAU,QAAQ;;AAGpB,SAAO;;CAGT,AAAQ,2BACN,SACA,YACS;AACT,MACE,QAAQ,SAAS,wBACjB,QAAQ,GAAG,SAAS,aAEpB,QAAO;EAGT,IAAI,OAAyC,QAAQ;AACrD,SACE,MAAM,SAAS,oBACf,MAAM,SAAS,2BACf,MAAM,SAAS,qBACf,MAAM,SAAS,sBAEf,QAAQ,KAAiC;AAG3C,MAAI,SAAS,WAAY,QAAO;EAEhC,MAAM,OAAO,QAAQ,GAAG;AAExB,MAAI,oBAAoB,KAAK,KAAK,CAAE,QAAO;AAO3C,MAAI,SAAS,YAAY,KAAK,SAAS,SAAS,CAAE,QAAO;AAEzD,MAAI,KAAK,wBAAwB,QAAQ,CAAE,QAAO;AAElD,SAAO;;CAGT,AAAQ,wBAAwB,MAA4C;EAC1E,MAAM,aAAa,KAAK,GAAG,gBAAgB;AAC3C,MAAI,CAAC,WAAY,QAAO;AAExB,MACE,WAAW,SAAS,qBACpB,WAAW,SAAS,SAAS,aAE7B,QAAO,YAAY,KAAK,WAAW,SAAS,KAAK;AAGnD,SAAO;;CAGT,AAAQ,oBACN,MACA,YACA;EACA,MAAM,OAAO,KAAK;EAGlB,MAAM,aACJ,KAAK,UAAU,KAAK,KAAK,GAAG,SAAS,qBACjC,KAAK,KACL,KAAK;AAEX,MACE,YAAY,SAAS,sBACrB,KAAK,uBAAuB,YAAY,WAAW,CAEnD,QAAO;GACL,MAAM;GACN,cAAc;GACd,gBAAgB;GAChB,aAAa,KAAK,UAAU,KAAK,KAAK,GAAG,SAAS;GACnD;AAIH,MACE,YAAY,SAAS,sBACrB,KAAK,yBAAyB,YAAY,WAAW,CAErD,QAAO;GACL,MAAM;GACN,cAAc;GACd,gBAAgB;GAChB,aAAa;GACd;AAGH,SAAO;;CAGT,AAAQ,0BACN,MACA,YACA;EACA,MAAM,OAAO,KAAK;AAGlB,MAAI,KAAK,WAAW,KAAK,KAAK,OAAO,WACnC,QAAO;GACL,MAAM;GACN,cAAc;GACd,gBAAgB;GAChB,aAAa;GACd;AAIH,MAAI,KAAK,WAAW,KAAK,KAAK,OAAO,YAAY;GAC/C,MAAM,iBAAiB,KAAK,GAAG,SAAS;AACxC,UAAO;IACL,MAAM;IACN,cAAc;IACd;IACA,aAAa,CAAC;IACf;;AAGH,SAAO;;CAGT,AAAQ,uBACN,YACA,YACS;AACT,OAAK,MAAM,QAAQ,WAAW,WAC5B,KACE,KAAK,SAAS,cACd,KAAK,IAAI,SAAS,gBAClB,KAAK,IAAI,SAAS,YAClB,KAAK,UAAU,WAEf,QAAO;AAGX,SAAO;;CAGT,AAAQ,yBACN,YACA,YACS;AACT,OAAK,MAAM,QAAQ,WAAW,WAC5B,KACE,KAAK,SAAS,cACd,KAAK,IAAI,SAAS,gBAClB,KAAK,IAAI,SAAS,cAClB,KAAK,MAAM,SAAS,oBAEpB;QAAK,MAAM,eAAe,KAAK,MAAM,WACnC,KACE,YAAY,SAAS,cACrB,YAAY,UAAU,WAEtB,QAAO;;AAKf,SAAO;;;;;;;;;CAUT,sBAAsB,MAA0C;EAC9D,MAAM,SAAS,KAAK;AAEpB,MAAI,QAAQ,SAAS,cAAc,CAAC,OAAO,UAAU;AACnD,OAAI,WAAW,OAAO,IAAI,KAAK,SAAU,QAAO;GAGhD,MAAM,aAAa,OAAO;GAC1B,MAAM,eACJ,YAAY,SAAS,qBAAqB,WAAW,SAAS;AAEhE,UACE,cAAc,SAAS,cACvB,CAAC,aAAa,YACd,WAAW,aAAa,IAAI,KAAK;;AAIrC,MAAI,QAAQ,SAAS,0BAA0B;GAC7C,MAAM,YAAY,OAAO;AAEzB,UACE,WAAW,SAAS,kBACpB,UAAU,KAAK,SAAS,mBACxB,UAAU,KAAK,SAAS;;AAI5B,SAAO;;;;;;CAOT,WACE,MACA,gBACS;EACT,MAAM,UAAU,WAAW,eAAe,IAAI;AAC9C,MAAI,YAAY,KAAM,QAAO;AAG7B,MAAI,SAAS,KAAK,QAAQ,CAAE,QAAO;AAInC,MAAI,mBAAmB,IAAI,QAAQ,CAAE,QAAO;AAK5C,SAAO,KAAK,WAAW,MAAM,SAAS;AACpC,OAAI,KAAK,SAAS,WAAY,QAAO;AACrC,OAAI,KAAK,IAAI,SAAS,aAAa,KAAK,IAAI,UAAU,GAAI,QAAO;AACjE,OAAI,KAAK,IAAI,SAAS,aAAc,QAAO;AAC3C,OAAI,KAAK,IAAI,SAAS,aAAa,OAAO,KAAK,IAAI,UAAU,SAC3D,QAAO;AAET,UAAO;IACP;;;;;CAMJ,gBAAgB,KAAsB;AACpC,SAAO,SAAS,KAAK,IAAI;;;;;CAM3B,oBAAoB,KAAsB;AACxC,SAAO,IAAI,WAAW,IAAI;;;;;CAM5B,oBAAoB,KAAsB;AACxC,SAAO,IAAI,WAAW,IAAI;;;;;CAM5B,gBAAgB,KAAsB;AACpC,SAAO,IAAI,WAAW,IAAI;;;;;CAM5B,aAAa,KAAsB;AACjC,SAAO,IAAI,WAAW,IAAI"}
package/dist/index.js CHANGED
@@ -33,49 +33,52 @@ import prefer_hide_default from "./rules/prefer-hide.js";
33
33
  import prefer_directional_shorthand_default from "./rules/prefer-directional-shorthand.js";
34
34
  import prefer_longhand_property_default from "./rules/prefer-longhand-property.js";
35
35
  import { recommended, strict } from "./configs.js";
36
+ import { createRequire } from "node:module";
36
37
 
37
38
  //#region src/index.ts
39
+ const ruleMap = {
40
+ "known-property": known_property_default,
41
+ "valid-value": valid_value_default,
42
+ "valid-color-token": valid_color_token_default,
43
+ "valid-custom-unit": valid_custom_unit_default,
44
+ "valid-state-key": valid_state_key_default,
45
+ "valid-styles-structure": valid_styles_structure_default,
46
+ "no-nested-selector": no_nested_selector_default,
47
+ "valid-custom-property": valid_custom_property_default,
48
+ "valid-preset": valid_preset_default,
49
+ "valid-recipe": valid_recipe_default,
50
+ "valid-boolean-property": valid_boolean_property_default,
51
+ "valid-directional-modifier": valid_directional_modifier_default,
52
+ "valid-radius-shape": valid_radius_shape_default,
53
+ "no-important": no_important_default,
54
+ "valid-sub-element": valid_sub_element_default,
55
+ "no-nested-state-map": no_nested_state_map_default,
56
+ "static-no-dynamic-values": static_no_dynamic_values_default,
57
+ "static-valid-selector": static_valid_selector_default,
58
+ "prefer-shorthand-property": prefer_shorthand_property_default,
59
+ "valid-transition": valid_transition_default,
60
+ "require-default-state": require_default_state_default,
61
+ "no-unknown-state-alias": no_unknown_state_alias_default,
62
+ "no-raw-color-values": no_raw_color_values_default,
63
+ "no-styles-prop": no_styles_prop_default,
64
+ "consistent-token-usage": consistent_token_usage_default,
65
+ "no-runtime-styles-mutation": no_runtime_styles_mutation_default,
66
+ "valid-state-definition": valid_state_definition_default,
67
+ "no-own-at-root": no_own_at_root_default,
68
+ "valid-default-state-order": valid_default_state_order_default,
69
+ "prefer-auto-calc": prefer_auto_calc_default,
70
+ "prefer-custom-property-syntax": prefer_custom_property_syntax_default,
71
+ "prefer-hide": prefer_hide_default,
72
+ "prefer-directional-shorthand": prefer_directional_shorthand_default,
73
+ "prefer-longhand-property": prefer_longhand_property_default
74
+ };
75
+ const { version } = createRequire(import.meta.url)("../package.json");
38
76
  const plugin = {
39
77
  meta: {
40
78
  name: "@tenphi/eslint-plugin-tasty",
41
- version: "0.1.0"
42
- },
43
- rules: {
44
- "known-property": known_property_default,
45
- "valid-value": valid_value_default,
46
- "valid-color-token": valid_color_token_default,
47
- "valid-custom-unit": valid_custom_unit_default,
48
- "valid-state-key": valid_state_key_default,
49
- "valid-styles-structure": valid_styles_structure_default,
50
- "no-nested-selector": no_nested_selector_default,
51
- "valid-custom-property": valid_custom_property_default,
52
- "valid-preset": valid_preset_default,
53
- "valid-recipe": valid_recipe_default,
54
- "valid-boolean-property": valid_boolean_property_default,
55
- "valid-directional-modifier": valid_directional_modifier_default,
56
- "valid-radius-shape": valid_radius_shape_default,
57
- "no-important": no_important_default,
58
- "valid-sub-element": valid_sub_element_default,
59
- "no-nested-state-map": no_nested_state_map_default,
60
- "static-no-dynamic-values": static_no_dynamic_values_default,
61
- "static-valid-selector": static_valid_selector_default,
62
- "prefer-shorthand-property": prefer_shorthand_property_default,
63
- "valid-transition": valid_transition_default,
64
- "require-default-state": require_default_state_default,
65
- "no-unknown-state-alias": no_unknown_state_alias_default,
66
- "no-raw-color-values": no_raw_color_values_default,
67
- "no-styles-prop": no_styles_prop_default,
68
- "consistent-token-usage": consistent_token_usage_default,
69
- "no-runtime-styles-mutation": no_runtime_styles_mutation_default,
70
- "valid-state-definition": valid_state_definition_default,
71
- "no-own-at-root": no_own_at_root_default,
72
- "valid-default-state-order": valid_default_state_order_default,
73
- "prefer-auto-calc": prefer_auto_calc_default,
74
- "prefer-custom-property-syntax": prefer_custom_property_syntax_default,
75
- "prefer-hide": prefer_hide_default,
76
- "prefer-directional-shorthand": prefer_directional_shorthand_default,
77
- "prefer-longhand-property": prefer_longhand_property_default
79
+ version
78
80
  },
81
+ rules: ruleMap,
79
82
  configs: {
80
83
  recommended: {
81
84
  plugins: { get tasty() {
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.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 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-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\nconst plugin = {\n meta: {\n name: '@tenphi/eslint-plugin-tasty',\n version: '0.1.0',\n },\n rules: ruleMap,\n configs: {\n recommended: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: recommended,\n },\n strict: {\n plugins: {\n get tasty() {\n return plugin;\n },\n },\n rules: strict,\n },\n },\n} satisfies TSESLint.FlatConfig.Plugin & {\n configs: Record<string, TSESLint.FlatConfig.Config>;\n};\n\nexport default plugin;\n\nexport { recommended, strict } from './configs.js';\nexport type { TastyValidationConfig, ResolvedConfig } from './types.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,MAAM,SAAS;CACb,MAAM;EACJ,MAAM;EACN,SAAS;EACV;CACD,OA1CsE;EACtE,kBAAkBA;EAClB,eAAeC;EACf,qBAAqBC;EACrB,qBAAqBC;EACrB,mBAAmBC;EACnB,0BAA0BC;EAC1B,sBAAsBC;EACtB,yBAAyBC;EACzB,gBAAgBC;EAChB,gBAAgBC;EAChB,0BAA0BC;EAC1B,8BAA8BC;EAC9B,sBAAsBC;EACtB,gBAAgBC;EAChB,qBAAqBC;EACrB,uBAAuBC;EACvB,4BAA4BC;EAC5B,yBAAyBC;EACzB,6BAA6BC;EAC7B,oBAAoBC;EACpB,yBAAyBC;EACzB,0BAA0BC;EAC1B,uBAAuBC;EACvB,kBAAkBC;EAClB,0BAA0BC;EAC1B,8BAA8BC;EAC9B,0BAA0BC;EAC1B,kBAAkBC;EAClB,6BAA6BC;EAC7B,oBAAoBC;EACpB,iCAAiCC;EACjC,eAAeC;EACf,gCAAgCC;EAChC,4BAA4BC;EAC7B;CAQC,SAAS;EACP,aAAa;GACX,SAAS,EACP,IAAI,QAAQ;AACV,WAAO;MAEV;GACD,OAAO;GACR;EACD,QAAQ;GACN,SAAS,EACP,IAAI,QAAQ;AACV,WAAO;MAEV;GACD,OAAO;GACR;EACF;CACF"}
1
+ {"version":3,"file":"index.js","names":["rules.knownProperty","rules.validValue","rules.validColorToken","rules.validCustomUnit","rules.validStateKey","rules.validStylesStructure","rules.noNestedSelector","rules.validCustomProperty","rules.validPreset","rules.validRecipe","rules.validBooleanProperty","rules.validDirectionalModifier","rules.validRadiusShape","rules.noImportant","rules.validSubElement","rules.noNestedStateMap","rules.staticNoDynamicValues","rules.staticValidSelector","rules.preferShorthandProperty","rules.validTransition","rules.requireDefaultState","rules.noUnknownStateAlias","rules.noRawColorValues","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-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 { TastyValidationConfig, ResolvedConfig } 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,kBAAkBC;CAClB,0BAA0BC;CAC1B,8BAA8BC;CAC9B,0BAA0BC;CAC1B,kBAAkBC;CAClB,6BAA6BC;CAC7B,oBAAoBC;CACpB,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"}
@@ -321,6 +321,10 @@ function classifyToken(raw, offset, errors, opts) {
321
321
  type: "important",
322
322
  raw: token
323
323
  };
324
+ if (token.startsWith("$$") || token.startsWith("##")) {
325
+ const call = classifyTastyFunctionCall(token, offset, errors);
326
+ if (call) return call;
327
+ }
324
328
  if (token.startsWith("$$")) {
325
329
  const name = token.slice(2);
326
330
  if (/^[a-z_][a-z0-9-_]*$/i.test(name)) return {
@@ -494,6 +498,36 @@ function classifyToken(raw, offset, errors, opts) {
494
498
  raw: token
495
499
  };
496
500
  }
501
+ /**
502
+ * Classify a `$$name(...)` / `##name(...)` Tasty `@function` call.
503
+ *
504
+ * Returns `null` when the token is not a call, so the caller falls through to the
505
+ * bare `$$name` / `##name` reference handling that `transition` relies on.
506
+ */
507
+ function classifyTastyFunctionCall(token, offset, errors) {
508
+ const openIdx = token.indexOf("(");
509
+ if (openIdx <= 2 || !token.endsWith(")")) return null;
510
+ const name = token.slice(2, openIdx);
511
+ const args = token.slice(openIdx + 1, -1);
512
+ if (!/^[a-z_][a-z0-9-_]*$/i.test(name)) {
513
+ errors.push({
514
+ message: `Invalid function name in '${token}'.`,
515
+ offset,
516
+ length: token.length,
517
+ raw: token
518
+ });
519
+ return {
520
+ type: "unknown",
521
+ raw: token
522
+ };
523
+ }
524
+ return {
525
+ type: "tasty-function",
526
+ name,
527
+ args,
528
+ raw: token
529
+ };
530
+ }
497
531
  function classifyColorToken(token, offset, errors) {
498
532
  const raw = token;
499
533
  const name = token.slice(1);
@@ -1 +1 @@
1
- {"version":3,"file":"value-parser.js","names":[],"sources":["../../src/parsers/value-parser.ts"],"sourcesContent":["import { scanTokens, checkBracketBalance } from './utils.js';\nimport { BUILT_IN_UNITS, CSS_UNITS } from '../constants.js';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type ValueToken =\n | { type: 'color-token'; name: string; opacity?: string; raw: string }\n | { type: 'color-ref'; name: string; raw: string }\n | { type: 'custom-prop'; name: string; raw: string }\n | { type: 'custom-prop-ref'; name: string; raw: string }\n | { type: 'custom-unit'; value: number; unit: string; raw: string }\n | { type: 'css-unit'; value: number; unit: string; raw: string }\n | { type: 'number'; value: number; raw: string }\n | { type: 'keyword'; value: string }\n | { type: 'css-function'; name: string; args: string; raw: string }\n | { type: 'string'; value: string; raw: string }\n | { type: 'important'; raw: string }\n | { type: 'group-expr'; inner: string; raw: string }\n | { type: 'unknown'; raw: string };\n\nexport interface ValueError {\n message: string;\n offset: number;\n length: number;\n raw?: string;\n}\n\nexport interface ValuePart {\n tokens: ValueToken[];\n}\n\nexport interface ValueGroup {\n parts: ValuePart[];\n}\n\nexport interface ValueParseResult {\n groups: ValueGroup[];\n errors: ValueError[];\n}\n\nexport interface ValueParserOptions {\n knownUnits?: Set<string> | string[];\n knownFuncs?: Set<string> | string[];\n skipUnitValidation?: boolean;\n skipFuncValidation?: boolean;\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst RE_UNIT_NUM = /^[+-]?(?:\\d*\\.\\d+|\\d+)([a-z][a-z0-9]*)$/;\nconst RE_NUMBER = /^[+-]?(?:\\d*\\.\\d+|\\d+)$/;\nconst RE_CSS_UNIT_NUM = /^[+-]?(?:\\d*\\.\\d+|\\d+)([a-z%]+)$/;\n\nconst COLOR_FUNCS = new Set([\n 'rgb',\n 'rgba',\n 'hsl',\n 'hsla',\n 'hwb',\n 'lab',\n 'lch',\n 'oklab',\n 'oklch',\n 'color',\n 'device-cmyk',\n 'gray',\n 'color-mix',\n 'color-contrast',\n]);\n\nconst CSS_FUNCS = new Set([\n 'calc',\n 'calc-size',\n 'min',\n 'max',\n 'clamp',\n 'abs',\n 'sign',\n 'round',\n 'mod',\n 'rem',\n 'sin',\n 'cos',\n 'tan',\n 'asin',\n 'acos',\n 'atan',\n 'atan2',\n 'pow',\n 'sqrt',\n 'hypot',\n 'log',\n 'exp',\n 'var',\n 'env',\n 'attr',\n 'counter',\n 'counters',\n 'image-set',\n 'cross-fade',\n 'image',\n 'paint',\n 'element',\n 'linear-gradient',\n 'radial-gradient',\n 'conic-gradient',\n 'repeating-linear-gradient',\n 'repeating-radial-gradient',\n 'repeating-conic-gradient',\n 'url',\n 'fit-content',\n 'minmax',\n 'repeat',\n 'cubic-bezier',\n 'steps',\n 'linear',\n 'rotate',\n 'scale',\n 'translate',\n 'skew',\n 'matrix',\n 'perspective',\n 'rotate3d',\n 'rotateX',\n 'rotateY',\n 'rotateZ',\n 'scale3d',\n 'scaleX',\n 'scaleY',\n 'scaleZ',\n 'translate3d',\n 'translateX',\n 'translateY',\n 'translateZ',\n 'skewX',\n 'skewY',\n 'matrix3d',\n 'blur',\n 'brightness',\n 'contrast',\n 'drop-shadow',\n 'grayscale',\n 'hue-rotate',\n 'invert',\n 'opacity',\n 'saturate',\n 'sepia',\n 'polygon',\n 'circle',\n 'ellipse',\n 'inset',\n 'path',\n 'xywh',\n 'rect',\n 'ray',\n 'light-dark',\n 'anchor',\n 'anchor-size',\n 'scroll',\n 'view',\n 'symbols',\n]);\n\nconst VALUE_KEYWORDS = new Set([\n 'auto',\n 'none',\n 'normal',\n 'inherit',\n 'initial',\n 'unset',\n 'revert',\n 'revert-layer',\n 'max-content',\n 'min-content',\n 'fit-content',\n 'stretch',\n 'transparent',\n 'currentcolor',\n 'currentColor',\n // display\n 'block',\n 'inline',\n 'inline-block',\n 'flex',\n 'inline-flex',\n 'grid',\n 'inline-grid',\n 'contents',\n 'table',\n 'table-row',\n 'table-cell',\n 'list-item',\n 'flow-root',\n // position\n 'static',\n 'relative',\n 'absolute',\n 'fixed',\n 'sticky',\n // overflow\n 'visible',\n 'hidden',\n 'scroll',\n 'clip',\n 'overlay',\n // flex/align\n 'center',\n 'start',\n 'end',\n 'flex-start',\n 'flex-end',\n 'space-between',\n 'space-around',\n 'space-evenly',\n 'baseline',\n // flow\n 'row',\n 'column',\n 'row-reverse',\n 'column-reverse',\n 'wrap',\n 'nowrap',\n 'dense',\n // border\n 'solid',\n 'dashed',\n 'dotted',\n 'double',\n 'groove',\n 'ridge',\n 'outset',\n // directional\n 'top',\n 'right',\n 'bottom',\n 'left',\n 'top-left',\n 'top-right',\n 'bottom-left',\n 'bottom-right',\n // radius shapes\n 'round',\n 'ellipse',\n 'leaf',\n 'backleaf',\n // dimension modifiers\n 'min',\n 'max',\n // text\n 'bold',\n 'bolder',\n 'lighter',\n 'italic',\n 'oblique',\n 'uppercase',\n 'lowercase',\n 'capitalize',\n 'line-through',\n 'underline',\n 'overline',\n 'wavy',\n // cursor\n 'pointer',\n 'default',\n 'text',\n 'move',\n 'grab',\n 'grabbing',\n 'not-allowed',\n 'crosshair',\n 'wait',\n 'help',\n 'col-resize',\n 'row-resize',\n 'n-resize',\n 's-resize',\n 'e-resize',\n 'w-resize',\n 'ne-resize',\n 'nw-resize',\n 'se-resize',\n 'sw-resize',\n 'ew-resize',\n 'ns-resize',\n 'zoom-in',\n 'zoom-out',\n // misc\n 'cover',\n 'contain',\n 'fill',\n 'no-repeat',\n 'repeat-x',\n 'repeat-y',\n 'border-box',\n 'padding-box',\n 'content-box',\n 'break-word',\n 'break-all',\n 'keep-all',\n 'anywhere',\n 'pre',\n 'pre-wrap',\n 'pre-line',\n 'balance',\n 'smooth',\n 'horizontal',\n 'vertical',\n 'both',\n 'mandatory',\n 'proximity',\n // white-space\n 'collapse',\n 'preserve',\n 'preserve-breaks',\n 'break-spaces',\n // text-wrap\n 'pretty',\n 'stable',\n // will-change\n 'transform',\n 'opacity',\n // animation\n 'infinite',\n 'alternate',\n 'alternate-reverse',\n 'reverse',\n 'forwards',\n 'backwards',\n 'running',\n 'paused',\n 'ease',\n 'ease-in',\n 'ease-out',\n 'ease-in-out',\n 'step-start',\n 'step-end',\n // scrollbar\n 'thin',\n 'always',\n 'both-edges',\n // width/height\n 'fixed',\n // shadow\n 'inset',\n // textOverflow\n 'clip',\n // other\n 'ltr',\n 'rtl',\n 'embed',\n 'isolate',\n 'isolate-override',\n 'plaintext',\n 'horizontal-tb',\n 'vertical-rl',\n 'vertical-lr',\n 'sideways-rl',\n 'sideways-lr',\n 'monospace',\n 'serif',\n 'sans-serif',\n 'cursive',\n 'fantasy',\n 'system-ui',\n 'ui-serif',\n 'ui-sans-serif',\n 'ui-monospace',\n 'ui-rounded',\n 'to',\n // misc\n 'strong',\n 'tight',\n 'icon',\n]);\n\n// ============================================================================\n// Classifier\n// ============================================================================\n\nfunction toSet(input?: Set<string> | string[]): Set<string> {\n if (!input) return new Set();\n return input instanceof Set ? input : new Set(input);\n}\n\nfunction classifyToken(\n raw: string,\n offset: number,\n errors: ValueError[],\n opts: ValueParserOptions,\n): ValueToken {\n const token = raw.trim();\n if (!token) return { type: 'keyword', value: '' };\n\n // Quoted strings\n if (\n (token.startsWith('\"') && token.endsWith('\"')) ||\n (token.startsWith(\"'\") && token.endsWith(\"'\"))\n ) {\n return { type: 'string', value: token.slice(1, -1), raw: token };\n }\n\n // !important\n if (token === '!important') {\n return { type: 'important', raw: token };\n }\n\n // Double prefix: $$name (custom property reference for transitions)\n if (token.startsWith('$$')) {\n const name = token.slice(2);\n if (/^[a-z_][a-z0-9-_]*$/i.test(name)) {\n return { type: 'custom-prop-ref', name, raw: token };\n }\n errors.push({\n message: `Invalid custom property reference '${token}'.`,\n offset,\n length: token.length,\n raw: token,\n });\n return { type: 'unknown', raw: token };\n }\n\n // Double prefix: ##name (color reference for transitions)\n if (token.startsWith('##')) {\n const name = token.slice(2);\n if (/^[a-z_][a-z0-9-_]*$/i.test(name)) {\n return { type: 'color-ref', name, raw: token };\n }\n errors.push({\n message: `Invalid color reference '${token}'.`,\n offset,\n length: token.length,\n raw: token,\n });\n return { type: 'unknown', raw: token };\n }\n\n // Custom property: $name\n if (token.startsWith('$')) {\n const name = token.slice(1);\n if (/^[a-z_][a-z0-9-_]*$/i.test(name)) {\n return { type: 'custom-prop', name, raw: token };\n }\n errors.push({\n message: `Invalid custom property syntax '${token}'.`,\n offset,\n length: token.length,\n raw: token,\n });\n return { type: 'unknown', raw: token };\n }\n\n // Color token: #name, #name.N, #name.$prop, or bare # (error)\n if (token.startsWith('#')) {\n if (token.length === 1) {\n errors.push({\n message: 'Empty color token name.',\n offset,\n length: 1,\n raw: token,\n });\n return { type: 'unknown', raw: token };\n }\n return classifyColorToken(token, offset, errors);\n }\n\n // Parenthesized expression: (expr) — fallback, auto-calc, etc.\n if (token.startsWith('(') && token.endsWith(')')) {\n return { type: 'group-expr', inner: token.slice(1, -1), raw: token };\n }\n\n // Function call: name(...)\n const openIdx = token.indexOf('(');\n if (openIdx > 0 && token.endsWith(')')) {\n const fname = token.slice(0, openIdx);\n const args = token.slice(openIdx + 1, -1);\n\n if (opts.skipFuncValidation) {\n return { type: 'css-function', name: fname, args, raw: token };\n }\n\n const knownFuncs = toSet(opts.knownFuncs);\n\n if (\n COLOR_FUNCS.has(fname) ||\n CSS_FUNCS.has(fname) ||\n knownFuncs.has(fname)\n ) {\n return { type: 'css-function', name: fname, args, raw: token };\n }\n\n // Unknown function — still return as css-function but flag it\n errors.push({\n message: `Unknown function '${fname}()'.`,\n offset,\n length: token.length,\n raw: token,\n });\n return { type: 'css-function', name: fname, args, raw: token };\n }\n\n // Unit number: 2x, 1.5r, 10px, 50%, 1fr\n const unitMatch = token.match(RE_UNIT_NUM);\n if (unitMatch) {\n const unit = unitMatch[1];\n const numVal = parseFloat(token.slice(0, -unit.length));\n\n if (opts.skipUnitValidation) {\n return { type: 'custom-unit', value: numVal, unit, raw: token };\n }\n\n const knownUnits = toSet(opts.knownUnits);\n\n if (BUILT_IN_UNITS.has(unit) || knownUnits.has(unit)) {\n return { type: 'custom-unit', value: numVal, unit, raw: token };\n }\n if (CSS_UNITS.has(unit)) {\n return { type: 'css-unit', value: numVal, unit, raw: token };\n }\n\n errors.push({\n message: `Unknown unit '${unit}' in '${token}'.`,\n offset,\n length: token.length,\n raw: token,\n });\n return { type: 'unknown', raw: token };\n }\n\n // CSS unit with % (RE_UNIT_NUM doesn't match % since it expects alpha)\n const cssUnitMatch = token.match(RE_CSS_UNIT_NUM);\n if (cssUnitMatch) {\n const unit = cssUnitMatch[1];\n const numVal = parseFloat(token.slice(0, -unit.length));\n if (CSS_UNITS.has(unit)) {\n return { type: 'css-unit', value: numVal, unit, raw: token };\n }\n }\n\n // Plain number\n if (RE_NUMBER.test(token)) {\n return { type: 'number', value: parseFloat(token), raw: token };\n }\n\n // Tasty merge directive (@inherit)\n if (token === '@inherit') {\n return { type: 'keyword', value: token };\n }\n\n // Known keyword\n if (VALUE_KEYWORDS.has(token) || VALUE_KEYWORDS.has(token.toLowerCase())) {\n return { type: 'keyword', value: token };\n }\n\n // CSS custom property function var(--name)\n if (token.startsWith('var(') && token.endsWith(')')) {\n return {\n type: 'css-function',\n name: 'var',\n args: token.slice(4, -1),\n raw: token,\n };\n }\n\n // Unknown token\n return { type: 'unknown', raw: token };\n}\n\nfunction classifyColorToken(\n token: string,\n offset: number,\n errors: ValueError[],\n): ValueToken {\n const raw = token;\n\n // Strip leading #\n const name = token.slice(1);\n\n if (name.length === 0) {\n errors.push({\n message: 'Empty color token name.',\n offset,\n length: token.length,\n raw,\n });\n return { type: 'unknown', raw };\n }\n\n // Check for opacity suffix\n const dotIndex = name.indexOf('.');\n if (dotIndex !== -1) {\n const tokenName = name.slice(0, dotIndex);\n const opacitySuffix = name.slice(dotIndex + 1);\n\n if (tokenName.length === 0) {\n errors.push({\n message: 'Empty color token name before opacity.',\n offset,\n length: token.length,\n raw,\n });\n return { type: 'unknown', raw };\n }\n\n // Dynamic opacity from CSS custom property\n if (opacitySuffix.startsWith('$')) {\n return {\n type: 'color-token',\n name: tokenName,\n opacity: opacitySuffix,\n raw,\n };\n }\n\n if (opacitySuffix.length === 0) {\n errors.push({\n message: 'Trailing dot with no opacity value.',\n offset,\n length: token.length,\n raw,\n });\n return { type: 'unknown', raw };\n }\n\n const opacity = Number(opacitySuffix);\n if (isNaN(opacity)) {\n errors.push({\n message: `Invalid opacity value '${opacitySuffix}'.`,\n offset,\n length: token.length,\n raw,\n });\n return { type: 'unknown', raw };\n }\n if (opacity < 0) {\n errors.push({\n message: 'Opacity cannot be negative.',\n offset,\n length: token.length,\n raw,\n });\n return { type: 'unknown', raw };\n }\n if (opacity > 100) {\n errors.push({\n message: `Opacity '${opacitySuffix}' exceeds 100.`,\n offset,\n length: token.length,\n raw,\n });\n return { type: 'unknown', raw };\n }\n\n return {\n type: 'color-token',\n name: tokenName,\n opacity: opacitySuffix,\n raw,\n };\n }\n\n return { type: 'color-token', name, raw };\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Parse a style value string into typed tokens organized by\n * comma-separated groups and slash-separated parts.\n */\nexport function parseValue(\n src: string,\n opts: ValueParserOptions = {},\n): ValueParseResult {\n const errors: ValueError[] = [];\n\n // Check bracket balance first\n const bracketError = checkBracketBalance(src);\n if (bracketError) {\n errors.push({\n message: bracketError.message,\n offset: bracketError.position,\n length: 1,\n });\n return { groups: [], errors };\n }\n\n const scanned = scanTokens(src);\n\n const groups: ValueGroup[] = [];\n let currentParts: ValuePart[] = [];\n let currentTokens: ValueToken[] = [];\n\n const endPart = () => {\n if (currentTokens.length > 0) {\n currentParts.push({ tokens: currentTokens });\n currentTokens = [];\n }\n };\n\n const endGroup = () => {\n endPart();\n if (currentParts.length > 0) {\n groups.push({ parts: currentParts });\n } else {\n groups.push({ parts: [{ tokens: [] }] });\n }\n currentParts = [];\n };\n\n for (const scanned_token of scanned) {\n if (scanned_token.value) {\n const classified = classifyToken(\n scanned_token.value,\n scanned_token.offset,\n errors,\n opts,\n );\n currentTokens.push(classified);\n }\n if (scanned_token.isSlash) endPart();\n if (scanned_token.isComma) endGroup();\n }\n\n // Push final group\n if (\n currentTokens.length > 0 ||\n currentParts.length > 0 ||\n groups.length === 0\n ) {\n endGroup();\n }\n\n return { groups, errors };\n}\n\n/**\n * Extract all tokens of a specific type from a parse result.\n */\nexport function extractTokensByType<T extends ValueToken['type']>(\n result: ValueParseResult,\n type: T,\n): Extract<ValueToken, { type: T }>[] {\n const tokens: Extract<ValueToken, { type: T }>[] = [];\n for (const group of result.groups) {\n for (const part of group.parts) {\n for (const token of part.tokens) {\n if (token.type === type) {\n tokens.push(token as Extract<ValueToken, { type: T }>);\n }\n }\n }\n }\n return tokens;\n}\n\n/**\n * Get a flat list of all tokens from a parse result.\n */\nexport function flattenTokens(result: ValueParseResult): ValueToken[] {\n const tokens: ValueToken[] = [];\n for (const group of result.groups) {\n for (const part of group.parts) {\n tokens.push(...part.tokens);\n }\n }\n return tokens;\n}\n"],"mappings":";;;;AAqDA,MAAM,cAAc;AACpB,MAAM,YAAY;AAClB,MAAM,kBAAkB;AAExB,MAAM,cAAc,IAAI,IAAI;CAC1B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAEF,MAAM,YAAY,IAAI,IAAI;CACxB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAEF,MAAM,iBAAiB,IAAI,IAAI;CAC7B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CAEA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;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"}
1
+ {"version":3,"file":"value-parser.js","names":[],"sources":["../../src/parsers/value-parser.ts"],"sourcesContent":["import { scanTokens, checkBracketBalance } from './utils.js';\nimport { BUILT_IN_UNITS, CSS_UNITS } from '../constants.js';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type ValueToken =\n | { type: 'color-token'; name: string; opacity?: string; raw: string }\n | { type: 'color-ref'; name: string; raw: string }\n | { type: 'custom-prop'; name: string; raw: string }\n | { type: 'custom-prop-ref'; name: string; raw: string }\n | { type: 'custom-unit'; value: number; unit: string; raw: string }\n | { type: 'css-unit'; value: number; unit: string; raw: string }\n | { type: 'number'; value: number; raw: string }\n | { type: 'keyword'; value: string }\n | { type: 'css-function'; name: string; args: string; raw: string }\n /**\n * A Tasty CSS `@function` call: `$$name(args)` (emits native `--name(args)`).\n * Distinct from `custom-prop-ref`, which is the bare `$$name` form that\n * `transition` uses to name a custom property.\n */\n | { type: 'tasty-function'; name: string; args: string; raw: string }\n | { type: 'string'; value: string; raw: string }\n | { type: 'important'; raw: string }\n | { type: 'group-expr'; inner: string; raw: string }\n | { type: 'unknown'; raw: string };\n\nexport interface ValueError {\n message: string;\n offset: number;\n length: number;\n raw?: string;\n}\n\nexport interface ValuePart {\n tokens: ValueToken[];\n}\n\nexport interface ValueGroup {\n parts: ValuePart[];\n}\n\nexport interface ValueParseResult {\n groups: ValueGroup[];\n errors: ValueError[];\n}\n\nexport interface ValueParserOptions {\n knownUnits?: Set<string> | string[];\n knownFuncs?: Set<string> | string[];\n skipUnitValidation?: boolean;\n skipFuncValidation?: boolean;\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst RE_UNIT_NUM = /^[+-]?(?:\\d*\\.\\d+|\\d+)([a-z][a-z0-9]*)$/;\nconst RE_NUMBER = /^[+-]?(?:\\d*\\.\\d+|\\d+)$/;\nconst RE_CSS_UNIT_NUM = /^[+-]?(?:\\d*\\.\\d+|\\d+)([a-z%]+)$/;\n\nconst COLOR_FUNCS = new Set([\n 'rgb',\n 'rgba',\n 'hsl',\n 'hsla',\n 'hwb',\n 'lab',\n 'lch',\n 'oklab',\n 'oklch',\n 'color',\n 'device-cmyk',\n 'gray',\n 'color-mix',\n 'color-contrast',\n]);\n\nconst CSS_FUNCS = new Set([\n 'calc',\n 'calc-size',\n 'min',\n 'max',\n 'clamp',\n 'abs',\n 'sign',\n 'round',\n 'mod',\n 'rem',\n 'sin',\n 'cos',\n 'tan',\n 'asin',\n 'acos',\n 'atan',\n 'atan2',\n 'pow',\n 'sqrt',\n 'hypot',\n 'log',\n 'exp',\n 'var',\n 'env',\n 'attr',\n 'counter',\n 'counters',\n 'image-set',\n 'cross-fade',\n 'image',\n 'paint',\n 'element',\n 'linear-gradient',\n 'radial-gradient',\n 'conic-gradient',\n 'repeating-linear-gradient',\n 'repeating-radial-gradient',\n 'repeating-conic-gradient',\n 'url',\n 'fit-content',\n 'minmax',\n 'repeat',\n 'cubic-bezier',\n 'steps',\n 'linear',\n 'rotate',\n 'scale',\n 'translate',\n 'skew',\n 'matrix',\n 'perspective',\n 'rotate3d',\n 'rotateX',\n 'rotateY',\n 'rotateZ',\n 'scale3d',\n 'scaleX',\n 'scaleY',\n 'scaleZ',\n 'translate3d',\n 'translateX',\n 'translateY',\n 'translateZ',\n 'skewX',\n 'skewY',\n 'matrix3d',\n 'blur',\n 'brightness',\n 'contrast',\n 'drop-shadow',\n 'grayscale',\n 'hue-rotate',\n 'invert',\n 'opacity',\n 'saturate',\n 'sepia',\n 'polygon',\n 'circle',\n 'ellipse',\n 'inset',\n 'path',\n 'xywh',\n 'rect',\n 'ray',\n 'light-dark',\n 'anchor',\n 'anchor-size',\n 'scroll',\n 'view',\n 'symbols',\n]);\n\nconst VALUE_KEYWORDS = new Set([\n 'auto',\n 'none',\n 'normal',\n 'inherit',\n 'initial',\n 'unset',\n 'revert',\n 'revert-layer',\n 'max-content',\n 'min-content',\n 'fit-content',\n 'stretch',\n 'transparent',\n 'currentcolor',\n 'currentColor',\n // display\n 'block',\n 'inline',\n 'inline-block',\n 'flex',\n 'inline-flex',\n 'grid',\n 'inline-grid',\n 'contents',\n 'table',\n 'table-row',\n 'table-cell',\n 'list-item',\n 'flow-root',\n // position\n 'static',\n 'relative',\n 'absolute',\n 'fixed',\n 'sticky',\n // overflow\n 'visible',\n 'hidden',\n 'scroll',\n 'clip',\n 'overlay',\n // flex/align\n 'center',\n 'start',\n 'end',\n 'flex-start',\n 'flex-end',\n 'space-between',\n 'space-around',\n 'space-evenly',\n 'baseline',\n // flow\n 'row',\n 'column',\n 'row-reverse',\n 'column-reverse',\n 'wrap',\n 'nowrap',\n 'dense',\n // border\n 'solid',\n 'dashed',\n 'dotted',\n 'double',\n 'groove',\n 'ridge',\n 'outset',\n // directional\n 'top',\n 'right',\n 'bottom',\n 'left',\n 'top-left',\n 'top-right',\n 'bottom-left',\n 'bottom-right',\n // radius shapes\n 'round',\n 'ellipse',\n 'leaf',\n 'backleaf',\n // dimension modifiers\n 'min',\n 'max',\n // text\n 'bold',\n 'bolder',\n 'lighter',\n 'italic',\n 'oblique',\n 'uppercase',\n 'lowercase',\n 'capitalize',\n 'line-through',\n 'underline',\n 'overline',\n 'wavy',\n // cursor\n 'pointer',\n 'default',\n 'text',\n 'move',\n 'grab',\n 'grabbing',\n 'not-allowed',\n 'crosshair',\n 'wait',\n 'help',\n 'col-resize',\n 'row-resize',\n 'n-resize',\n 's-resize',\n 'e-resize',\n 'w-resize',\n 'ne-resize',\n 'nw-resize',\n 'se-resize',\n 'sw-resize',\n 'ew-resize',\n 'ns-resize',\n 'zoom-in',\n 'zoom-out',\n // misc\n 'cover',\n 'contain',\n 'fill',\n 'no-repeat',\n 'repeat-x',\n 'repeat-y',\n 'border-box',\n 'padding-box',\n 'content-box',\n 'break-word',\n 'break-all',\n 'keep-all',\n 'anywhere',\n 'pre',\n 'pre-wrap',\n 'pre-line',\n 'balance',\n 'smooth',\n 'horizontal',\n 'vertical',\n 'both',\n 'mandatory',\n 'proximity',\n // white-space\n 'collapse',\n 'preserve',\n 'preserve-breaks',\n 'break-spaces',\n // text-wrap\n 'pretty',\n 'stable',\n // will-change\n 'transform',\n 'opacity',\n // animation\n 'infinite',\n 'alternate',\n 'alternate-reverse',\n 'reverse',\n 'forwards',\n 'backwards',\n 'running',\n 'paused',\n 'ease',\n 'ease-in',\n 'ease-out',\n 'ease-in-out',\n 'step-start',\n 'step-end',\n // scrollbar\n 'thin',\n 'always',\n 'both-edges',\n // width/height\n 'fixed',\n // shadow\n 'inset',\n // textOverflow\n 'clip',\n // other\n 'ltr',\n 'rtl',\n 'embed',\n 'isolate',\n 'isolate-override',\n 'plaintext',\n 'horizontal-tb',\n 'vertical-rl',\n 'vertical-lr',\n 'sideways-rl',\n 'sideways-lr',\n 'monospace',\n 'serif',\n 'sans-serif',\n 'cursive',\n 'fantasy',\n 'system-ui',\n 'ui-serif',\n 'ui-sans-serif',\n 'ui-monospace',\n 'ui-rounded',\n 'to',\n // misc\n 'strong',\n 'tight',\n 'icon',\n]);\n\n// ============================================================================\n// Classifier\n// ============================================================================\n\nfunction toSet(input?: Set<string> | string[]): Set<string> {\n if (!input) return new Set();\n return input instanceof Set ? input : new Set(input);\n}\n\nfunction classifyToken(\n raw: string,\n offset: number,\n errors: ValueError[],\n opts: ValueParserOptions,\n): ValueToken {\n const token = raw.trim();\n if (!token) return { type: 'keyword', value: '' };\n\n // Quoted strings\n if (\n (token.startsWith('\"') && token.endsWith('\"')) ||\n (token.startsWith(\"'\") && token.endsWith(\"'\"))\n ) {\n return { type: 'string', value: token.slice(1, -1), raw: token };\n }\n\n // !important\n if (token === '!important') {\n return { type: 'important', raw: token };\n }\n\n // Double prefix: $$name(...) is a Tasty `@function` call; bare $$name is the\n // custom-property reference `transition` uses. The call form must be checked\n // first, or the bare-name regex rejects `$$negative(10px)` outright.\n if (token.startsWith('$$') || token.startsWith('##')) {\n const call = classifyTastyFunctionCall(token, offset, errors);\n if (call) return call;\n }\n\n // Double prefix: $$name (custom property reference for transitions)\n if (token.startsWith('$$')) {\n const name = token.slice(2);\n if (/^[a-z_][a-z0-9-_]*$/i.test(name)) {\n return { type: 'custom-prop-ref', name, raw: token };\n }\n errors.push({\n message: `Invalid custom property reference '${token}'.`,\n offset,\n length: token.length,\n raw: token,\n });\n return { type: 'unknown', raw: token };\n }\n\n // Double prefix: ##name (color reference for transitions)\n if (token.startsWith('##')) {\n const name = token.slice(2);\n if (/^[a-z_][a-z0-9-_]*$/i.test(name)) {\n return { type: 'color-ref', name, raw: token };\n }\n errors.push({\n message: `Invalid color reference '${token}'.`,\n offset,\n length: token.length,\n raw: token,\n });\n return { type: 'unknown', raw: token };\n }\n\n // Custom property: $name\n if (token.startsWith('$')) {\n const name = token.slice(1);\n if (/^[a-z_][a-z0-9-_]*$/i.test(name)) {\n return { type: 'custom-prop', name, raw: token };\n }\n errors.push({\n message: `Invalid custom property syntax '${token}'.`,\n offset,\n length: token.length,\n raw: token,\n });\n return { type: 'unknown', raw: token };\n }\n\n // Color token: #name, #name.N, #name.$prop, or bare # (error)\n if (token.startsWith('#')) {\n if (token.length === 1) {\n errors.push({\n message: 'Empty color token name.',\n offset,\n length: 1,\n raw: token,\n });\n return { type: 'unknown', raw: token };\n }\n return classifyColorToken(token, offset, errors);\n }\n\n // Parenthesized expression: (expr) — fallback, auto-calc, etc.\n if (token.startsWith('(') && token.endsWith(')')) {\n return { type: 'group-expr', inner: token.slice(1, -1), raw: token };\n }\n\n // Function call: name(...)\n const openIdx = token.indexOf('(');\n if (openIdx > 0 && token.endsWith(')')) {\n const fname = token.slice(0, openIdx);\n const args = token.slice(openIdx + 1, -1);\n\n if (opts.skipFuncValidation) {\n return { type: 'css-function', name: fname, args, raw: token };\n }\n\n const knownFuncs = toSet(opts.knownFuncs);\n\n if (\n COLOR_FUNCS.has(fname) ||\n CSS_FUNCS.has(fname) ||\n knownFuncs.has(fname)\n ) {\n return { type: 'css-function', name: fname, args, raw: token };\n }\n\n // Unknown function — still return as css-function but flag it\n errors.push({\n message: `Unknown function '${fname}()'.`,\n offset,\n length: token.length,\n raw: token,\n });\n return { type: 'css-function', name: fname, args, raw: token };\n }\n\n // Unit number: 2x, 1.5r, 10px, 50%, 1fr\n const unitMatch = token.match(RE_UNIT_NUM);\n if (unitMatch) {\n const unit = unitMatch[1];\n const numVal = parseFloat(token.slice(0, -unit.length));\n\n if (opts.skipUnitValidation) {\n return { type: 'custom-unit', value: numVal, unit, raw: token };\n }\n\n const knownUnits = toSet(opts.knownUnits);\n\n if (BUILT_IN_UNITS.has(unit) || knownUnits.has(unit)) {\n return { type: 'custom-unit', value: numVal, unit, raw: token };\n }\n if (CSS_UNITS.has(unit)) {\n return { type: 'css-unit', value: numVal, unit, raw: token };\n }\n\n errors.push({\n message: `Unknown unit '${unit}' in '${token}'.`,\n offset,\n length: token.length,\n raw: token,\n });\n return { type: 'unknown', raw: token };\n }\n\n // CSS unit with % (RE_UNIT_NUM doesn't match % since it expects alpha)\n const cssUnitMatch = token.match(RE_CSS_UNIT_NUM);\n if (cssUnitMatch) {\n const unit = cssUnitMatch[1];\n const numVal = parseFloat(token.slice(0, -unit.length));\n if (CSS_UNITS.has(unit)) {\n return { type: 'css-unit', value: numVal, unit, raw: token };\n }\n }\n\n // Plain number\n if (RE_NUMBER.test(token)) {\n return { type: 'number', value: parseFloat(token), raw: token };\n }\n\n // Tasty merge directive (@inherit)\n if (token === '@inherit') {\n return { type: 'keyword', value: token };\n }\n\n // Known keyword\n if (VALUE_KEYWORDS.has(token) || VALUE_KEYWORDS.has(token.toLowerCase())) {\n return { type: 'keyword', value: token };\n }\n\n // CSS custom property function var(--name)\n if (token.startsWith('var(') && token.endsWith(')')) {\n return {\n type: 'css-function',\n name: 'var',\n args: token.slice(4, -1),\n raw: token,\n };\n }\n\n // Unknown token\n return { type: 'unknown', raw: token };\n}\n\n/**\n * Classify a `$$name(...)` / `##name(...)` Tasty `@function` call.\n *\n * Returns `null` when the token is not a call, so the caller falls through to the\n * bare `$$name` / `##name` reference handling that `transition` relies on.\n */\nfunction classifyTastyFunctionCall(\n token: string,\n offset: number,\n errors: ValueError[],\n): ValueToken | null {\n const openIdx = token.indexOf('(');\n\n if (openIdx <= 2 || !token.endsWith(')')) return null;\n\n const name = token.slice(2, openIdx);\n const args = token.slice(openIdx + 1, -1);\n\n if (!/^[a-z_][a-z0-9-_]*$/i.test(name)) {\n errors.push({\n message: `Invalid function name in '${token}'.`,\n offset,\n length: token.length,\n raw: token,\n });\n return { type: 'unknown', raw: token };\n }\n\n return { type: 'tasty-function', name, args, raw: token };\n}\n\nfunction classifyColorToken(\n token: string,\n offset: number,\n errors: ValueError[],\n): ValueToken {\n const raw = token;\n\n // Strip leading #\n const name = token.slice(1);\n\n if (name.length === 0) {\n errors.push({\n message: 'Empty color token name.',\n offset,\n length: token.length,\n raw,\n });\n return { type: 'unknown', raw };\n }\n\n // Check for opacity suffix\n const dotIndex = name.indexOf('.');\n if (dotIndex !== -1) {\n const tokenName = name.slice(0, dotIndex);\n const opacitySuffix = name.slice(dotIndex + 1);\n\n if (tokenName.length === 0) {\n errors.push({\n message: 'Empty color token name before opacity.',\n offset,\n length: token.length,\n raw,\n });\n return { type: 'unknown', raw };\n }\n\n // Dynamic opacity from CSS custom property\n if (opacitySuffix.startsWith('$')) {\n return {\n type: 'color-token',\n name: tokenName,\n opacity: opacitySuffix,\n raw,\n };\n }\n\n if (opacitySuffix.length === 0) {\n errors.push({\n message: 'Trailing dot with no opacity value.',\n offset,\n length: token.length,\n raw,\n });\n return { type: 'unknown', raw };\n }\n\n const opacity = Number(opacitySuffix);\n if (isNaN(opacity)) {\n errors.push({\n message: `Invalid opacity value '${opacitySuffix}'.`,\n offset,\n length: token.length,\n raw,\n });\n return { type: 'unknown', raw };\n }\n if (opacity < 0) {\n errors.push({\n message: 'Opacity cannot be negative.',\n offset,\n length: token.length,\n raw,\n });\n return { type: 'unknown', raw };\n }\n if (opacity > 100) {\n errors.push({\n message: `Opacity '${opacitySuffix}' exceeds 100.`,\n offset,\n length: token.length,\n raw,\n });\n return { type: 'unknown', raw };\n }\n\n return {\n type: 'color-token',\n name: tokenName,\n opacity: opacitySuffix,\n raw,\n };\n }\n\n return { type: 'color-token', name, raw };\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Parse a style value string into typed tokens organized by\n * comma-separated groups and slash-separated parts.\n */\nexport function parseValue(\n src: string,\n opts: ValueParserOptions = {},\n): ValueParseResult {\n const errors: ValueError[] = [];\n\n // Check bracket balance first\n const bracketError = checkBracketBalance(src);\n if (bracketError) {\n errors.push({\n message: bracketError.message,\n offset: bracketError.position,\n length: 1,\n });\n return { groups: [], errors };\n }\n\n const scanned = scanTokens(src);\n\n const groups: ValueGroup[] = [];\n let currentParts: ValuePart[] = [];\n let currentTokens: ValueToken[] = [];\n\n const endPart = () => {\n if (currentTokens.length > 0) {\n currentParts.push({ tokens: currentTokens });\n currentTokens = [];\n }\n };\n\n const endGroup = () => {\n endPart();\n if (currentParts.length > 0) {\n groups.push({ parts: currentParts });\n } else {\n groups.push({ parts: [{ tokens: [] }] });\n }\n currentParts = [];\n };\n\n for (const scanned_token of scanned) {\n if (scanned_token.value) {\n const classified = classifyToken(\n scanned_token.value,\n scanned_token.offset,\n errors,\n opts,\n );\n currentTokens.push(classified);\n }\n if (scanned_token.isSlash) endPart();\n if (scanned_token.isComma) endGroup();\n }\n\n // Push final group\n if (\n currentTokens.length > 0 ||\n currentParts.length > 0 ||\n groups.length === 0\n ) {\n endGroup();\n }\n\n return { groups, errors };\n}\n\n/**\n * Extract all tokens of a specific type from a parse result.\n */\nexport function extractTokensByType<T extends ValueToken['type']>(\n result: ValueParseResult,\n type: T,\n): Extract<ValueToken, { type: T }>[] {\n const tokens: Extract<ValueToken, { type: T }>[] = [];\n for (const group of result.groups) {\n for (const part of group.parts) {\n for (const token of part.tokens) {\n if (token.type === type) {\n tokens.push(token as Extract<ValueToken, { type: T }>);\n }\n }\n }\n }\n return tokens;\n}\n\n/**\n * Get a flat list of all tokens from a parse result.\n */\nexport function flattenTokens(result: ValueParseResult): ValueToken[] {\n const tokens: ValueToken[] = [];\n for (const group of result.groups) {\n for (const part of group.parts) {\n tokens.push(...part.tokens);\n }\n }\n return tokens;\n}\n"],"mappings":";;;;AA2DA,MAAM,cAAc;AACpB,MAAM,YAAY;AAClB,MAAM,kBAAkB;AAExB,MAAM,cAAc,IAAI,IAAI;CAC1B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAEF,MAAM,YAAY,IAAI,IAAI;CACxB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAEF,MAAM,iBAAiB,IAAI,IAAI;CAC7B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CAEA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;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"}
@@ -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 runtime-computed values',\n },\n messages: {\n dynamicStyleValue:\n \"Style value for '{{property}}' should be static. Use the 'mods' prop, tokens, or CSS custom properties for dynamic behavior.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkProperties(\n node: TSESTree.ObjectExpression,\n parentProperty: string | null = null,\n ): void {\n for (const prop of node.properties) {\n if (prop.type === 'SpreadElement') {\n context.report({\n node: prop,\n messageId: 'dynamicStyleValue',\n data: { property: parentProperty ?? '(spread)' },\n });\n continue;\n }\n\n if (prop.type !== 'Property') continue;\n\n const key =\n !prop.computed && prop.key.type === 'Identifier'\n ? prop.key.name\n : parentProperty;\n\n // Skip sub-elements (they contain nested style objects)\n if (\n !prop.computed &&\n prop.key.type === 'Identifier' &&\n /^[A-Z]/.test(prop.key.name)\n ) {\n if (prop.value.type === 'ObjectExpression') {\n checkProperties(prop.value, key);\n }\n continue;\n }\n\n // Skip @keyframes and @properties\n if (\n !prop.computed &&\n prop.key.type === 'Identifier' &&\n prop.key.name.startsWith('@')\n ) {\n continue;\n }\n\n if (!isStaticValue(prop.value)) {\n context.report({\n node: prop.value,\n messageId: 'dynamicStyleValue',\n data: { property: key ?? '(unknown)' },\n });\n }\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n checkProperties(node);\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;AAOA,yCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,2DACd;EACD,UAAU,EACR,mBACE,gIACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,gBACP,MACA,iBAAgC,MAC1B;AACN,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,iBAAiB;AACjC,aAAQ,OAAO;MACb,MAAM;MACN,WAAW;MACX,MAAM,EAAE,UAAU,kBAAkB,YAAY;MACjD,CAAC;AACF;;AAGF,QAAI,KAAK,SAAS,WAAY;IAE9B,MAAM,MACJ,CAAC,KAAK,YAAY,KAAK,IAAI,SAAS,eAChC,KAAK,IAAI,OACT;AAGN,QACE,CAAC,KAAK,YACN,KAAK,IAAI,SAAS,gBAClB,SAAS,KAAK,KAAK,IAAI,KAAK,EAC5B;AACA,SAAI,KAAK,MAAM,SAAS,mBACtB,iBAAgB,KAAK,OAAO,IAAI;AAElC;;AAIF,QACE,CAAC,KAAK,YACN,KAAK,IAAI,SAAS,gBAClB,KAAK,IAAI,KAAK,WAAW,IAAI,CAE7B;AAGF,QAAI,CAAC,cAAc,KAAK,MAAM,CAC5B,SAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACX,MAAM,EAAE,UAAU,OAAO,aAAa;KACvC,CAAC;;;EAKR,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAC9B,mBAAgB,KAAK;;AAGvB,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
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 runtime-computed values',\n },\n messages: {\n dynamicStyleValue:\n \"Style value for '{{property}}' should be static. Use the 'mods' prop, tokens, or CSS custom properties for dynamic behavior.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkProperties(\n node: TSESTree.ObjectExpression,\n parentProperty: string | null = null,\n ): void {\n for (const prop of node.properties) {\n if (prop.type === 'SpreadElement') {\n context.report({\n node: prop,\n messageId: 'dynamicStyleValue',\n data: { property: parentProperty ?? '(spread)' },\n });\n continue;\n }\n\n if (prop.type !== 'Property') continue;\n\n const key =\n !prop.computed && prop.key.type === 'Identifier'\n ? prop.key.name\n : parentProperty;\n\n // Skip sub-elements (they contain nested style objects)\n if (\n !prop.computed &&\n prop.key.type === 'Identifier' &&\n /^[A-Z]/.test(prop.key.name)\n ) {\n if (prop.value.type === 'ObjectExpression') {\n checkProperties(prop.value, key);\n }\n continue;\n }\n\n // Skip at-rule blocks (@keyframes, @property, ...)\n if (\n !prop.computed &&\n prop.key.type === 'Identifier' &&\n prop.key.name.startsWith('@')\n ) {\n continue;\n }\n\n if (!isStaticValue(prop.value)) {\n context.report({\n node: prop.value,\n messageId: 'dynamicStyleValue',\n data: { property: key ?? '(unknown)' },\n });\n }\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n checkProperties(node);\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;AAOA,yCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,2DACd;EACD,UAAU,EACR,mBACE,gIACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,gBACP,MACA,iBAAgC,MAC1B;AACN,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,iBAAiB;AACjC,aAAQ,OAAO;MACb,MAAM;MACN,WAAW;MACX,MAAM,EAAE,UAAU,kBAAkB,YAAY;MACjD,CAAC;AACF;;AAGF,QAAI,KAAK,SAAS,WAAY;IAE9B,MAAM,MACJ,CAAC,KAAK,YAAY,KAAK,IAAI,SAAS,eAChC,KAAK,IAAI,OACT;AAGN,QACE,CAAC,KAAK,YACN,KAAK,IAAI,SAAS,gBAClB,SAAS,KAAK,KAAK,IAAI,KAAK,EAC5B;AACA,SAAI,KAAK,MAAM,SAAS,mBACtB,iBAAgB,KAAK,OAAO,IAAI;AAElC;;AAIF,QACE,CAAC,KAAK,YACN,KAAK,IAAI,SAAS,gBAClB,KAAK,IAAI,KAAK,WAAW,IAAI,CAE7B;AAGF,QAAI,CAAC,cAAc,KAAK,MAAM,CAC5B,SAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACX,MAAM,EAAE,UAAU,OAAO,aAAa;KACvC,CAAC;;;EAKR,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAC9B,mBAAgB,KAAK;;AAGvB,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
@@ -1,11 +1,12 @@
1
1
  import { createRule } from "../create-rule.js";
2
+ import { PROPERTIES_WITHOUT_COLOR_TOKEN_EXPANSION, PROPERTIES_WITHOUT_CUSTOM_PROPERTY_EXPANSION } from "../constants.js";
2
3
  import { getKeyName, getStringValue } from "../utils.js";
3
4
  import { TastyContext, styleObjectListeners } from "../context.js";
4
5
  import { replaceInStringValue } from "../fix-utils.js";
5
6
 
6
7
  //#region src/rules/prefer-custom-property-syntax.ts
7
8
  const VAR_REGEX = /var\(\s*--([a-zA-Z0-9_-]+)\s*(?:,\s*([^)]+))?\s*\)/g;
8
- const COLOR_PROP_REGEX = /(?<![$#])\$([a-zA-Z][a-zA-Z0-9_-]*)-color\b(\.[0-9]+|\.\$[a-zA-Z][a-zA-Z0-9_-]*)?/g;
9
+ const COLOR_PROP_REGEX = /(?<![$#])\$([a-zA-Z][a-zA-Z0-9_-]*)-color(?![a-zA-Z0-9_-])(\.[0-9]+|\.\$[a-zA-Z][a-zA-Z0-9_-]*)?/g;
9
10
  const KEYWORD_REGEX = /\b(transparent|currentColor)\b/gi;
10
11
  function normalizeFallback(fallback) {
11
12
  const trimmed = fallback.trim();
@@ -45,7 +46,22 @@ var prefer_custom_property_syntax_default = createRule({
45
46
  defaultOptions: [],
46
47
  create(context) {
47
48
  const ctx = new TastyContext(context);
48
- function checkValue(value, node) {
49
+ /**
50
+ * Whether rewriting to `suggestion` actually round-trips for `property`.
51
+ *
52
+ * Every rewrite this rule offers has to render to the same CSS as what it
53
+ * replaces. Tasty expands `$name` and `#name` per property, not globally, so
54
+ * a suggestion that is correct on `gap` can silently delete the declaration on
55
+ * `fontFamily`. Suppress the report entirely rather than offer a broken fix —
56
+ * the rule is autofixable and runs unattended in pre-commit hooks.
57
+ */
58
+ function roundTrips(property, suggestion) {
59
+ if (suggestion.startsWith("$")) return !PROPERTIES_WITHOUT_CUSTOM_PROPERTY_EXPANSION.has(property);
60
+ if (suggestion.startsWith("#")) return !PROPERTIES_WITHOUT_COLOR_TOKEN_EXPANSION.has(property);
61
+ if (suggestion.startsWith("(")) return roundTrips(property, suggestion.slice(1));
62
+ return true;
63
+ }
64
+ function checkValue(value, node, property) {
49
65
  const varSpans = [];
50
66
  VAR_REGEX.lastIndex = 0;
51
67
  let match;
@@ -56,6 +72,7 @@ var prefer_custom_property_syntax_default = createRule({
56
72
  const suggestion = suggestVarSyntax(name, fallback);
57
73
  const start = match.index;
58
74
  const end = start + raw.length;
75
+ if (!roundTrips(property, suggestion)) continue;
59
76
  if (match.index !== void 0) varSpans.push({
60
77
  start: match.index,
61
78
  end: match.index + raw.length
@@ -82,6 +99,7 @@ var prefer_custom_property_syntax_default = createRule({
82
99
  const suggestion = `#${match[1]}${match[2] ?? ""}`;
83
100
  const start = match.index;
84
101
  const end = start + raw.length;
102
+ if (!roundTrips(property, suggestion)) continue;
85
103
  context.report({
86
104
  node,
87
105
  messageId: "preferColorToken",
@@ -105,6 +123,7 @@ var prefer_custom_property_syntax_default = createRule({
105
123
  if (isInsideSpans(start, end, varSpans)) continue;
106
124
  const lower = match[0].toLowerCase();
107
125
  const replacement = lower === "transparent" ? "#clear" : "#current";
126
+ if (!roundTrips(property, replacement)) continue;
108
127
  context.report({
109
128
  node,
110
129
  messageId: lower === "transparent" ? "preferClearToken" : "preferCurrentToken",
@@ -127,13 +146,13 @@ var prefer_custom_property_syntax_default = createRule({
127
146
  if (/^[A-Z@&$#]/.test(key)) continue;
128
147
  const str = getStringValue(prop.value);
129
148
  if (str) {
130
- checkValue(str, prop.value);
149
+ checkValue(str, prop.value, key);
131
150
  continue;
132
151
  }
133
152
  if (prop.value.type === "ObjectExpression") for (const stateProp of prop.value.properties) {
134
153
  if (stateProp.type !== "Property") continue;
135
154
  const stateStr = getStringValue(stateProp.value);
136
- if (stateStr) checkValue(stateStr, stateProp.value);
155
+ if (stateStr) checkValue(stateStr, stateProp.value, key);
137
156
  }
138
157
  }
139
158
  }
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-custom-property-syntax.js","names":[],"sources":["../../src/rules/prefer-custom-property-syntax.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 { replaceInStringValue } from '../fix-utils.js';\n\ntype MessageIds =\n | 'preferCustomPropertySyntax'\n | 'preferColorToken'\n | 'preferClearToken'\n | 'preferCurrentToken';\n\nconst VAR_REGEX = /var\\(\\s*--([a-zA-Z0-9_-]+)\\s*(?:,\\s*([^)]+))?\\s*\\)/g;\n// `$x-color` → `#x`. Negative lookbehind avoids `$$func` / `##transition`.\n// `\\b` after `-color` avoids matching `$x-colorful`. Optional opacity suffix.\nconst COLOR_PROP_REGEX =\n /(?<![$#])\\$([a-zA-Z][a-zA-Z0-9_-]*)-color\\b(\\.[0-9]+|\\.\\$[a-zA-Z][a-zA-Z0-9_-]*)?/g;\nconst KEYWORD_REGEX = /\\b(transparent|currentColor)\\b/gi;\n\nfunction normalizeFallback(fallback: string): string {\n const trimmed = fallback.trim();\n const lower = trimmed.toLowerCase();\n if (lower === 'transparent') return '#clear';\n if (lower === 'currentcolor') return '#current';\n return trimmed;\n}\n\nfunction suggestVarSyntax(name: string, fallback?: string): string {\n if (name.endsWith('-color')) {\n const tokenName = name.slice(0, -'-color'.length);\n const tastyToken = `#${tokenName}`;\n if (fallback) {\n return `(${tastyToken}, ${normalizeFallback(fallback)})`;\n }\n return tastyToken;\n }\n\n const propToken = `$${name}`;\n if (fallback) {\n return `(${propToken}, ${normalizeFallback(fallback)})`;\n }\n return propToken;\n}\n\ninterface Span {\n start: number;\n end: number;\n}\n\nfunction isInsideSpans(start: number, end: number, spans: Span[]): boolean {\n for (const span of spans) {\n if (start >= span.start && end <= span.end) return true;\n }\n return false;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-custom-property-syntax',\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description:\n 'Prefer Tasty token syntax ($prop, #color, #clear, #current) over raw CSS (var(), $x-color, transparent, currentColor)',\n },\n messages: {\n preferCustomPropertySyntax:\n \"Use Tasty token syntax '{{suggestion}}' instead of '{{raw}}'.\",\n preferColorToken:\n \"Use color token '{{suggestion}}' instead of custom property reference '{{raw}}'.\",\n preferClearToken:\n \"Use the '#clear' token instead of the 'transparent' keyword.\",\n preferCurrentToken:\n \"Use the '#current' token instead of the 'currentColor' keyword.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkValue(value: string, node: TSESTree.Node): void {\n // Pass 1: var(--x) / var(--x, fallback) → $x / (#x, fallback).\n // Records spans so standalone keywords inside var() aren't double-reported.\n const varSpans: Span[] = [];\n VAR_REGEX.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = VAR_REGEX.exec(value)) !== null) {\n const raw = match[0];\n const name = match[1];\n const fallback = match[2];\n const suggestion = suggestVarSyntax(name, fallback);\n const start = match.index;\n const end = start + raw.length;\n\n if (match.index !== undefined) {\n varSpans.push({\n start: match.index,\n end: match.index + raw.length,\n });\n }\n\n context.report({\n node,\n messageId: 'preferCustomPropertySyntax',\n data: { raw, suggestion },\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement: suggestion }],\n context.sourceCode,\n );\n },\n });\n }\n\n // Pass 2: $x-color → #x.\n COLOR_PROP_REGEX.lastIndex = 0;\n while ((match = COLOR_PROP_REGEX.exec(value)) !== null) {\n const raw = match[0];\n const name = match[1];\n const opacity = match[2] ?? '';\n const suggestion = `#${name}${opacity}`;\n const start = match.index;\n const end = start + raw.length;\n\n context.report({\n node,\n messageId: 'preferColorToken',\n data: { raw, suggestion },\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement: suggestion }],\n context.sourceCode,\n );\n },\n });\n }\n\n // Pass 3: standalone transparent / currentColor (skip inside var()).\n KEYWORD_REGEX.lastIndex = 0;\n while ((match = KEYWORD_REGEX.exec(value)) !== null) {\n const start = match.index;\n const end = start + match[0].length;\n if (isInsideSpans(start, end, varSpans)) continue;\n\n const lower = match[0].toLowerCase();\n const replacement = lower === 'transparent' ? '#clear' : '#current';\n context.report({\n node,\n messageId:\n lower === 'transparent' ? 'preferClearToken' : 'preferCurrentToken',\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement }],\n context.sourceCode,\n );\n },\n });\n }\n }\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\n const key = getKeyName(prop.key);\n if (key === null) continue;\n if (/^[A-Z@&$#]/.test(key)) continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkValue(str, prop.value);\n continue;\n }\n\n if (prop.value.type === 'ObjectExpression') {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr) {\n checkValue(stateStr, stateProp.value);\n }\n }\n }\n }\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
+ {"version":3,"file":"prefer-custom-property-syntax.js","names":[],"sources":["../../src/rules/prefer-custom-property-syntax.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport {\n PROPERTIES_WITHOUT_COLOR_TOKEN_EXPANSION,\n PROPERTIES_WITHOUT_CUSTOM_PROPERTY_EXPANSION,\n} from '../constants.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { replaceInStringValue } from '../fix-utils.js';\n\ntype MessageIds =\n | 'preferCustomPropertySyntax'\n | 'preferColorToken'\n | 'preferClearToken'\n | 'preferCurrentToken';\n\nconst VAR_REGEX = /var\\(\\s*--([a-zA-Z0-9_-]+)\\s*(?:,\\s*([^)]+))?\\s*\\)/g;\n// `$x-color` → `#x`. Negative lookbehind avoids `$$func` / `##transition`.\n// Optional opacity suffix.\n//\n// `-color` must END the identifier, hence the lookahead rather than `\\b`. `\\b`\n// matches between `color` and a following `-`, so `$purple-color-rgb` matched as\n// `$purple-color` and was rewritten to `#purple-rgb` — a property that does not\n// exist. That bit in practice: `var(--purple-color-rgb)` became `$purple-color-rgb`\n// on one --fix pass and `#purple-rgb` on the next, and the declaration was dropped\n// at computed-value time with no error anywhere.\nconst COLOR_PROP_REGEX =\n /(?<![$#])\\$([a-zA-Z][a-zA-Z0-9_-]*)-color(?![a-zA-Z0-9_-])(\\.[0-9]+|\\.\\$[a-zA-Z][a-zA-Z0-9_-]*)?/g;\nconst KEYWORD_REGEX = /\\b(transparent|currentColor)\\b/gi;\n\nfunction normalizeFallback(fallback: string): string {\n const trimmed = fallback.trim();\n const lower = trimmed.toLowerCase();\n if (lower === 'transparent') return '#clear';\n if (lower === 'currentcolor') return '#current';\n return trimmed;\n}\n\nfunction suggestVarSyntax(name: string, fallback?: string): string {\n if (name.endsWith('-color')) {\n const tokenName = name.slice(0, -'-color'.length);\n const tastyToken = `#${tokenName}`;\n if (fallback) {\n return `(${tastyToken}, ${normalizeFallback(fallback)})`;\n }\n return tastyToken;\n }\n\n const propToken = `$${name}`;\n if (fallback) {\n return `(${propToken}, ${normalizeFallback(fallback)})`;\n }\n return propToken;\n}\n\ninterface Span {\n start: number;\n end: number;\n}\n\nfunction isInsideSpans(start: number, end: number, spans: Span[]): boolean {\n for (const span of spans) {\n if (start >= span.start && end <= span.end) return true;\n }\n return false;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-custom-property-syntax',\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description:\n 'Prefer Tasty token syntax ($prop, #color, #clear, #current) over raw CSS (var(), $x-color, transparent, currentColor)',\n },\n messages: {\n preferCustomPropertySyntax:\n \"Use Tasty token syntax '{{suggestion}}' instead of '{{raw}}'.\",\n preferColorToken:\n \"Use color token '{{suggestion}}' instead of custom property reference '{{raw}}'.\",\n preferClearToken:\n \"Use the '#clear' token instead of the 'transparent' keyword.\",\n preferCurrentToken:\n \"Use the '#current' token instead of the 'currentColor' keyword.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n /**\n * Whether rewriting to `suggestion` actually round-trips for `property`.\n *\n * Every rewrite this rule offers has to render to the same CSS as what it\n * replaces. Tasty expands `$name` and `#name` per property, not globally, so\n * a suggestion that is correct on `gap` can silently delete the declaration on\n * `fontFamily`. Suppress the report entirely rather than offer a broken fix —\n * the rule is autofixable and runs unattended in pre-commit hooks.\n */\n function roundTrips(property: string, suggestion: string): boolean {\n if (suggestion.startsWith('$')) {\n return !PROPERTIES_WITHOUT_CUSTOM_PROPERTY_EXPANSION.has(property);\n }\n\n if (suggestion.startsWith('#')) {\n return !PROPERTIES_WITHOUT_COLOR_TOKEN_EXPANSION.has(property);\n }\n\n // `(token, fallback)` auto-calc form — the token inside decides.\n if (suggestion.startsWith('(')) {\n const inner = suggestion.slice(1);\n\n return roundTrips(property, inner);\n }\n\n return true;\n }\n\n function checkValue(\n value: string,\n node: TSESTree.Node,\n property: string,\n ): void {\n // Pass 1: var(--x) / var(--x, fallback) → $x / (#x, fallback).\n // Records spans so standalone keywords inside var() aren't double-reported.\n const varSpans: Span[] = [];\n VAR_REGEX.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = VAR_REGEX.exec(value)) !== null) {\n const raw = match[0];\n const name = match[1];\n const fallback = match[2];\n const suggestion = suggestVarSyntax(name, fallback);\n const start = match.index;\n const end = start + raw.length;\n\n if (!roundTrips(property, suggestion)) continue;\n\n if (match.index !== undefined) {\n varSpans.push({\n start: match.index,\n end: match.index + raw.length,\n });\n }\n\n context.report({\n node,\n messageId: 'preferCustomPropertySyntax',\n data: { raw, suggestion },\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement: suggestion }],\n context.sourceCode,\n );\n },\n });\n }\n\n // Pass 2: $x-color → #x.\n COLOR_PROP_REGEX.lastIndex = 0;\n while ((match = COLOR_PROP_REGEX.exec(value)) !== null) {\n const raw = match[0];\n const name = match[1];\n const opacity = match[2] ?? '';\n const suggestion = `#${name}${opacity}`;\n const start = match.index;\n const end = start + raw.length;\n\n if (!roundTrips(property, suggestion)) continue;\n\n context.report({\n node,\n messageId: 'preferColorToken',\n data: { raw, suggestion },\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement: suggestion }],\n context.sourceCode,\n );\n },\n });\n }\n\n // Pass 3: standalone transparent / currentColor (skip inside var()).\n KEYWORD_REGEX.lastIndex = 0;\n while ((match = KEYWORD_REGEX.exec(value)) !== null) {\n const start = match.index;\n const end = start + match[0].length;\n if (isInsideSpans(start, end, varSpans)) continue;\n\n const lower = match[0].toLowerCase();\n const replacement = lower === 'transparent' ? '#clear' : '#current';\n\n if (!roundTrips(property, replacement)) continue;\n\n context.report({\n node,\n messageId:\n lower === 'transparent' ? 'preferClearToken' : 'preferCurrentToken',\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement }],\n context.sourceCode,\n );\n },\n });\n }\n }\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\n const key = getKeyName(prop.key);\n if (key === null) continue;\n if (/^[A-Z@&$#]/.test(key)) continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkValue(str, prop.value, key);\n continue;\n }\n\n if (prop.value.type === 'ObjectExpression') {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr) {\n // State maps keep the outer property's expansion rules.\n checkValue(stateStr, stateProp.value, key);\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":"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"}
@@ -27,7 +27,7 @@ var valid_color_token_default = createRule({
27
27
  }
28
28
  }
29
29
  function checkColorTokensInValue(value, node) {
30
- const tokenRegex = /##?[a-zA-Z][a-zA-Z0-9-]*(?:\.\$?[a-zA-Z0-9-]+)?/g;
30
+ const tokenRegex = /##?[a-zA-Z][a-zA-Z0-9-]*(?:\.\$?[a-zA-Z0-9-]+)?(?!\()/g;
31
31
  let match;
32
32
  while ((match = tokenRegex.exec(value)) !== null) {
33
33
  const token = match[0];
@@ -1 +1 @@
1
- {"version":3,"file":"valid-color-token.js","names":[],"sources":["../../src/rules/valid-color-token.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport {\n getKeyName,\n getStringValue,\n validateColorTokenSyntax,\n isRawHexColor,\n} from '../utils.js';\n\ntype MessageIds = 'invalidSyntax' | 'unknownToken';\n\ninterface PendingExistenceCheck {\n token: string;\n baseName: string;\n node: TSESTree.Node;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'valid-color-token',\n meta: {\n type: 'problem',\n docs: {\n description: 'Validate color token syntax and existence',\n },\n messages: {\n invalidSyntax: \"Invalid color token '{{token}}': {{reason}}.\",\n unknownToken:\n \"Unknown color token '{{token}}'. Declare it as a '#name' key, or add it to 'tokens' in your tasty config.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n const fileColorTokens = new Set<string>();\n const pendingChecks: PendingExistenceCheck[] = [];\n\n function collectLocalTokens(node: TSESTree.ObjectExpression): void {\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n const key = getKeyName(prop.key);\n if (key && key.startsWith('#') && !key.startsWith('##')) {\n fileColorTokens.add(key);\n }\n }\n }\n\n function checkColorTokensInValue(value: string, node: TSESTree.Node): void {\n const tokenRegex = /##?[a-zA-Z][a-zA-Z0-9-]*(?:\\.\\$?[a-zA-Z0-9-]+)?/g;\n let match;\n\n while ((match = tokenRegex.exec(value)) !== null) {\n const token = match[0];\n\n if (isRawHexColor(token)) continue;\n\n const syntaxError = validateColorTokenSyntax(token);\n if (syntaxError) {\n context.report({\n node,\n messageId: 'invalidSyntax',\n data: { token, reason: syntaxError },\n });\n continue;\n }\n\n if (ctx.config.tokens === false) continue;\n\n const baseName = token.startsWith('##')\n ? '#' + token.slice(2).split('.')[0]\n : '#' + token.slice(1).split('.')[0];\n\n if (baseName === '#current') continue;\n\n pendingChecks.push({ token, baseName, node });\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n collectLocalTokens(node);\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property') continue;\n\n if (prop.value.type === 'Literal') {\n const str = getStringValue(prop.value);\n if (str && str.includes('#')) {\n checkColorTokensInValue(str, prop.value);\n }\n }\n\n if (prop.value.type === 'ObjectExpression') {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const str = getStringValue(stateProp.value);\n if (str && str.includes('#')) {\n checkColorTokensInValue(str, stateProp.value);\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n ...styleObjectListeners(handleStyleObject),\n\n 'Program:exit'() {\n if (\n !Array.isArray(ctx.config.tokens) ||\n ctx.config.tokens.length === 0\n ) {\n return;\n }\n\n for (const { token, baseName, node } of pendingChecks) {\n if (fileColorTokens.has(baseName)) continue;\n if (ctx.config.tokens.includes(baseName)) continue;\n\n context.report({\n node,\n messageId: 'unknownToken',\n data: { token },\n });\n }\n },\n };\n },\n});\n"],"mappings":";;;;;AAkBA,gCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,6CACd;EACD,UAAU;GACR,eAAe;GACf,cACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EACrC,MAAM,kCAAkB,IAAI,KAAa;EACzC,MAAM,gBAAyC,EAAE;EAEjD,SAAS,mBAAmB,MAAuC;AACjE,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAC/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,OAAO,IAAI,WAAW,IAAI,IAAI,CAAC,IAAI,WAAW,KAAK,CACrD,iBAAgB,IAAI,IAAI;;;EAK9B,SAAS,wBAAwB,OAAe,MAA2B;GACzE,MAAM,aAAa;GACnB,IAAI;AAEJ,WAAQ,QAAQ,WAAW,KAAK,MAAM,MAAM,MAAM;IAChD,MAAM,QAAQ,MAAM;AAEpB,QAAI,cAAc,MAAM,CAAE;IAE1B,MAAM,cAAc,yBAAyB,MAAM;AACnD,QAAI,aAAa;AACf,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM;OAAE;OAAO,QAAQ;OAAa;MACrC,CAAC;AACF;;AAGF,QAAI,IAAI,OAAO,WAAW,MAAO;IAEjC,MAAM,WAAW,MAAM,WAAW,KAAK,GACnC,MAAM,MAAM,MAAM,EAAE,CAAC,MAAM,IAAI,CAAC,KAChC,MAAM,MAAM,MAAM,EAAE,CAAC,MAAM,IAAI,CAAC;AAEpC,QAAI,aAAa,WAAY;AAE7B,kBAAc,KAAK;KAAE;KAAO;KAAU;KAAM,CAAC;;;EAIjD,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAC9B,sBAAmB,KAAK;AAExB,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,WAAY;AAE9B,QAAI,KAAK,MAAM,SAAS,WAAW;KACjC,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,SAAI,OAAO,IAAI,SAAS,IAAI,CAC1B,yBAAwB,KAAK,KAAK,MAAM;;AAI5C,QAAI,KAAK,MAAM,SAAS,mBACtB,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,MAAM,eAAe,UAAU,MAAM;AAC3C,SAAI,OAAO,IAAI,SAAS,IAAI,CAC1B,yBAAwB,KAAK,UAAU,MAAM;;;;AAOvD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,GAAG,qBAAqB,kBAAkB;GAE1C,iBAAiB;AACf,QACE,CAAC,MAAM,QAAQ,IAAI,OAAO,OAAO,IACjC,IAAI,OAAO,OAAO,WAAW,EAE7B;AAGF,SAAK,MAAM,EAAE,OAAO,UAAU,UAAU,eAAe;AACrD,SAAI,gBAAgB,IAAI,SAAS,CAAE;AACnC,SAAI,IAAI,OAAO,OAAO,SAAS,SAAS,CAAE;AAE1C,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM,EAAE,OAAO;MAChB,CAAC;;;GAGP;;CAEJ,CAAC"}
1
+ {"version":3,"file":"valid-color-token.js","names":[],"sources":["../../src/rules/valid-color-token.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport {\n getKeyName,\n getStringValue,\n validateColorTokenSyntax,\n isRawHexColor,\n} from '../utils.js';\n\ntype MessageIds = 'invalidSyntax' | 'unknownToken';\n\ninterface PendingExistenceCheck {\n token: string;\n baseName: string;\n node: TSESTree.Node;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'valid-color-token',\n meta: {\n type: 'problem',\n docs: {\n description: 'Validate color token syntax and existence',\n },\n messages: {\n invalidSyntax: \"Invalid color token '{{token}}': {{reason}}.\",\n unknownToken:\n \"Unknown color token '{{token}}'. Declare it as a '#name' key, or add it to 'tokens' in your tasty config.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n const fileColorTokens = new Set<string>();\n const pendingChecks: PendingExistenceCheck[] = [];\n\n function collectLocalTokens(node: TSESTree.ObjectExpression): void {\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n const key = getKeyName(prop.key);\n if (key && key.startsWith('#') && !key.startsWith('##')) {\n fileColorTokens.add(key);\n }\n }\n }\n\n function checkColorTokensInValue(value: string, node: TSESTree.Node): void {\n // `(?!\\()` keeps a `##name(...)` Tasty `@function` call from being read as a\n // bare color reference, which would demand the token be declared.\n const tokenRegex =\n /##?[a-zA-Z][a-zA-Z0-9-]*(?:\\.\\$?[a-zA-Z0-9-]+)?(?!\\()/g;\n let match;\n\n while ((match = tokenRegex.exec(value)) !== null) {\n const token = match[0];\n\n if (isRawHexColor(token)) continue;\n\n const syntaxError = validateColorTokenSyntax(token);\n if (syntaxError) {\n context.report({\n node,\n messageId: 'invalidSyntax',\n data: { token, reason: syntaxError },\n });\n continue;\n }\n\n if (ctx.config.tokens === false) continue;\n\n const baseName = token.startsWith('##')\n ? '#' + token.slice(2).split('.')[0]\n : '#' + token.slice(1).split('.')[0];\n\n if (baseName === '#current') continue;\n\n pendingChecks.push({ token, baseName, node });\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n collectLocalTokens(node);\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property') continue;\n\n if (prop.value.type === 'Literal') {\n const str = getStringValue(prop.value);\n if (str && str.includes('#')) {\n checkColorTokensInValue(str, prop.value);\n }\n }\n\n if (prop.value.type === 'ObjectExpression') {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const str = getStringValue(stateProp.value);\n if (str && str.includes('#')) {\n checkColorTokensInValue(str, stateProp.value);\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n ...styleObjectListeners(handleStyleObject),\n\n 'Program:exit'() {\n if (\n !Array.isArray(ctx.config.tokens) ||\n ctx.config.tokens.length === 0\n ) {\n return;\n }\n\n for (const { token, baseName, node } of pendingChecks) {\n if (fileColorTokens.has(baseName)) continue;\n if (ctx.config.tokens.includes(baseName)) continue;\n\n context.report({\n node,\n messageId: 'unknownToken',\n data: { token },\n });\n }\n },\n };\n },\n});\n"],"mappings":";;;;;AAkBA,gCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aAAa,6CACd;EACD,UAAU;GACR,eAAe;GACf,cACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EACrC,MAAM,kCAAkB,IAAI,KAAa;EACzC,MAAM,gBAAyC,EAAE;EAEjD,SAAS,mBAAmB,MAAuC;AACjE,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAC/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,OAAO,IAAI,WAAW,IAAI,IAAI,CAAC,IAAI,WAAW,KAAK,CACrD,iBAAgB,IAAI,IAAI;;;EAK9B,SAAS,wBAAwB,OAAe,MAA2B;GAGzE,MAAM,aACJ;GACF,IAAI;AAEJ,WAAQ,QAAQ,WAAW,KAAK,MAAM,MAAM,MAAM;IAChD,MAAM,QAAQ,MAAM;AAEpB,QAAI,cAAc,MAAM,CAAE;IAE1B,MAAM,cAAc,yBAAyB,MAAM;AACnD,QAAI,aAAa;AACf,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM;OAAE;OAAO,QAAQ;OAAa;MACrC,CAAC;AACF;;AAGF,QAAI,IAAI,OAAO,WAAW,MAAO;IAEjC,MAAM,WAAW,MAAM,WAAW,KAAK,GACnC,MAAM,MAAM,MAAM,EAAE,CAAC,MAAM,IAAI,CAAC,KAChC,MAAM,MAAM,MAAM,EAAE,CAAC,MAAM,IAAI,CAAC;AAEpC,QAAI,aAAa,WAAY;AAE7B,kBAAc,KAAK;KAAE;KAAO;KAAU;KAAM,CAAC;;;EAIjD,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAC9B,sBAAmB,KAAK;AAExB,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,WAAY;AAE9B,QAAI,KAAK,MAAM,SAAS,WAAW;KACjC,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,SAAI,OAAO,IAAI,SAAS,IAAI,CAC1B,yBAAwB,KAAK,KAAK,MAAM;;AAI5C,QAAI,KAAK,MAAM,SAAS,mBACtB,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,MAAM,eAAe,UAAU,MAAM;AAC3C,SAAI,OAAO,IAAI,SAAS,IAAI,CAC1B,yBAAwB,KAAK,UAAU,MAAM;;;;AAOvD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,GAAG,qBAAqB,kBAAkB;GAE1C,iBAAiB;AACf,QACE,CAAC,MAAM,QAAQ,IAAI,OAAO,OAAO,IACjC,IAAI,OAAO,OAAO,WAAW,EAE7B;AAGF,SAAK,MAAM,EAAE,OAAO,UAAU,UAAU,eAAe;AACrD,SAAI,gBAAgB,IAAI,SAAS,CAAE;AACnC,SAAI,IAAI,OAAO,OAAO,SAAS,SAAS,CAAE;AAE1C,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM,EAAE,OAAO;MAChB,CAAC;;;GAGP;;CAEJ,CAAC"}
@@ -3,7 +3,7 @@ import { getKeyName, getStringValue } from "../utils.js";
3
3
  import { TastyContext, styleObjectListeners } from "../context.js";
4
4
 
5
5
  //#region src/rules/valid-custom-property.ts
6
- const CUSTOM_PROP_REGEX = /\$\$?[a-zA-Z][a-zA-Z0-9-]*/g;
6
+ const CUSTOM_PROP_REGEX = /\$\$?[a-zA-Z][a-zA-Z0-9-]*(?!\()/g;
7
7
  var valid_custom_property_default = createRule({
8
8
  name: "valid-custom-property",
9
9
  meta: {