@tenphi/eslint-plugin-tasty 1.14.0 → 1.16.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 +7 -4
- package/dist/parsers/value-parser.js +37 -0
- package/dist/parsers/value-parser.js.map +1 -1
- package/dist/rules/consistent-token-usage.js +25 -8
- package/dist/rules/consistent-token-usage.js.map +1 -1
- package/dist/rules/prefer-fill-mix.js +138 -22
- package/dist/rules/prefer-fill-mix.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -263,12 +263,12 @@ not matched.
|
|
|
263
263
|
| `tasty/no-classname-prop` | warn | `className` on recognized Tasty components; keep styling in Tasty or use `data-element` for sub-elements ([details](docs/rules/no-classname-prop.md)) |
|
|
264
264
|
| `tasty/no-styles-prop` | warn | Instance `styles` on recognized Tasty components; use `tokens`, `mods`, exposed props/variants, or `tasty(Component, { styles })` ([details](docs/rules/no-styles-prop.md)) |
|
|
265
265
|
| `tasty/no-protected-restyle` | warn | Restyling a design system's `protectedComponents`: instance `styles` and `*Styles`, appearance style props, and `tasty()` wrappers that restyle them, followed through aliases, wrappers and relative re-exports. Inactive without the config ([details](docs/rules/no-protected-restyle.md)) |
|
|
266
|
-
| `tasty/consistent-token-usage` | warn | Nonzero raw pixel lengths, including compounds, expressions and numeric inputs to enhanced length handlers |
|
|
266
|
+
| `tasty/consistent-token-usage` | warn | Nonzero raw pixel lengths, including compounds, expressions and numeric inputs to enhanced length handlers. The `pixels: 'replaceable'` option reports only lengths with an exact unit, such as `8px` → `1x` ([details](docs/rules/consistent-token-usage.md)) |
|
|
267
267
|
| `tasty/prefer-auto-calc` | warn | `calc(...)` instead of Tasty auto-calc `(...)` |
|
|
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
|
|
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
|
|
|
@@ -565,8 +565,11 @@ are checked only for enhanced handlers that convert them to pixels; unitless
|
|
|
565
565
|
properties such as `opacity` and `zIndex` are unaffected. Known equivalents still
|
|
566
566
|
have suggestions (`8px` → `1x`, radius `6px` → `1r`, border `1px` → `1bw`). These
|
|
567
567
|
assume the conventional scale defaults; review them against your design system.
|
|
568
|
-
|
|
569
|
-
|
|
568
|
+
Font size, line height and letter spacing are on the type scale, so they get no
|
|
569
|
+
`x` suggestion.
|
|
570
|
+
Other pixels are report-only. With the `pixels: 'replaceable'` option, they are
|
|
571
|
+
not reported, and only lengths with an exact unit are. Zero, token definitions,
|
|
572
|
+
quoted CSS text, and URLs are allowed. Quotes and URLs are also excluded from token and raw-color checks.
|
|
570
573
|
Range-based fixes are withheld when JavaScript escapes or JSX entities make
|
|
571
574
|
source offsets differ from the decoded value.
|
|
572
575
|
|
|
@@ -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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"}
|
|
@@ -17,6 +17,15 @@ import { replaceInStringValue } from "../fix-utils.js";
|
|
|
17
17
|
* happened to use.
|
|
18
18
|
*/
|
|
19
19
|
const BORDER_WIDTH_PROPERTIES = new Set(["border", ...LOGICAL_BORDER_STYLES]);
|
|
20
|
+
/**
|
|
21
|
+
* Lengths on the type scale, which presets set. `x` is the spacing scale, so a
|
|
22
|
+
* `16px` line height equals `2x` only by coincidence and gets no suggestion.
|
|
23
|
+
*/
|
|
24
|
+
const TYPE_SCALE_PROPERTIES = new Set([
|
|
25
|
+
"fontSize",
|
|
26
|
+
"lineHeight",
|
|
27
|
+
"letterSpacing"
|
|
28
|
+
]);
|
|
20
29
|
/** Enhanced handlers that convert numeric inputs to pixels. Unitless CSS stays out. */
|
|
21
30
|
const NUMERIC_PIXEL_PROPERTIES = new Set([
|
|
22
31
|
...LOGICAL_STYLES,
|
|
@@ -81,32 +90,40 @@ var consistent_token_usage_default = createRule({
|
|
|
81
90
|
preferToken: "Consider using '{{suggestion}}' instead of '{{raw}}'. For an intentional pixel value, explicitly disable tasty/consistent-token-usage on this line with a reason.",
|
|
82
91
|
replaceWithToken: "Replace '{{raw}}' with '{{suggestion}}'"
|
|
83
92
|
},
|
|
84
|
-
schema: [
|
|
93
|
+
schema: [{
|
|
94
|
+
type: "object",
|
|
95
|
+
properties: { pixels: {
|
|
96
|
+
type: "string",
|
|
97
|
+
enum: ["all", "replaceable"]
|
|
98
|
+
} },
|
|
99
|
+
additionalProperties: false
|
|
100
|
+
}]
|
|
85
101
|
},
|
|
86
|
-
defaultOptions: [],
|
|
87
|
-
create(context) {
|
|
102
|
+
defaultOptions: [{ pixels: "all" }],
|
|
103
|
+
create(context, [{ pixels = "all" }]) {
|
|
88
104
|
const ctx = new TastyContext(context);
|
|
89
105
|
function checkValue(property, value, node) {
|
|
90
106
|
if (/^[#$]/.test(property)) return;
|
|
91
|
-
const
|
|
107
|
+
const lengths = /* @__PURE__ */ new Map();
|
|
92
108
|
for (const word of scanValueWords(value)) {
|
|
93
109
|
if (!/^[+-]?(?:\d*\.\d+|\d+\.?\d*)(?:e[+-]?\d+)?px$/i.test(word.value)) continue;
|
|
94
110
|
const amount = Number(word.value.slice(0, -2));
|
|
95
111
|
if (amount === 0) continue;
|
|
96
112
|
const raw = word.value;
|
|
97
113
|
const canonical = `${amount}px`;
|
|
98
|
-
const suggestion = property === "radius" && amount === 6 ? "1r" : BORDER_WIDTH_PROPERTIES.has(property) && amount === 1 ? "1bw" : PX_TO_UNIT[canonical];
|
|
99
|
-
const edits =
|
|
114
|
+
const suggestion = property === "radius" && amount === 6 ? "1r" : BORDER_WIDTH_PROPERTIES.has(property) && amount === 1 ? "1bw" : TYPE_SCALE_PROPERTIES.has(property) ? void 0 : PX_TO_UNIT[canonical];
|
|
115
|
+
const edits = lengths.get(raw) ?? [];
|
|
100
116
|
edits.push({
|
|
101
117
|
start: word.offset,
|
|
102
118
|
end: word.offset + raw.length,
|
|
103
119
|
replacement: suggestion ?? ""
|
|
104
120
|
});
|
|
105
|
-
|
|
121
|
+
lengths.set(raw, edits);
|
|
106
122
|
}
|
|
107
|
-
for (const [raw, edits] of
|
|
123
|
+
for (const [raw, edits] of lengths) {
|
|
108
124
|
const suggestion = edits[0].replacement;
|
|
109
125
|
if (!suggestion) {
|
|
126
|
+
if (pixels === "replaceable") continue;
|
|
110
127
|
context.report({
|
|
111
128
|
node,
|
|
112
129
|
messageId: "rawPixelValue",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consistent-token-usage.js","names":[],"sources":["../../src/rules/consistent-token-usage.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { styleValueListeners } from '../style-values.js';\nimport { LOGICAL_BORDER_STYLES, LOGICAL_STYLES } from '../constants.js';\nimport { getStringValue } from '../utils.js';\nimport { replaceInStringValue } from '../fix-utils.js';\nimport { scanValueWords } from '../value-words.js';\n\ntype MessageIds = 'preferToken' | 'rawPixelValue' | 'replaceWithToken';\n\n/**\n * Properties whose value carries a border width, so a `1px` in one is the\n * `1bw` token spelled out.\n *\n * The logical axes belong here for the same reason `border` does: they take the\n * same width/style/colour value, parsed by the same `parseBorderValue`. Leaving\n * them out would make the advice depend on which axis vocabulary the author\n * happened to use.\n */\nconst BORDER_WIDTH_PROPERTIES = new Set<string>([\n 'border',\n ...LOGICAL_BORDER_STYLES,\n]);\n\n/** Enhanced handlers that convert numeric inputs to pixels. Unitless CSS stays out. */\nconst NUMERIC_PIXEL_PROPERTIES = new Set<string>([\n ...LOGICAL_STYLES,\n 'gap',\n 'radius',\n 'outlineOffset',\n 'width',\n 'minWidth',\n 'maxWidth',\n 'height',\n 'minHeight',\n 'maxHeight',\n 'padding',\n 'paddingTop',\n 'paddingRight',\n 'paddingBottom',\n 'paddingLeft',\n 'margin',\n 'marginTop',\n 'marginRight',\n 'marginBottom',\n 'marginLeft',\n 'inset',\n 'top',\n 'right',\n 'bottom',\n 'left',\n 'border',\n 'borderTop',\n 'borderRight',\n 'borderBottom',\n 'borderLeft',\n 'scrollPadding',\n 'scrollPaddingTop',\n 'scrollPaddingRight',\n 'scrollPaddingBottom',\n 'scrollPaddingLeft',\n 'scrollMargin',\n 'scrollMarginTop',\n 'scrollMarginRight',\n 'scrollMarginBottom',\n 'scrollMarginLeft',\n]);\n\nconst PX_TO_UNIT: Record<string, string> = {\n '8px': '1x',\n '16px': '2x',\n '24px': '3x',\n '32px': '4x',\n '40px': '5x',\n '48px': '6x',\n '56px': '7x',\n '64px': '8x',\n};\n\nexport default createRule<[], MessageIds>({\n name: 'consistent-token-usage',\n meta: {\n type: 'suggestion',\n hasSuggestions: true,\n docs: {\n description:\n 'Suggest using design tokens and custom units instead of raw CSS values',\n },\n messages: {\n rawPixelValue:\n \"Use a design token or custom unit instead of raw pixel value '{{raw}}'. For an intentional pixel value, explicitly disable tasty/consistent-token-usage on this line with a reason.\",\n preferToken:\n \"Consider using '{{suggestion}}' instead of '{{raw}}'. For an intentional pixel value, explicitly disable tasty/consistent-token-usage on this line with a reason.\",\n replaceWithToken: \"Replace '{{raw}}' with '{{suggestion}}'\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkValue(\n property: string,\n value: string,\n node: TSESTree.Node,\n ): void {\n // Definitions establish the scale itself; quoted text and URLs are opaque.\n if (/^[#$]/.test(property)) return;\n const
|
|
1
|
+
{"version":3,"file":"consistent-token-usage.js","names":[],"sources":["../../src/rules/consistent-token-usage.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { styleValueListeners } from '../style-values.js';\nimport { LOGICAL_BORDER_STYLES, LOGICAL_STYLES } from '../constants.js';\nimport { getStringValue } from '../utils.js';\nimport { replaceInStringValue } from '../fix-utils.js';\nimport { scanValueWords } from '../value-words.js';\n\ntype MessageIds = 'preferToken' | 'rawPixelValue' | 'replaceWithToken';\n\n/**\n * Which pixel lengths to report: every one, or only those with an exact\n * replacement, so a project can enforce the known units without listing every\n * layout size it chose in pixels.\n */\ntype Pixels = 'all' | 'replaceable';\n\n/**\n * Properties whose value carries a border width, so a `1px` in one is the\n * `1bw` token spelled out.\n *\n * The logical axes belong here for the same reason `border` does: they take the\n * same width/style/colour value, parsed by the same `parseBorderValue`. Leaving\n * them out would make the advice depend on which axis vocabulary the author\n * happened to use.\n */\nconst BORDER_WIDTH_PROPERTIES = new Set<string>([\n 'border',\n ...LOGICAL_BORDER_STYLES,\n]);\n\n/**\n * Lengths on the type scale, which presets set. `x` is the spacing scale, so a\n * `16px` line height equals `2x` only by coincidence and gets no suggestion.\n */\nconst TYPE_SCALE_PROPERTIES = new Set<string>([\n 'fontSize',\n 'lineHeight',\n 'letterSpacing',\n]);\n\n/** Enhanced handlers that convert numeric inputs to pixels. Unitless CSS stays out. */\nconst NUMERIC_PIXEL_PROPERTIES = new Set<string>([\n ...LOGICAL_STYLES,\n 'gap',\n 'radius',\n 'outlineOffset',\n 'width',\n 'minWidth',\n 'maxWidth',\n 'height',\n 'minHeight',\n 'maxHeight',\n 'padding',\n 'paddingTop',\n 'paddingRight',\n 'paddingBottom',\n 'paddingLeft',\n 'margin',\n 'marginTop',\n 'marginRight',\n 'marginBottom',\n 'marginLeft',\n 'inset',\n 'top',\n 'right',\n 'bottom',\n 'left',\n 'border',\n 'borderTop',\n 'borderRight',\n 'borderBottom',\n 'borderLeft',\n 'scrollPadding',\n 'scrollPaddingTop',\n 'scrollPaddingRight',\n 'scrollPaddingBottom',\n 'scrollPaddingLeft',\n 'scrollMargin',\n 'scrollMarginTop',\n 'scrollMarginRight',\n 'scrollMarginBottom',\n 'scrollMarginLeft',\n]);\n\nconst PX_TO_UNIT: Record<string, string> = {\n '8px': '1x',\n '16px': '2x',\n '24px': '3x',\n '32px': '4x',\n '40px': '5x',\n '48px': '6x',\n '56px': '7x',\n '64px': '8x',\n};\n\nexport default createRule<[{ pixels?: Pixels }], MessageIds>({\n name: 'consistent-token-usage',\n meta: {\n type: 'suggestion',\n hasSuggestions: true,\n docs: {\n description:\n 'Suggest using design tokens and custom units instead of raw CSS values',\n },\n messages: {\n rawPixelValue:\n \"Use a design token or custom unit instead of raw pixel value '{{raw}}'. For an intentional pixel value, explicitly disable tasty/consistent-token-usage on this line with a reason.\",\n preferToken:\n \"Consider using '{{suggestion}}' instead of '{{raw}}'. For an intentional pixel value, explicitly disable tasty/consistent-token-usage on this line with a reason.\",\n replaceWithToken: \"Replace '{{raw}}' with '{{suggestion}}'\",\n },\n schema: [\n {\n type: 'object',\n properties: {\n pixels: { type: 'string', enum: ['all', 'replaceable'] },\n },\n additionalProperties: false,\n },\n ],\n },\n defaultOptions: [{ pixels: 'all' }],\n create(context, [{ pixels = 'all' }]) {\n const ctx = new TastyContext(context);\n\n function checkValue(\n property: string,\n value: string,\n node: TSESTree.Node,\n ): void {\n // Definitions establish the scale itself; quoted text and URLs are opaque.\n if (/^[#$]/.test(property)) return;\n const lengths = new Map<\n string,\n { start: number; end: number; replacement: string }[]\n >();\n for (const word of scanValueWords(value)) {\n if (!/^[+-]?(?:\\d*\\.\\d+|\\d+\\.?\\d*)(?:e[+-]?\\d+)?px$/i.test(word.value))\n continue;\n const amount = Number(word.value.slice(0, -2));\n if (amount === 0) continue;\n const raw = word.value;\n const canonical = `${amount}px`;\n const suggestion =\n property === 'radius' && amount === 6\n ? '1r'\n : BORDER_WIDTH_PROPERTIES.has(property) && amount === 1\n ? '1bw'\n : TYPE_SCALE_PROPERTIES.has(property)\n ? undefined\n : PX_TO_UNIT[canonical];\n const edits = lengths.get(raw) ?? [];\n edits.push({\n start: word.offset,\n end: word.offset + raw.length,\n replacement: suggestion ?? '',\n });\n lengths.set(raw, edits);\n }\n for (const [raw, edits] of lengths) {\n const suggestion = edits[0].replacement;\n if (!suggestion) {\n if (pixels === 'replaceable') continue;\n context.report({ node, messageId: 'rawPixelValue', data: { raw } });\n continue;\n }\n context.report({\n node,\n messageId: 'preferToken',\n data: { suggestion, raw },\n suggest: [\n {\n messageId: 'replaceWithToken',\n data: { raw, suggestion },\n fix: (fixer) =>\n getStringValue(node) === null\n ? fixer.replaceText(node, `'${suggestion}'`)\n : replaceInStringValue(\n fixer,\n node,\n edits,\n context.sourceCode,\n ),\n },\n ],\n });\n }\n }\n\n return styleValueListeners(ctx, (property, node) => {\n const value = getStringValue(node);\n if (value !== null) {\n checkValue(property, value, node);\n } else if (NUMERIC_PIXEL_PROPERTIES.has(property)) {\n if (node.type === 'Literal' && typeof node.value === 'number') {\n checkValue(property, `${node.value}px`, node);\n } else if (\n node.type === 'UnaryExpression' &&\n (node.operator === '-' || node.operator === '+') &&\n node.argument.type === 'Literal' &&\n typeof node.argument.value === 'number'\n ) {\n const amount =\n node.operator === '-' ? -node.argument.value : node.argument.value;\n checkValue(property, `${amount}px`, node);\n }\n }\n });\n },\n});\n"],"mappings":"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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
|
|
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.
|
|
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
|
|
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: {
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "1.16.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.17.1",
|
|
57
57
|
"@types/node": "^22.0.0",
|
|
58
58
|
"@types/semver": "^7.8.0",
|
|
59
59
|
"@typescript-eslint/parser": "^8.56.1",
|