@tenphi/eslint-plugin-tasty 1.14.0 → 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 CHANGED
@@ -268,7 +268,7 @@ not matched.
268
268
  | `tasty/prefer-state-negation` | warn | Top-level `:not(...)` in state keys instead of the `!` prefix (autofixable; nested CSS selectors stay intact) |
269
269
  | `tasty/prefer-custom-property-syntax` | warn | `var(--prop)` / `$x-color` / `transparent` / `currentColor` instead of `$prop` / `#color` / `#clear` / `#current` |
270
270
  | `tasty/prefer-hide` | warn | `display: 'none'` instead of `hide: true` |
271
- | `tasty/prefer-fill-mix` | warn | Deprecated two-color `fill` (`'#surface #dark.04'`) instead of a comma mix (`'#dark 4%, #surface'`), autofixed when the overlay's opacity is written on it; mix parts weighted by their opacity (`'#dark.04, #surface'`). Needs `@tenphi/tasty` 3.15 ([details](docs/rules/prefer-fill-mix.md)) |
271
+ | `tasty/prefer-fill-mix` | warn | Deprecated two-color `fill`, space-separated (`'#surface #dark.04'`) or comma-mixed (`'#dark 4%, #surface'`), instead of a compound color (`'#surface+#dark.04'`) or `color-mix()`, autofixed when the result is the same color; mix parts weighted by their opacity (`'#dark.04, #surface'`). Needs `@tenphi/tasty` 3.15, and 3.16 for compound colors ([details](docs/rules/prefer-fill-mix.md)) |
272
272
  | `tasty/prefer-directional-shorthand` | warn | 4-value `margin`/`padding`/`inset`/`radius`/`fade`/`border` with placeholder positions instead of directional form |
273
273
  | `tasty/prefer-longhand-property` | error | Lossy `flex` shorthand instead of `flexGrow` / `flexShrink` / `flexBasis` |
274
274
 
@@ -395,6 +395,7 @@ function classifyToken(raw, offset, errors, opts) {
395
395
  raw: token
396
396
  };
397
397
  }
398
+ if (token.includes("+")) return classifyCompoundColor(token, offset, errors);
398
399
  return classifyColorToken(token, offset, errors);
399
400
  }
400
401
  if (token.startsWith("(") && token.endsWith(")")) return {
@@ -614,6 +615,42 @@ function classifyColorToken(token, offset, errors) {
614
615
  raw
615
616
  };
616
617
  }
618
+ /** One layer of a compound color: a `#name`, before any opacity suffix. */
619
+ const RE_COMPOUND_LAYER = /^#[a-z0-9-]+(?:\.|$)/i;
620
+ /**
621
+ * Classify a compound color, `#surface+#dark.04`: color tokens joined by `+`,
622
+ * each with its own opacity, painted left to right. Tasty drops the whole value
623
+ * when a part is not a color token, so that is an error. The token's name is
624
+ * the compound as written, and it has no opacity of its own.
625
+ */
626
+ function classifyCompoundColor(token, offset, errors) {
627
+ const raw = token;
628
+ let layerOffset = offset;
629
+ for (const layer of token.split("+")) {
630
+ if (!RE_COMPOUND_LAYER.test(layer)) {
631
+ errors.push({
632
+ message: `'${raw}' is not a compound color: every part joined by '+' must be a color token, such as '#surface+#dark.04'.`,
633
+ offset,
634
+ length: raw.length,
635
+ raw
636
+ });
637
+ return {
638
+ type: "unknown",
639
+ raw
640
+ };
641
+ }
642
+ if (classifyColorToken(layer, layerOffset, errors).type !== "color-token") return {
643
+ type: "unknown",
644
+ raw
645
+ };
646
+ layerOffset += layer.length + 1;
647
+ }
648
+ return {
649
+ type: "color-token",
650
+ name: token.slice(1),
651
+ raw
652
+ };
653
+ }
617
654
  /**
618
655
  * Parse a style value string into typed tokens organized by
619
656
  * comma-separated groups and slash-separated parts.
@@ -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\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 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;;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;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;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CAEA;CACA;CAEA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CAEA;CAEA;CAEA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACD,CAAC;AAMF,SAAS,MAAM,OAA6C;AAC1D,KAAI,CAAC,MAAO,wBAAO,IAAI,KAAK;AAC5B,QAAO,iBAAiB,MAAM,QAAQ,IAAI,IAAI,MAAM;;AAGtD,SAAS,cACP,KACA,QACA,QACA,MACY;CACZ,MAAM,QAAQ,IAAI,MAAM;AACxB,KAAI,CAAC,MAAO,QAAO;EAAE,MAAM;EAAW,OAAO;EAAI;AAGjD,KACG,MAAM,WAAW,KAAI,IAAI,MAAM,SAAS,KAAI,IAC5C,MAAM,WAAW,IAAI,IAAI,MAAM,SAAS,IAAI,CAE7C,QAAO;EAAE,MAAM;EAAU,OAAO,MAAM,MAAM,GAAG,GAAG;EAAE,KAAK;EAAO;AAIlE,KAAI,UAAU,aACZ,QAAO;EAAE,MAAM;EAAa,KAAK;EAAO;AAM1C,KAAI,MAAM,WAAW,KAAK,IAAI,MAAM,WAAW,KAAK,EAAE;EACpD,MAAM,OAAO,0BAA0B,OAAO,QAAQ,OAAO;AAC7D,MAAI,KAAM,QAAO;;AAInB,KAAI,MAAM,WAAW,KAAK,EAAE;EAC1B,MAAM,OAAO,MAAM,MAAM,EAAE;AAC3B,MAAI,uBAAuB,KAAK,KAAK,CACnC,QAAO;GAAE,MAAM;GAAmB;GAAM,KAAK;GAAO;AAEtD,SAAO,KAAK;GACV,SAAS,sCAAsC,MAAM;GACrD;GACA,QAAQ,MAAM;GACd,KAAK;GACN,CAAC;AACF,SAAO;GAAE,MAAM;GAAW,KAAK;GAAO;;AAIxC,KAAI,MAAM,WAAW,KAAK,EAAE;EAC1B,MAAM,OAAO,MAAM,MAAM,EAAE;AAC3B,MAAI,uBAAuB,KAAK,KAAK,CACnC,QAAO;GAAE,MAAM;GAAa;GAAM,KAAK;GAAO;AAEhD,SAAO,KAAK;GACV,SAAS,4BAA4B,MAAM;GAC3C;GACA,QAAQ,MAAM;GACd,KAAK;GACN,CAAC;AACF,SAAO;GAAE,MAAM;GAAW,KAAK;GAAO;;AAIxC,KAAI,MAAM,WAAW,IAAI,EAAE;EACzB,MAAM,OAAO,MAAM,MAAM,EAAE;AAC3B,MAAI,uBAAuB,KAAK,KAAK,CACnC,QAAO;GAAE,MAAM;GAAe;GAAM,KAAK;GAAO;AAElD,SAAO,KAAK;GACV,SAAS,mCAAmC,MAAM;GAClD;GACA,QAAQ,MAAM;GACd,KAAK;GACN,CAAC;AACF,SAAO;GAAE,MAAM;GAAW,KAAK;GAAO;;AAIxC,KAAI,MAAM,WAAW,IAAI,EAAE;AACzB,MAAI,MAAM,WAAW,GAAG;AACtB,UAAO,KAAK;IACV,SAAS;IACT;IACA,QAAQ;IACR,KAAK;IACN,CAAC;AACF,UAAO;IAAE,MAAM;IAAW,KAAK;IAAO;;AAExC,SAAO,mBAAmB,OAAO,QAAQ,OAAO;;AAIlD,KAAI,MAAM,WAAW,IAAI,IAAI,MAAM,SAAS,IAAI,CAC9C,QAAO;EAAE,MAAM;EAAc,OAAO,MAAM,MAAM,GAAG,GAAG;EAAE,KAAK;EAAO;CAItE,MAAM,UAAU,MAAM,QAAQ,IAAI;AAClC,KAAI,UAAU,KAAK,MAAM,SAAS,IAAI,EAAE;EACtC,MAAM,QAAQ,MAAM,MAAM,GAAG,QAAQ;EACrC,MAAM,OAAO,MAAM,MAAM,UAAU,GAAG,GAAG;AAEzC,MAAI,KAAK,mBACP,QAAO;GAAE,MAAM;GAAgB,MAAM;GAAO;GAAM,KAAK;GAAO;EAGhE,MAAM,aAAa,MAAM,KAAK,WAAW;AAEzC,MACE,YAAY,IAAI,MAAM,IACtB,UAAU,IAAI,MAAM,IACpB,WAAW,IAAI,MAAM,CAErB,QAAO;GAAE,MAAM;GAAgB,MAAM;GAAO;GAAM,KAAK;GAAO;AAIhE,SAAO,KAAK;GACV,SAAS,qBAAqB,MAAM;GACpC;GACA,QAAQ,MAAM;GACd,KAAK;GACN,CAAC;AACF,SAAO;GAAE,MAAM;GAAgB,MAAM;GAAO;GAAM,KAAK;GAAO;;CAIhE,MAAM,YAAY,MAAM,MAAM,YAAY;AAC1C,KAAI,WAAW;EACb,MAAM,OAAO,UAAU;EACvB,MAAM,SAAS,WAAW,MAAM,MAAM,GAAG,CAAC,KAAK,OAAO,CAAC;AAEvD,MAAI,KAAK,mBACP,QAAO;GAAE,MAAM;GAAe,OAAO;GAAQ;GAAM,KAAK;GAAO;EAGjE,MAAM,aAAa,MAAM,KAAK,WAAW;AAEzC,MAAI,eAAe,IAAI,KAAK,IAAI,WAAW,IAAI,KAAK,CAClD,QAAO;GAAE,MAAM;GAAe,OAAO;GAAQ;GAAM,KAAK;GAAO;AAEjE,MAAI,UAAU,IAAI,KAAK,CACrB,QAAO;GAAE,MAAM;GAAY,OAAO;GAAQ;GAAM,KAAK;GAAO;AAG9D,SAAO,KAAK;GACV,SAAS,iBAAiB,KAAK,QAAQ,MAAM;GAC7C;GACA,QAAQ,MAAM;GACd,KAAK;GACN,CAAC;AACF,SAAO;GAAE,MAAM;GAAW,KAAK;GAAO;;CAIxC,MAAM,eAAe,MAAM,MAAM,gBAAgB;AACjD,KAAI,cAAc;EAChB,MAAM,OAAO,aAAa;EAC1B,MAAM,SAAS,WAAW,MAAM,MAAM,GAAG,CAAC,KAAK,OAAO,CAAC;AACvD,MAAI,UAAU,IAAI,KAAK,CACrB,QAAO;GAAE,MAAM;GAAY,OAAO;GAAQ;GAAM,KAAK;GAAO;;AAKhE,KAAI,UAAU,KAAK,MAAM,CACvB,QAAO;EAAE,MAAM;EAAU,OAAO,WAAW,MAAM;EAAE,KAAK;EAAO;AAIjE,KAAI,SAAS,KAAK,MAAM,CACtB,QAAO;EAAE,MAAM;EAAS,KAAK;EAAO;AAItC,KAAI,UAAU,WACZ,QAAO;EAAE,MAAM;EAAW,OAAO;EAAO;AAI1C,KAAI,eAAe,IAAI,MAAM,IAAI,eAAe,IAAI,MAAM,aAAa,CAAC,CACtE,QAAO;EAAE,MAAM;EAAW,OAAO;EAAO;CAK1C,MAAM,QAAQ,MAAM,aAAa;AACjC,KAAI,iBAAiB,IAAI,MAAM,IAAI,cAAc,IAAI,MAAM,CACzD,QAAO;EAAE,MAAM;EAAiB,KAAK;EAAO;AAI9C,KAAI,MAAM,WAAW,OAAO,IAAI,MAAM,SAAS,IAAI,CACjD,QAAO;EACL,MAAM;EACN,MAAM;EACN,MAAM,MAAM,MAAM,GAAG,GAAG;EACxB,KAAK;EACN;AAIH,QAAO;EAAE,MAAM;EAAW,KAAK;EAAO;;;;;;;;AASxC,SAAS,0BACP,OACA,QACA,QACmB;CACnB,MAAM,UAAU,MAAM,QAAQ,IAAI;AAElC,KAAI,WAAW,KAAK,CAAC,MAAM,SAAS,IAAI,CAAE,QAAO;CAEjD,MAAM,OAAO,MAAM,MAAM,GAAG,QAAQ;CACpC,MAAM,OAAO,MAAM,MAAM,UAAU,GAAG,GAAG;AAEzC,KAAI,CAAC,uBAAuB,KAAK,KAAK,EAAE;AACtC,SAAO,KAAK;GACV,SAAS,6BAA6B,MAAM;GAC5C;GACA,QAAQ,MAAM;GACd,KAAK;GACN,CAAC;AACF,SAAO;GAAE,MAAM;GAAW,KAAK;GAAO;;AAGxC,QAAO;EAAE,MAAM;EAAkB;EAAM;EAAM,KAAK;EAAO;;AAG3D,SAAS,mBACP,OACA,QACA,QACY;CACZ,MAAM,MAAM;CAGZ,MAAM,OAAO,MAAM,MAAM,EAAE;AAE3B,KAAI,KAAK,WAAW,GAAG;AACrB,SAAO,KAAK;GACV,SAAS;GACT;GACA,QAAQ,MAAM;GACd;GACD,CAAC;AACF,SAAO;GAAE,MAAM;GAAW;GAAK;;CAIjC,MAAM,WAAW,KAAK,QAAQ,IAAI;AAClC,KAAI,aAAa,IAAI;EACnB,MAAM,YAAY,KAAK,MAAM,GAAG,SAAS;EACzC,MAAM,gBAAgB,KAAK,MAAM,WAAW,EAAE;AAE9C,MAAI,UAAU,WAAW,GAAG;AAC1B,UAAO,KAAK;IACV,SAAS;IACT;IACA,QAAQ,MAAM;IACd;IACD,CAAC;AACF,UAAO;IAAE,MAAM;IAAW;IAAK;;AAIjC,MAAI,cAAc,WAAW,IAAI,CAC/B,QAAO;GACL,MAAM;GACN,MAAM;GACN,SAAS;GACT;GACD;AAGH,MAAI,cAAc,WAAW,GAAG;AAC9B,UAAO,KAAK;IACV,SAAS;IACT;IACA,QAAQ,MAAM;IACd;IACD,CAAC;AACF,UAAO;IAAE,MAAM;IAAW;IAAK;;AAKjC,MAAI,CAAC,QAAQ,KAAK,cAAc,EAAE;AAChC,UAAO,KAAK;IACV,SAAS,SAAS,KAAK,cAAc,GACjC,gCACA,0BAA0B,cAAc;IAC5C;IACA,QAAQ,MAAM;IACd;IACD,CAAC;AACF,UAAO;IAAE,MAAM;IAAW;IAAK;;AAGjC,SAAO;GACL,MAAM;GACN,MAAM;GACN,SAAS;GACT;GACD;;AAGH,QAAO;EAAE,MAAM;EAAe;EAAM;EAAK;;;;;;AAW3C,SAAgB,WACd,KACA,OAA2B,EAAE,EACX;CAClB,MAAM,SAAuB,EAAE;CAG/B,MAAM,eAAe,oBAAoB,IAAI;AAC7C,KAAI,cAAc;AAChB,SAAO,KAAK;GACV,SAAS,aAAa;GACtB,QAAQ,aAAa;GACrB,QAAQ;GACT,CAAC;AACF,SAAO;GAAE,QAAQ,EAAE;GAAE;GAAQ;;CAG/B,MAAM,UAAU,WAAW,IAAI;CAE/B,MAAM,SAAuB,EAAE;CAC/B,IAAI,eAA4B,EAAE;CAClC,IAAI,gBAA8B,EAAE;CAEpC,MAAM,gBAAgB;AACpB,MAAI,cAAc,SAAS,GAAG;AAC5B,gBAAa,KAAK,EAAE,QAAQ,eAAe,CAAC;AAC5C,mBAAgB,EAAE;;;CAItB,MAAM,iBAAiB;AACrB,WAAS;AACT,MAAI,aAAa,SAAS,EACxB,QAAO,KAAK,EAAE,OAAO,cAAc,CAAC;MAEpC,QAAO,KAAK,EAAE,OAAO,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC,EAAE,CAAC;AAE1C,iBAAe,EAAE;;AAGnB,MAAK,MAAM,iBAAiB,SAAS;AACnC,MAAI,cAAc,OAAO;GACvB,MAAM,aAAa,cACjB,cAAc,OACd,cAAc,QACd,QACA,KACD;AACD,iBAAc,KAAK,WAAW;;AAEhC,MAAI,cAAc,QAAS,UAAS;AACpC,MAAI,cAAc,QAAS,WAAU;;AAIvC,KACE,cAAc,SAAS,KACvB,aAAa,SAAS,KACtB,OAAO,WAAW,EAElB,WAAU;AAGZ,QAAO;EAAE;EAAQ;EAAQ"}
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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"}
@@ -10,6 +10,11 @@ import semver from "semver";
10
10
  //#region src/rules/prefer-fill-mix.ts
11
11
  /** The first `@tenphi/tasty` release that mixes comma-separated fill colors. */
12
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";
13
18
  const FILL_PROPERTIES = new Set(["fill", "backgroundColor"]);
14
19
  /** Color functions Tasty recognizes beyond the ones the parser classifies. */
15
20
  const EXTRA_COLOR_FUNCS = new Set([
@@ -18,6 +23,10 @@ const EXTRA_COLOR_FUNCS = new Set([
18
23
  "okhsl",
19
24
  "okhst"
20
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;
21
30
  /** Whether Tasty's parser takes this token for a color. */
22
31
  function isColor(token) {
23
32
  switch (token.type) {
@@ -36,6 +45,10 @@ function isColor(token) {
36
45
  function isClear(token) {
37
46
  return token.type === "color-token" && (token.raw === "#clear" || token.raw.startsWith("#clear."));
38
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
+ }
39
52
  /**
40
53
  * The weight a `#name.N` fade stands for, written the way Tasty compiles the
41
54
  * opacity: `.04` is `4%`, `.5` is `50%`, `.$hover` is `($hover * 100%)`.
@@ -49,10 +62,33 @@ function splitFade(token) {
49
62
  if (token.type !== "color-token" || token.opacity === void 0) return null;
50
63
  return {
51
64
  color: token.raw.slice(0, -(token.opacity.length + 1)),
65
+ opacity: token.opacity,
52
66
  weight: opacityWeight(token.opacity)
53
67
  };
54
68
  }
55
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
+ /**
56
92
  * The comma mix that paints what `'<base> <overlay>'` painted, or `null` when
57
93
  * the overlay's alpha is not written on it. Exact when the overlay token holds
58
94
  * an opaque color: drawing it at alpha `a` over the base is the base mixed with
@@ -65,6 +101,38 @@ function rewriteOverlay(base, overlay) {
65
101
  if (fade.weight === "0%") return base;
66
102
  return `${fade.color} ${fade.weight}, ${base}`;
67
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
+ }
68
136
  /** A token as written. */
69
137
  function tokenText(token) {
70
138
  return "raw" in token ? token.raw : token.value;
@@ -75,12 +143,16 @@ var prefer_fill_mix_default = createRule({
75
143
  type: "suggestion",
76
144
  fixable: "code",
77
145
  hasSuggestions: true,
78
- docs: { description: "Replace deprecated space-separated two-color fills with a comma mix ('#dark 4%, #surface'), and catch mix parts weighted by their opacity" },
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" },
79
147
  messages: {
80
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}}'.",
81
- twoColorFillManual: "Two space-separated colors in '{{property}}' are deprecated: the second is painted as a background-image layer that contrast checkers cannot read. Mix an opaque overlay color in by weight ('#overlay 4%, #base'), or use an opaque overlay alone.",
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}}",
82
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}}'.",
83
- useWeight: "Use '{{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}}'."
84
156
  },
85
157
  schema: []
86
158
  },
@@ -88,14 +160,19 @@ var prefer_fill_mix_default = createRule({
88
160
  create(context) {
89
161
  const tastyVersion = getInstalledTastyVersion(context.filename);
90
162
  if (!tastyVersion || !semver.satisfies(tastyVersion, `>=${FILL_MIX_TASTY_VERSION}`)) return {};
163
+ const compound = semver.satisfies(tastyVersion, `>=${COMPOUND_COLOR_TASTY_VERSION}`);
91
164
  const ctx = new TastyContext(context);
92
165
  function checkSpaceSeparated(tokens, colorCount, node, property) {
93
- const suggestion = tokens.length === 2 && colorCount === 2 ? rewriteOverlay(tokenText(tokens[0]), tokens[1]) : null;
166
+ const rewrite = compound ? rewriteOverlayAsCompound : rewriteOverlay;
167
+ const suggestion = tokens.length === 2 && colorCount === 2 ? rewrite(tokenText(tokens[0]), tokens[1]) : null;
94
168
  if (suggestion === null) {
95
169
  context.report({
96
170
  node,
97
171
  messageId: "twoColorFillManual",
98
- data: { property }
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
+ }
99
176
  });
100
177
  return;
101
178
  }
@@ -109,39 +186,73 @@ var prefer_fill_mix_default = createRule({
109
186
  fix: (fixer) => replaceStringValue(fixer, node, suggestion)
110
187
  });
111
188
  }
112
- function checkMix(value, groups, node) {
113
- const parts = splitTopLevelCommas(value).map((part) => part.trim());
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) {
114
195
  for (const [i, tokens] of groups.entries()) {
115
196
  if (tokens.length !== 1) continue;
116
197
  const [token] = tokens;
117
198
  const other = parts[1 - i];
118
- let replacement = null;
119
- if (isClear(token)) replacement = other;
120
- else {
121
- const fade = splitFade(token);
122
- if (fade) {
123
- const weighted = [...parts];
124
- weighted[i] = `${fade.color} ${fade.weight}`;
125
- replacement = weighted.join(", ");
126
- }
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(", ");
127
216
  }
128
- if (replacement === null) continue;
129
- const suggestion = replacement;
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;
130
229
  context.report({
131
230
  node,
132
- messageId: "translucentMixPart",
231
+ messageId: compound ? "translucentCommaMix" : "translucentMixPart",
133
232
  data: {
134
- part: parts[i],
233
+ property,
234
+ part,
135
235
  suggestion
136
236
  },
137
237
  suggest: [{
138
- messageId: "useWeight",
238
+ messageId: compound ? "useCompound" : "useWeight",
139
239
  data: { suggestion },
140
240
  fix: (fixer) => replaceStringValue(fixer, node, suggestion)
141
241
  }]
142
242
  });
143
243
  return;
144
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
+ });
145
256
  }
146
257
  function checkValue(value, node, property) {
147
258
  const { groups } = parseValue(value);
@@ -150,7 +261,12 @@ var prefer_fill_mix_default = createRule({
150
261
  if (tokenGroups.length === 1) {
151
262
  const colorCount = tokenGroups[0].filter(isColor).length;
152
263
  if (colorCount >= 2) checkSpaceSeparated(tokenGroups[0], colorCount, node, property);
153
- } else if (tokenGroups.length === 2) checkMix(value, tokenGroups, node);
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
+ });
154
270
  }
155
271
  return stringStyleValueListeners(ctx, (value, node, property) => {
156
272
  if (FILL_PROPERTIES.has(property)) checkValue(value, node, property);
@@ -1 +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\n/** The first `@tenphi/tasty` release that mixes comma-separated fill colors. */\nconst FILL_MIX_TASTY_VERSION = '3.15.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/** 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/**\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; 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 weight: opacityWeight(token.opacity),\n };\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/** 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 space-separated two-color fills with a comma mix ('#dark 4%, #surface'), 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. Mix an opaque overlay color in by weight ('#overlay 4%, #base'), or use an opaque overlay alone.\",\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 },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n // The comma mix is new syntax: rewriting to it for an older Tasty would\n // 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\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 suggestion =\n tokens.length === 2 && colorCount === 2\n ? rewriteOverlay(tokenText(tokens[0]), tokens[1])\n : null;\n\n if (suggestion === null) {\n context.report({\n node,\n messageId: 'twoColorFillManual',\n data: { property },\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 function checkMix(\n value: string,\n groups: ValueToken[][],\n node: TSESTree.Node,\n ): void {\n const parts = splitTopLevelCommas(value).map((part) => part.trim());\n\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 let replacement: string | null = null;\n\n if (isClear(token)) {\n // A transparent color mixes in nothing.\n replacement = other;\n } else {\n const fade = splitFade(token);\n if (fade) {\n const weighted = [...parts];\n weighted[i] = `${fade.color} ${fade.weight}`;\n replacement = weighted.join(', ');\n }\n }\n\n if (replacement === null) continue;\n\n const suggestion = replacement;\n context.report({\n node,\n messageId: 'translucentMixPart',\n data: { part: parts[i], suggestion },\n suggest: [\n {\n messageId: 'useWeight',\n data: { suggestion },\n fix: (fixer) => replaceStringValue(fixer, node, suggestion),\n },\n ],\n });\n return;\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);\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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"}
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.14.0",
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.29.8",
54
+ "@changesets/cli": "^2.31.1",
55
55
  "@eslint/js": "^10.0.1",
56
- "@tenphi/tasty": "^3.14.0",
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",