@tenphi/eslint-plugin-tasty 1.13.1 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/config.js +32 -1
- package/dist/config.js.map +1 -1
- package/dist/configs.js +1 -0
- package/dist/configs.js.map +1 -1
- package/dist/constants.js +5 -8
- package/dist/constants.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/parsers/value-parser.js +39 -1
- package/dist/parsers/value-parser.js.map +1 -1
- package/dist/rules/prefer-fill-mix.js +279 -0
- package/dist/rules/prefer-fill-mix.js.map +1 -0
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"value-parser.js","names":[],"sources":["../../src/parsers/value-parser.ts"],"sourcesContent":["import { scanTokens, checkBracketBalance } from './utils.js';\nimport {\n BUILT_IN_UNITS,\n CSS_UNITS,\n NAMED_CSS_COLORS,\n SYSTEM_COLORS,\n} 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 /** A CSS named color (`rebeccapurple`) or system color (`Canvas`). */\n | { type: 'color-keyword'; 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 /** An unspaced `<ratio>`, such as `16/10`; the spaced form is two parts. */\n | { type: 'ratio'; 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/** An unspaced `<ratio>`, such as `16/10`. */\nconst RE_RATIO = /^(?:\\d*\\.\\d+|\\d+)\\/(?:\\d*\\.\\d+|\\d+)$/;\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 // Unspaced ratio: 16/10\n if (RE_RATIO.test(token)) {\n return { type: 'ratio', 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 // Color keyword: no token syntax, so the keyword itself is the color.\n // Matched case-insensitively, as CSS and Tasty match them.\n const lower = token.toLowerCase();\n if (NAMED_CSS_COLORS.has(lower) || SYSTEM_COLORS.has(lower)) {\n return { type: 'color-keyword', raw: 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 // Tasty reads the digits as a decimal fraction (`.5` and `.50` are 50%,\n // `.150` is 15%), so any digit string is in range.\n if (!/^\\d+$/.test(opacitySuffix)) {\n errors.push({\n message: /^-\\d+$/.test(opacitySuffix)\n ? 'Opacity cannot be negative.'\n : `Invalid opacity value '${opacitySuffix}'.`,\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":";;;;AAoEA,MAAM,cAAc;AACpB,MAAM,YAAY;AAClB,MAAM,kBAAkB;;AAExB,MAAM,WAAW;AAEjB,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;CAC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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 {\n BUILT_IN_UNITS,\n CSS_UNITS,\n NAMED_CSS_COLORS,\n SYSTEM_COLORS,\n} 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 /** A CSS named color (`rebeccapurple`) or system color (`Canvas`). */\n | { type: 'color-keyword'; 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 /** An unspaced `<ratio>`, such as `16/10`; the spaced form is two parts. */\n | { type: 'ratio'; 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/** An unspaced `<ratio>`, such as `16/10`. */\nconst RE_RATIO = /^(?:\\d*\\.\\d+|\\d+)\\/(?:\\d*\\.\\d+|\\d+)$/;\n\n/** Functions the parser classifies as colors. */\nexport const 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 if (token.includes('+')) {\n return classifyCompoundColor(token, offset, errors);\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 // Unspaced ratio: 16/10\n if (RE_RATIO.test(token)) {\n return { type: 'ratio', 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 // Color keyword: no token syntax, so the keyword itself is the color.\n // Matched case-insensitively, as CSS and Tasty match them.\n const lower = token.toLowerCase();\n if (NAMED_CSS_COLORS.has(lower) || SYSTEM_COLORS.has(lower)) {\n return { type: 'color-keyword', raw: 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 // Tasty reads the digits as a decimal fraction (`.5` and `.50` are 50%,\n // `.150` is 15%), so any digit string is in range.\n if (!/^\\d+$/.test(opacitySuffix)) {\n errors.push({\n message: /^-\\d+$/.test(opacitySuffix)\n ? 'Opacity cannot be negative.'\n : `Invalid opacity value '${opacitySuffix}'.`,\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/** One layer of a compound color: a `#name`, before any opacity suffix. */\nconst RE_COMPOUND_LAYER = /^#[a-z0-9-]+(?:\\.|$)/i;\n\n/**\n * Classify a compound color, `#surface+#dark.04`: color tokens joined by `+`,\n * each with its own opacity, painted left to right. Tasty drops the whole value\n * when a part is not a color token, so that is an error. The token's name is\n * the compound as written, and it has no opacity of its own.\n */\nfunction classifyCompoundColor(\n token: string,\n offset: number,\n errors: ValueError[],\n): ValueToken {\n const raw = token;\n let layerOffset = offset;\n\n for (const layer of token.split('+')) {\n if (!RE_COMPOUND_LAYER.test(layer)) {\n errors.push({\n message: `'${raw}' is not a compound color: every part joined by '+' must be a color token, such as '#surface+#dark.04'.`,\n offset,\n length: raw.length,\n raw,\n });\n return { type: 'unknown', raw };\n }\n\n if (classifyColorToken(layer, layerOffset, errors).type !== 'color-token') {\n return { type: 'unknown', raw };\n }\n layerOffset += layer.length + 1;\n }\n\n return { type: 'color-token', name: token.slice(1), 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":";;;;AAoEA,MAAM,cAAc;AACpB,MAAM,YAAY;AAClB,MAAM,kBAAkB;;AAExB,MAAM,WAAW;;AAGjB,MAAa,cAAc,IAAI,IAAI;CACjC;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;CA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"}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { createRule } from "../create-rule.js";
|
|
2
|
+
import { getInstalledTastyVersion } from "../config.js";
|
|
3
|
+
import { TastyContext } from "../context.js";
|
|
4
|
+
import { stringStyleValueListeners } from "../style-values.js";
|
|
5
|
+
import { splitTopLevelCommas } from "../parsers/utils.js";
|
|
6
|
+
import { COLOR_FUNCS, parseValue } from "../parsers/value-parser.js";
|
|
7
|
+
import { replaceStringValue } from "../fix-utils.js";
|
|
8
|
+
import semver from "semver";
|
|
9
|
+
|
|
10
|
+
//#region src/rules/prefer-fill-mix.ts
|
|
11
|
+
/** The first `@tenphi/tasty` release that mixes comma-separated fill colors. */
|
|
12
|
+
const FILL_MIX_TASTY_VERSION = "3.15.0";
|
|
13
|
+
/**
|
|
14
|
+
* The first `@tenphi/tasty` release with compound colors (`'#surface+#dark.04'`),
|
|
15
|
+
* which replace both two-color fills.
|
|
16
|
+
*/
|
|
17
|
+
const COMPOUND_COLOR_TASTY_VERSION = "3.16.0";
|
|
18
|
+
const FILL_PROPERTIES = new Set(["fill", "backgroundColor"]);
|
|
19
|
+
/** Color functions Tasty recognizes beyond the ones the parser classifies. */
|
|
20
|
+
const EXTRA_COLOR_FUNCS = new Set([
|
|
21
|
+
"light-dark",
|
|
22
|
+
"contrast-color",
|
|
23
|
+
"okhsl",
|
|
24
|
+
"okhst"
|
|
25
|
+
]);
|
|
26
|
+
/** A color token, optionally faded: one operand of a compound color. */
|
|
27
|
+
const RE_LAYER_OPERAND = /^#[a-z0-9-]+(?:\.(?:\$[a-z_][a-z0-9-_]*|[0-9]+))?$/i;
|
|
28
|
+
/** The weight `#name.$prop` compiles to: `($prop * 100%)`. */
|
|
29
|
+
const RE_PROP_WEIGHT = /^\(\s*(\$[a-z_][a-z0-9-_]*)\s*\*\s*100%\s*\)$/i;
|
|
30
|
+
/** Whether Tasty's parser takes this token for a color. */
|
|
31
|
+
function isColor(token) {
|
|
32
|
+
switch (token.type) {
|
|
33
|
+
case "color-token":
|
|
34
|
+
case "color-keyword": return true;
|
|
35
|
+
case "custom-prop": return token.name.endsWith("-color");
|
|
36
|
+
case "css-function": {
|
|
37
|
+
const name = token.name.toLowerCase();
|
|
38
|
+
return COLOR_FUNCS.has(name) || EXTRA_COLOR_FUNCS.has(name);
|
|
39
|
+
}
|
|
40
|
+
case "group-expr": return token.inner.includes("#");
|
|
41
|
+
default: return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** `#clear` and its fades compile to `transparent`. */
|
|
45
|
+
function isClear(token) {
|
|
46
|
+
return token.type === "color-token" && (token.raw === "#clear" || token.raw.startsWith("#clear."));
|
|
47
|
+
}
|
|
48
|
+
/** Whether a color can be the base of a compound: a token, or a compound. */
|
|
49
|
+
function isCompoundBase(color) {
|
|
50
|
+
return color.split("+").every((operand) => RE_LAYER_OPERAND.test(operand));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The weight a `#name.N` fade stands for, written the way Tasty compiles the
|
|
54
|
+
* opacity: `.04` is `4%`, `.5` is `50%`, `.$hover` is `($hover * 100%)`.
|
|
55
|
+
*/
|
|
56
|
+
function opacityWeight(opacity) {
|
|
57
|
+
if (opacity.startsWith("$")) return `(${opacity} * 100%)`;
|
|
58
|
+
return `${Number((Number(`0.${opacity}`) * 100).toFixed(6))}%`;
|
|
59
|
+
}
|
|
60
|
+
/** A faded color token split into the color and the weight its opacity is. */
|
|
61
|
+
function splitFade(token) {
|
|
62
|
+
if (token.type !== "color-token" || token.opacity === void 0) return null;
|
|
63
|
+
return {
|
|
64
|
+
color: token.raw.slice(0, -(token.opacity.length + 1)),
|
|
65
|
+
opacity: token.opacity,
|
|
66
|
+
weight: opacityWeight(token.opacity)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The opacity suffix a mix weight stands for, the inverse of
|
|
71
|
+
* {@link opacityWeight}: `4%` is `04`, `4.5%` is `045`, `($hover * 100%)` is
|
|
72
|
+
* `$hover`. Built from the digits, so no float artifact creeps in. `null` when
|
|
73
|
+
* the weight has no suffix, such as a `$prop` holding a percentage, or is not
|
|
74
|
+
* strictly between 0% and 100%.
|
|
75
|
+
*/
|
|
76
|
+
function weightToOpacity(weight) {
|
|
77
|
+
if (weight.type === "group-expr") return weight.raw.match(RE_PROP_WEIGHT)?.[1] ?? null;
|
|
78
|
+
if (weight.type !== "css-unit" || weight.unit !== "%") return null;
|
|
79
|
+
if (!(weight.value > 0 && weight.value < 100)) return null;
|
|
80
|
+
const [int, frac = ""] = weight.raw.slice(0, -1).split(".");
|
|
81
|
+
return (int.padStart(2, "0").slice(-2) + frac).replace(/0+$/, "") || null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* `color` painted over `base` at an opacity, as one color: a compound when
|
|
85
|
+
* `base` can be one, and `color-mix()` itself otherwise. Both compile to the
|
|
86
|
+
* same mix, `color-mix(in srgb, <color> <weight>, <base>)`.
|
|
87
|
+
*/
|
|
88
|
+
function layerOver(base, fade) {
|
|
89
|
+
return isCompoundBase(base) ? `${base}+${fade.color}.${fade.opacity}` : `color-mix(in srgb, ${fade.color} ${fade.weight}, ${base})`;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The comma mix that paints what `'<base> <overlay>'` painted, or `null` when
|
|
93
|
+
* the overlay's alpha is not written on it. Exact when the overlay token holds
|
|
94
|
+
* an opaque color: drawing it at alpha `a` over the base is the base mixed with
|
|
95
|
+
* it at weight `a` in sRGB, which is what Tasty compiles the mix to.
|
|
96
|
+
*/
|
|
97
|
+
function rewriteOverlay(base, overlay) {
|
|
98
|
+
if (isClear(overlay)) return base;
|
|
99
|
+
const fade = splitFade(overlay);
|
|
100
|
+
if (!fade) return null;
|
|
101
|
+
if (fade.weight === "0%") return base;
|
|
102
|
+
return `${fade.color} ${fade.weight}, ${base}`;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The compound color that paints what `'<base> <overlay>'` painted, or `null`
|
|
106
|
+
* when the overlay's alpha is not written on it. The same exact rewrite as
|
|
107
|
+
* {@link rewriteOverlay}, a `+` where the space was.
|
|
108
|
+
*/
|
|
109
|
+
function rewriteOverlayAsCompound(base, overlay) {
|
|
110
|
+
if (isClear(overlay)) return base;
|
|
111
|
+
const fade = splitFade(overlay);
|
|
112
|
+
if (!fade) return null;
|
|
113
|
+
if (fade.weight === "0%") return base;
|
|
114
|
+
return layerOver(base, fade);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The color a two-part comma mix is, written without the comma. A token with a
|
|
118
|
+
* weight its opacity suffix can hold is layered over the other part, as a
|
|
119
|
+
* compound; any other mix is the same parts in `color-mix()` itself.
|
|
120
|
+
*/
|
|
121
|
+
function rewriteMix(parts, groups) {
|
|
122
|
+
for (const [i, tokens] of groups.entries()) {
|
|
123
|
+
const [color, weight] = tokens;
|
|
124
|
+
const base = parts[1 - i];
|
|
125
|
+
if (tokens.length !== 2 || color.type !== "color-token" || color.opacity !== void 0 || color.raw.includes("+") || groups[1 - i].length !== 1 || !isCompoundBase(base)) continue;
|
|
126
|
+
if (isClear(color)) break;
|
|
127
|
+
if (weight.type === "css-unit" && weight.unit === "%") {
|
|
128
|
+
if (weight.value === 0) return base;
|
|
129
|
+
if (weight.value === 100) return color.raw;
|
|
130
|
+
}
|
|
131
|
+
const opacity = weightToOpacity(weight);
|
|
132
|
+
if (opacity !== null) return `${base}+${color.raw}.${opacity}`;
|
|
133
|
+
}
|
|
134
|
+
return `color-mix(in srgb, ${parts.join(", ")})`;
|
|
135
|
+
}
|
|
136
|
+
/** A token as written. */
|
|
137
|
+
function tokenText(token) {
|
|
138
|
+
return "raw" in token ? token.raw : token.value;
|
|
139
|
+
}
|
|
140
|
+
var prefer_fill_mix_default = createRule({
|
|
141
|
+
name: "prefer-fill-mix",
|
|
142
|
+
meta: {
|
|
143
|
+
type: "suggestion",
|
|
144
|
+
fixable: "code",
|
|
145
|
+
hasSuggestions: true,
|
|
146
|
+
docs: { description: "Replace deprecated two-color fills with a compound color ('#surface+#dark.04'), or with a comma mix before Tasty 3.16, and catch mix parts weighted by their opacity" },
|
|
147
|
+
messages: {
|
|
148
|
+
twoColorFill: "Two space-separated colors in '{{property}}' are deprecated: the second is painted as a background-image layer that contrast checkers cannot read. Use '{{suggestion}}'.",
|
|
149
|
+
twoColorFillManual: "Two space-separated colors in '{{property}}' are deprecated: the second is painted as a background-image layer that contrast checkers cannot read. {{advice}}",
|
|
150
|
+
translucentMixPart: "'{{part}}' is translucent and has no percentage, so its alpha weighs it down on top of its share of the mix and the background comes out see-through. Give it a weight instead: '{{suggestion}}'.",
|
|
151
|
+
useWeight: "Use '{{suggestion}}'.",
|
|
152
|
+
commaMix: "Mixing '{{property}}' colors with a comma is deprecated: a compound color or color-mix() is the same color, and works in every color slot. Use '{{suggestion}}'.",
|
|
153
|
+
commaMixManual: "Mixing '{{property}}' colors with a comma is deprecated, and Tasty mixes only the first two. Layer the colors as a compound color ('#base+#overlay.04'), or write color-mix() itself.",
|
|
154
|
+
translucentCommaMix: "Mixing '{{property}}' colors with a comma is deprecated, and '{{part}}' is translucent and has no percentage, so its alpha weighs it down on top of its share of the mix and the background comes out see-through. To paint it over the other color, use '{{suggestion}}'.",
|
|
155
|
+
useCompound: "Use '{{suggestion}}'."
|
|
156
|
+
},
|
|
157
|
+
schema: []
|
|
158
|
+
},
|
|
159
|
+
defaultOptions: [],
|
|
160
|
+
create(context) {
|
|
161
|
+
const tastyVersion = getInstalledTastyVersion(context.filename);
|
|
162
|
+
if (!tastyVersion || !semver.satisfies(tastyVersion, `>=${FILL_MIX_TASTY_VERSION}`)) return {};
|
|
163
|
+
const compound = semver.satisfies(tastyVersion, `>=${COMPOUND_COLOR_TASTY_VERSION}`);
|
|
164
|
+
const ctx = new TastyContext(context);
|
|
165
|
+
function checkSpaceSeparated(tokens, colorCount, node, property) {
|
|
166
|
+
const rewrite = compound ? rewriteOverlayAsCompound : rewriteOverlay;
|
|
167
|
+
const suggestion = tokens.length === 2 && colorCount === 2 ? rewrite(tokenText(tokens[0]), tokens[1]) : null;
|
|
168
|
+
if (suggestion === null) {
|
|
169
|
+
context.report({
|
|
170
|
+
node,
|
|
171
|
+
messageId: "twoColorFillManual",
|
|
172
|
+
data: {
|
|
173
|
+
property,
|
|
174
|
+
advice: compound ? "Layer an opaque overlay color with its opacity ('#base+#overlay.04'), or use an opaque overlay alone." : "Mix an opaque overlay color in by weight ('#overlay 4%, #base'), or use an opaque overlay alone."
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
context.report({
|
|
180
|
+
node,
|
|
181
|
+
messageId: "twoColorFill",
|
|
182
|
+
data: {
|
|
183
|
+
property,
|
|
184
|
+
suggestion
|
|
185
|
+
},
|
|
186
|
+
fix: (fixer) => replaceStringValue(fixer, node, suggestion)
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* The fix for a mix part that is translucent and has no weight, or `null`
|
|
191
|
+
* when neither part is. Each part without a weight takes half the mix, so
|
|
192
|
+
* such a part is weighed down twice, and the result is see-through.
|
|
193
|
+
*/
|
|
194
|
+
function translucentPart(parts, groups) {
|
|
195
|
+
for (const [i, tokens] of groups.entries()) {
|
|
196
|
+
if (tokens.length !== 1) continue;
|
|
197
|
+
const [token] = tokens;
|
|
198
|
+
const other = parts[1 - i];
|
|
199
|
+
if (isClear(token)) return {
|
|
200
|
+
part: parts[i],
|
|
201
|
+
suggestion: other
|
|
202
|
+
};
|
|
203
|
+
const fade = splitFade(token);
|
|
204
|
+
if (!fade) continue;
|
|
205
|
+
let suggestion;
|
|
206
|
+
if (compound) {
|
|
207
|
+
if (fade.weight === "0%") return {
|
|
208
|
+
part: parts[i],
|
|
209
|
+
suggestion: other
|
|
210
|
+
};
|
|
211
|
+
suggestion = layerOver(other, fade);
|
|
212
|
+
} else {
|
|
213
|
+
const weighted = [...parts];
|
|
214
|
+
weighted[i] = `${fade.color} ${fade.weight}`;
|
|
215
|
+
suggestion = weighted.join(", ");
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
part: parts[i],
|
|
219
|
+
suggestion
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
function checkMix(value, groups, node, property) {
|
|
225
|
+
const parts = splitTopLevelCommas(value).map((part) => part.trim());
|
|
226
|
+
const translucent = groups.every((tokens) => tokens.length === 1) ? translucentPart(parts, groups) : null;
|
|
227
|
+
if (translucent) {
|
|
228
|
+
const { part, suggestion } = translucent;
|
|
229
|
+
context.report({
|
|
230
|
+
node,
|
|
231
|
+
messageId: compound ? "translucentCommaMix" : "translucentMixPart",
|
|
232
|
+
data: {
|
|
233
|
+
property,
|
|
234
|
+
part,
|
|
235
|
+
suggestion
|
|
236
|
+
},
|
|
237
|
+
suggest: [{
|
|
238
|
+
messageId: compound ? "useCompound" : "useWeight",
|
|
239
|
+
data: { suggestion },
|
|
240
|
+
fix: (fixer) => replaceStringValue(fixer, node, suggestion)
|
|
241
|
+
}]
|
|
242
|
+
});
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (!compound) return;
|
|
246
|
+
const suggestion = rewriteMix(parts, groups);
|
|
247
|
+
context.report({
|
|
248
|
+
node,
|
|
249
|
+
messageId: "commaMix",
|
|
250
|
+
data: {
|
|
251
|
+
property,
|
|
252
|
+
suggestion
|
|
253
|
+
},
|
|
254
|
+
fix: (fixer) => replaceStringValue(fixer, node, suggestion)
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
function checkValue(value, node, property) {
|
|
258
|
+
const { groups } = parseValue(value);
|
|
259
|
+
if (groups.some((group) => group.parts.length !== 1)) return;
|
|
260
|
+
const tokenGroups = groups.map((group) => group.parts[0].tokens);
|
|
261
|
+
if (tokenGroups.length === 1) {
|
|
262
|
+
const colorCount = tokenGroups[0].filter(isColor).length;
|
|
263
|
+
if (colorCount >= 2) checkSpaceSeparated(tokenGroups[0], colorCount, node, property);
|
|
264
|
+
} else if (tokenGroups.length === 2) checkMix(value, tokenGroups, node, property);
|
|
265
|
+
else if (compound && tokenGroups.length > 2) context.report({
|
|
266
|
+
node,
|
|
267
|
+
messageId: "commaMixManual",
|
|
268
|
+
data: { property }
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
return stringStyleValueListeners(ctx, (value, node, property) => {
|
|
272
|
+
if (FILL_PROPERTIES.has(property)) checkValue(value, node, property);
|
|
273
|
+
}, { requireTastyJSX: true });
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
//#endregion
|
|
278
|
+
export { prefer_fill_mix_default as default };
|
|
279
|
+
//# sourceMappingURL=prefer-fill-mix.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefer-fill-mix.js","names":[],"sources":["../../src/rules/prefer-fill-mix.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport semver from 'semver';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { getInstalledTastyVersion } from '../config.js';\nimport { replaceStringValue } from '../fix-utils.js';\nimport { parseValue } from '../parsers/index.js';\nimport type { ValueToken } from '../parsers/index.js';\nimport { splitTopLevelCommas } from '../parsers/utils.js';\nimport { COLOR_FUNCS } from '../parsers/value-parser.js';\nimport { stringStyleValueListeners } from '../style-values.js';\n\ntype MessageIds =\n | 'twoColorFill'\n | 'twoColorFillManual'\n | 'translucentMixPart'\n | 'useWeight'\n | 'commaMix'\n | 'commaMixManual'\n | 'translucentCommaMix'\n | 'useCompound';\n\n/** The first `@tenphi/tasty` release that mixes comma-separated fill colors. */\nconst FILL_MIX_TASTY_VERSION = '3.15.0';\n\n/**\n * The first `@tenphi/tasty` release with compound colors (`'#surface+#dark.04'`),\n * which replace both two-color fills.\n */\nconst COMPOUND_COLOR_TASTY_VERSION = '3.16.0';\n\nconst FILL_PROPERTIES = new Set(['fill', 'backgroundColor']);\n\n/** Color functions Tasty recognizes beyond the ones the parser classifies. */\nconst EXTRA_COLOR_FUNCS = new Set([\n 'light-dark',\n 'contrast-color',\n 'okhsl',\n 'okhst',\n]);\n\n/** A color token, optionally faded: one operand of a compound color. */\nconst RE_LAYER_OPERAND = /^#[a-z0-9-]+(?:\\.(?:\\$[a-z_][a-z0-9-_]*|[0-9]+))?$/i;\n\n/** The weight `#name.$prop` compiles to: `($prop * 100%)`. */\nconst RE_PROP_WEIGHT = /^\\(\\s*(\\$[a-z_][a-z0-9-_]*)\\s*\\*\\s*100%\\s*\\)$/i;\n\n/** Whether Tasty's parser takes this token for a color. */\nfunction isColor(token: ValueToken): boolean {\n switch (token.type) {\n case 'color-token':\n case 'color-keyword':\n return true;\n case 'custom-prop':\n // `$name` is a color only when the name ends with `-color`.\n return token.name.endsWith('-color');\n case 'css-function': {\n const name = token.name.toLowerCase();\n return COLOR_FUNCS.has(name) || EXTRA_COLOR_FUNCS.has(name);\n }\n case 'group-expr':\n // A fallback chain such as `(#accent, #purple)`.\n return token.inner.includes('#');\n default:\n return false;\n }\n}\n\n/** `#clear` and its fades compile to `transparent`. */\nfunction isClear(token: ValueToken): boolean {\n return (\n token.type === 'color-token' &&\n (token.raw === '#clear' || token.raw.startsWith('#clear.'))\n );\n}\n\n/** Whether a color can be the base of a compound: a token, or a compound. */\nfunction isCompoundBase(color: string): boolean {\n return color.split('+').every((operand) => RE_LAYER_OPERAND.test(operand));\n}\n\n/**\n * The weight a `#name.N` fade stands for, written the way Tasty compiles the\n * opacity: `.04` is `4%`, `.5` is `50%`, `.$hover` is `($hover * 100%)`.\n */\nfunction opacityWeight(opacity: string): string {\n if (opacity.startsWith('$')) return `(${opacity} * 100%)`;\n return `${Number((Number(`0.${opacity}`) * 100).toFixed(6))}%`;\n}\n\n/** A faded color token split into the color and the weight its opacity is. */\nfunction splitFade(\n token: ValueToken,\n): { color: string; opacity: string; weight: string } | null {\n if (token.type !== 'color-token' || token.opacity === undefined) return null;\n return {\n color: token.raw.slice(0, -(token.opacity.length + 1)),\n opacity: token.opacity,\n weight: opacityWeight(token.opacity),\n };\n}\n\n/**\n * The opacity suffix a mix weight stands for, the inverse of\n * {@link opacityWeight}: `4%` is `04`, `4.5%` is `045`, `($hover * 100%)` is\n * `$hover`. Built from the digits, so no float artifact creeps in. `null` when\n * the weight has no suffix, such as a `$prop` holding a percentage, or is not\n * strictly between 0% and 100%.\n */\nfunction weightToOpacity(weight: ValueToken): string | null {\n if (weight.type === 'group-expr') {\n return weight.raw.match(RE_PROP_WEIGHT)?.[1] ?? null;\n }\n if (weight.type !== 'css-unit' || weight.unit !== '%') return null;\n if (!(weight.value > 0 && weight.value < 100)) return null;\n\n const [int, frac = ''] = weight.raw.slice(0, -1).split('.');\n const digits = (int.padStart(2, '0').slice(-2) + frac).replace(/0+$/, '');\n return digits || null;\n}\n\n/**\n * `color` painted over `base` at an opacity, as one color: a compound when\n * `base` can be one, and `color-mix()` itself otherwise. Both compile to the\n * same mix, `color-mix(in srgb, <color> <weight>, <base>)`.\n */\nfunction layerOver(\n base: string,\n fade: { color: string; opacity: string; weight: string },\n): string {\n return isCompoundBase(base)\n ? `${base}+${fade.color}.${fade.opacity}`\n : `color-mix(in srgb, ${fade.color} ${fade.weight}, ${base})`;\n}\n\n/**\n * The comma mix that paints what `'<base> <overlay>'` painted, or `null` when\n * the overlay's alpha is not written on it. Exact when the overlay token holds\n * an opaque color: drawing it at alpha `a` over the base is the base mixed with\n * it at weight `a` in sRGB, which is what Tasty compiles the mix to.\n */\nfunction rewriteOverlay(base: string, overlay: ValueToken): string | null {\n if (isClear(overlay)) return base;\n\n const fade = splitFade(overlay);\n if (!fade) return null;\n if (fade.weight === '0%') return base;\n\n return `${fade.color} ${fade.weight}, ${base}`;\n}\n\n/**\n * The compound color that paints what `'<base> <overlay>'` painted, or `null`\n * when the overlay's alpha is not written on it. The same exact rewrite as\n * {@link rewriteOverlay}, a `+` where the space was.\n */\nfunction rewriteOverlayAsCompound(\n base: string,\n overlay: ValueToken,\n): string | null {\n if (isClear(overlay)) return base;\n\n const fade = splitFade(overlay);\n if (!fade) return null;\n if (fade.weight === '0%') return base;\n\n return layerOver(base, fade);\n}\n\n/**\n * The color a two-part comma mix is, written without the comma. A token with a\n * weight its opacity suffix can hold is layered over the other part, as a\n * compound; any other mix is the same parts in `color-mix()` itself.\n */\nfunction rewriteMix(parts: string[], groups: ValueToken[][]): string {\n for (const [i, tokens] of groups.entries()) {\n const [color, weight] = tokens;\n const base = parts[1 - i];\n if (\n tokens.length !== 2 ||\n color.type !== 'color-token' ||\n color.opacity !== undefined ||\n color.raw.includes('+') ||\n groups[1 - i].length !== 1 ||\n !isCompoundBase(base)\n ) {\n continue;\n }\n\n // A compound leaves out a transparent layer, while the mix lets it thin\n // out the other color: `#clear 10%, #surface` is 90% opaque.\n if (isClear(color)) break;\n\n // A weight of nothing or of everything leaves one of the colors.\n if (weight.type === 'css-unit' && weight.unit === '%') {\n if (weight.value === 0) return base;\n if (weight.value === 100) return color.raw;\n }\n\n const opacity = weightToOpacity(weight);\n if (opacity !== null) return `${base}+${color.raw}.${opacity}`;\n }\n\n return `color-mix(in srgb, ${parts.join(', ')})`;\n}\n\n/** A token as written. */\nfunction tokenText(token: ValueToken): string {\n return 'raw' in token ? token.raw : token.value;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-fill-mix',\n meta: {\n type: 'suggestion',\n fixable: 'code',\n hasSuggestions: true,\n docs: {\n description:\n \"Replace deprecated two-color fills with a compound color ('#surface+#dark.04'), or with a comma mix before Tasty 3.16, and catch mix parts weighted by their opacity\",\n },\n messages: {\n twoColorFill:\n \"Two space-separated colors in '{{property}}' are deprecated: the second is painted as a background-image layer that contrast checkers cannot read. Use '{{suggestion}}'.\",\n twoColorFillManual:\n \"Two space-separated colors in '{{property}}' are deprecated: the second is painted as a background-image layer that contrast checkers cannot read. {{advice}}\",\n translucentMixPart:\n \"'{{part}}' is translucent and has no percentage, so its alpha weighs it down on top of its share of the mix and the background comes out see-through. Give it a weight instead: '{{suggestion}}'.\",\n useWeight: \"Use '{{suggestion}}'.\",\n commaMix:\n \"Mixing '{{property}}' colors with a comma is deprecated: a compound color or color-mix() is the same color, and works in every color slot. Use '{{suggestion}}'.\",\n commaMixManual:\n \"Mixing '{{property}}' colors with a comma is deprecated, and Tasty mixes only the first two. Layer the colors as a compound color ('#base+#overlay.04'), or write color-mix() itself.\",\n translucentCommaMix:\n \"Mixing '{{property}}' colors with a comma is deprecated, and '{{part}}' is translucent and has no percentage, so its alpha weighs it down on top of its share of the mix and the background comes out see-through. To paint it over the other color, use '{{suggestion}}'.\",\n useCompound: \"Use '{{suggestion}}'.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n // The comma mix and compound colors are new syntax: rewriting to them for\n // an older Tasty would leave the fill showing only its first color.\n const tastyVersion = getInstalledTastyVersion(context.filename);\n if (\n !tastyVersion ||\n !semver.satisfies(tastyVersion, `>=${FILL_MIX_TASTY_VERSION}`)\n ) {\n return {};\n }\n const compound = semver.satisfies(\n tastyVersion,\n `>=${COMPOUND_COLOR_TASTY_VERSION}`,\n );\n\n const ctx = new TastyContext(context);\n\n function checkSpaceSeparated(\n tokens: ValueToken[],\n colorCount: number,\n node: TSESTree.Node,\n property: string,\n ): void {\n const rewrite = compound ? rewriteOverlayAsCompound : rewriteOverlay;\n const suggestion =\n tokens.length === 2 && colorCount === 2\n ? rewrite(tokenText(tokens[0]), tokens[1])\n : null;\n\n if (suggestion === null) {\n context.report({\n node,\n messageId: 'twoColorFillManual',\n data: {\n property,\n advice: compound\n ? \"Layer an opaque overlay color with its opacity ('#base+#overlay.04'), or use an opaque overlay alone.\"\n : \"Mix an opaque overlay color in by weight ('#overlay 4%, #base'), or use an opaque overlay alone.\",\n },\n });\n return;\n }\n\n context.report({\n node,\n messageId: 'twoColorFill',\n data: { property, suggestion },\n fix: (fixer) => replaceStringValue(fixer, node, suggestion),\n });\n }\n\n /**\n * The fix for a mix part that is translucent and has no weight, or `null`\n * when neither part is. Each part without a weight takes half the mix, so\n * such a part is weighed down twice, and the result is see-through.\n */\n function translucentPart(\n parts: string[],\n groups: ValueToken[][],\n ): { part: string; suggestion: string } | null {\n for (const [i, tokens] of groups.entries()) {\n if (tokens.length !== 1) continue;\n\n const [token] = tokens;\n const other = parts[1 - i];\n\n // A transparent color mixes in nothing.\n if (isClear(token)) return { part: parts[i], suggestion: other };\n\n const fade = splitFade(token);\n if (!fade) continue;\n\n let suggestion: string;\n if (compound) {\n if (fade.weight === '0%')\n return { part: parts[i], suggestion: other };\n suggestion = layerOver(other, fade);\n } else {\n const weighted = [...parts];\n weighted[i] = `${fade.color} ${fade.weight}`;\n suggestion = weighted.join(', ');\n }\n return { part: parts[i], suggestion };\n }\n\n return null;\n }\n\n function checkMix(\n value: string,\n groups: ValueToken[][],\n node: TSESTree.Node,\n property: string,\n ): void {\n const parts = splitTopLevelCommas(value).map((part) => part.trim());\n\n // Opposite a weighted part, a faded one is only the base of a layer.\n const translucent = groups.every((tokens) => tokens.length === 1)\n ? translucentPart(parts, groups)\n : null;\n\n // A rewrite here changes what renders, so it is only suggested.\n if (translucent) {\n const { part, suggestion } = translucent;\n context.report({\n node,\n messageId: compound ? 'translucentCommaMix' : 'translucentMixPart',\n data: { property, part, suggestion },\n suggest: [\n {\n messageId: compound ? 'useCompound' : 'useWeight',\n data: { suggestion },\n fix: (fixer) => replaceStringValue(fixer, node, suggestion),\n },\n ],\n });\n return;\n }\n\n if (!compound) return;\n\n const suggestion = rewriteMix(parts, groups);\n context.report({\n node,\n messageId: 'commaMix',\n data: { property, suggestion },\n fix: (fixer) => replaceStringValue(fixer, node, suggestion),\n });\n }\n\n function checkValue(\n value: string,\n node: TSESTree.Node,\n property: string,\n ): void {\n const { groups } = parseValue(value);\n\n // Slash-separated parts mean nothing to `fill`; leave such values alone.\n if (groups.some((group) => group.parts.length !== 1)) return;\n const tokenGroups = groups.map((group) => group.parts[0].tokens);\n\n if (tokenGroups.length === 1) {\n const colorCount = tokenGroups[0].filter(isColor).length;\n if (colorCount >= 2) {\n checkSpaceSeparated(tokenGroups[0], colorCount, node, property);\n }\n } else if (tokenGroups.length === 2) {\n checkMix(value, tokenGroups, node, property);\n } else if (compound && tokenGroups.length > 2) {\n context.report({\n node,\n messageId: 'commaMixManual',\n data: { property },\n });\n }\n }\n\n return stringStyleValueListeners(\n ctx,\n (value, node, property) => {\n if (FILL_PROPERTIES.has(property)) checkValue(value, node, property);\n },\n { requireTastyJSX: true },\n );\n },\n});\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tenphi/eslint-plugin-tasty",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"description": "ESLint plugin for validating tasty() and tastyStatic() style objects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@changesets/changelog-github": "^0.5.2",
|
|
54
|
-
"@changesets/cli": "^2.
|
|
54
|
+
"@changesets/cli": "^2.31.1",
|
|
55
55
|
"@eslint/js": "^10.0.1",
|
|
56
|
-
"@tenphi/tasty": "^3.
|
|
56
|
+
"@tenphi/tasty": "^3.16.0",
|
|
57
57
|
"@types/node": "^22.0.0",
|
|
58
58
|
"@types/semver": "^7.8.0",
|
|
59
59
|
"@typescript-eslint/parser": "^8.56.1",
|