@tenphi/eslint-plugin-tasty 1.13.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/config.js +32 -1
- package/dist/config.js.map +1 -1
- package/dist/configs.js +1 -0
- package/dist/configs.js.map +1 -1
- package/dist/constants.js +7 -8
- package/dist/constants.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/parsers/value-parser.js +2 -1
- package/dist/parsers/value-parser.js.map +1 -1
- package/dist/property-expectations.js +10 -0
- package/dist/property-expectations.js.map +1 -1
- package/dist/rules/prefer-fill-mix.js +163 -0
- package/dist/rules/prefer-fill-mix.js.map +1 -0
- package/dist/rules/valid-value.js +24 -0
- package/dist/rules/valid-value.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"value-parser.js","names":[],"sources":["../../src/parsers/value-parser.ts"],"sourcesContent":["import { scanTokens, checkBracketBalance } from './utils.js';\nimport {\n BUILT_IN_UNITS,\n CSS_UNITS,\n NAMED_CSS_COLORS,\n SYSTEM_COLORS,\n} from '../constants.js';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type ValueToken =\n | { type: 'color-token'; name: string; opacity?: string; raw: string }\n | { type: 'color-ref'; name: string; raw: string }\n /** A CSS named color (`rebeccapurple`) or system color (`Canvas`). */\n | { type: 'color-keyword'; raw: string }\n | { type: 'custom-prop'; name: string; raw: string }\n | { type: 'custom-prop-ref'; name: string; raw: string }\n | { type: 'custom-unit'; value: number; unit: string; raw: string }\n | { type: 'css-unit'; value: number; unit: string; raw: string }\n | { type: 'number'; value: number; raw: string }\n /** An unspaced `<ratio>`, such as `16/10`; the spaced form is two parts. */\n | { type: 'ratio'; raw: string }\n | { type: 'keyword'; value: string }\n | { type: 'css-function'; name: string; args: string; raw: string }\n /**\n * A Tasty CSS `@function` call: `$$name(args)` (emits native `--name(args)`).\n * Distinct from `custom-prop-ref`, which is the bare `$$name` form that\n * `transition` uses to name a custom property.\n */\n | { type: 'tasty-function'; name: string; args: string; raw: string }\n | { type: 'string'; value: string; raw: string }\n | { type: 'important'; raw: string }\n | { type: 'group-expr'; inner: string; raw: string }\n | { type: 'unknown'; raw: string };\n\nexport interface ValueError {\n message: string;\n offset: number;\n length: number;\n raw?: string;\n}\n\nexport interface ValuePart {\n tokens: ValueToken[];\n}\n\nexport interface ValueGroup {\n parts: ValuePart[];\n}\n\nexport interface ValueParseResult {\n groups: ValueGroup[];\n errors: ValueError[];\n}\n\nexport interface ValueParserOptions {\n knownUnits?: Set<string> | string[];\n knownFuncs?: Set<string> | string[];\n skipUnitValidation?: boolean;\n skipFuncValidation?: boolean;\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst RE_UNIT_NUM = /^[+-]?(?:\\d*\\.\\d+|\\d+)([a-z][a-z0-9]*)$/;\nconst RE_NUMBER = /^[+-]?(?:\\d*\\.\\d+|\\d+)$/;\nconst RE_CSS_UNIT_NUM = /^[+-]?(?:\\d*\\.\\d+|\\d+)([a-z%]+)$/;\n/** An unspaced `<ratio>`, such as `16/10`. */\nconst RE_RATIO = /^(?:\\d*\\.\\d+|\\d+)\\/(?:\\d*\\.\\d+|\\d+)$/;\n\nconst COLOR_FUNCS = new Set([\n 'rgb',\n 'rgba',\n 'hsl',\n 'hsla',\n 'hwb',\n 'lab',\n 'lch',\n 'oklab',\n 'oklch',\n 'color',\n 'device-cmyk',\n 'gray',\n 'color-mix',\n 'color-contrast',\n]);\n\nconst CSS_FUNCS = new Set([\n 'calc',\n 'calc-size',\n 'min',\n 'max',\n 'clamp',\n 'abs',\n 'sign',\n 'round',\n 'mod',\n 'rem',\n 'sin',\n 'cos',\n 'tan',\n 'asin',\n 'acos',\n 'atan',\n 'atan2',\n 'pow',\n 'sqrt',\n 'hypot',\n 'log',\n 'exp',\n 'var',\n 'env',\n 'attr',\n 'counter',\n 'counters',\n 'image-set',\n 'cross-fade',\n 'image',\n 'paint',\n 'element',\n 'linear-gradient',\n 'radial-gradient',\n 'conic-gradient',\n 'repeating-linear-gradient',\n 'repeating-radial-gradient',\n 'repeating-conic-gradient',\n 'url',\n 'fit-content',\n 'minmax',\n 'repeat',\n 'cubic-bezier',\n 'steps',\n 'linear',\n 'rotate',\n 'scale',\n 'translate',\n 'skew',\n 'matrix',\n 'perspective',\n 'rotate3d',\n 'rotateX',\n 'rotateY',\n 'rotateZ',\n 'scale3d',\n 'scaleX',\n 'scaleY',\n 'scaleZ',\n 'translate3d',\n 'translateX',\n 'translateY',\n 'translateZ',\n 'skewX',\n 'skewY',\n 'matrix3d',\n 'blur',\n 'brightness',\n 'contrast',\n 'drop-shadow',\n 'grayscale',\n 'hue-rotate',\n 'invert',\n 'opacity',\n 'saturate',\n 'sepia',\n 'polygon',\n 'circle',\n 'ellipse',\n 'inset',\n 'path',\n 'xywh',\n 'rect',\n 'ray',\n 'light-dark',\n 'anchor',\n 'anchor-size',\n 'scroll',\n 'view',\n 'symbols',\n]);\n\nconst VALUE_KEYWORDS = new Set([\n 'auto',\n 'none',\n 'normal',\n 'inherit',\n 'initial',\n 'unset',\n 'revert',\n 'revert-layer',\n 'max-content',\n 'min-content',\n 'fit-content',\n 'stretch',\n 'transparent',\n 'currentcolor',\n 'currentColor',\n // display\n 'block',\n 'inline',\n 'inline-block',\n 'flex',\n 'inline-flex',\n 'grid',\n 'inline-grid',\n 'contents',\n 'table',\n 'table-row',\n 'table-cell',\n 'list-item',\n 'flow-root',\n // position\n 'static',\n 'relative',\n 'absolute',\n 'fixed',\n 'sticky',\n // overflow\n 'visible',\n 'hidden',\n 'scroll',\n 'clip',\n 'overlay',\n // flex/align\n 'center',\n 'start',\n 'end',\n 'flex-start',\n 'flex-end',\n 'space-between',\n 'space-around',\n 'space-evenly',\n 'baseline',\n // flow\n 'row',\n 'column',\n 'row-reverse',\n 'column-reverse',\n 'wrap',\n 'nowrap',\n 'dense',\n // border\n 'solid',\n 'dashed',\n 'dotted',\n 'double',\n 'groove',\n 'ridge',\n 'outset',\n // directional\n 'top',\n 'right',\n 'bottom',\n 'left',\n 'top-left',\n 'top-right',\n 'bottom-left',\n 'bottom-right',\n // radius shapes\n 'round',\n 'ellipse',\n 'leaf',\n 'backleaf',\n // dimension modifiers\n 'min',\n 'max',\n // text\n 'bold',\n 'bolder',\n 'lighter',\n 'italic',\n 'oblique',\n 'uppercase',\n 'lowercase',\n 'capitalize',\n 'line-through',\n 'underline',\n 'overline',\n 'wavy',\n // cursor\n 'pointer',\n 'default',\n 'text',\n 'move',\n 'grab',\n 'grabbing',\n 'not-allowed',\n 'crosshair',\n 'wait',\n 'help',\n 'col-resize',\n 'row-resize',\n 'n-resize',\n 's-resize',\n 'e-resize',\n 'w-resize',\n 'ne-resize',\n 'nw-resize',\n 'se-resize',\n 'sw-resize',\n 'ew-resize',\n 'ns-resize',\n 'zoom-in',\n 'zoom-out',\n // misc\n 'cover',\n 'contain',\n 'fill',\n 'no-repeat',\n 'repeat-x',\n 'repeat-y',\n 'border-box',\n 'padding-box',\n 'content-box',\n 'break-word',\n 'break-all',\n 'keep-all',\n 'anywhere',\n 'pre',\n 'pre-wrap',\n 'pre-line',\n 'balance',\n 'smooth',\n 'horizontal',\n 'vertical',\n 'both',\n 'mandatory',\n 'proximity',\n // white-space\n 'collapse',\n 'preserve',\n 'preserve-breaks',\n 'break-spaces',\n // text-wrap\n 'pretty',\n 'stable',\n // will-change\n 'transform',\n 'opacity',\n // animation\n 'infinite',\n 'alternate',\n 'alternate-reverse',\n 'reverse',\n 'forwards',\n 'backwards',\n 'running',\n 'paused',\n 'ease',\n 'ease-in',\n 'ease-out',\n 'ease-in-out',\n 'step-start',\n 'step-end',\n // scrollbar\n 'thin',\n 'always',\n 'both-edges',\n // width/height\n 'fixed',\n // shadow\n 'inset',\n // textOverflow\n 'clip',\n // other\n 'ltr',\n 'rtl',\n 'embed',\n 'isolate',\n 'isolate-override',\n 'plaintext',\n 'horizontal-tb',\n 'vertical-rl',\n 'vertical-lr',\n 'sideways-rl',\n 'sideways-lr',\n 'monospace',\n 'serif',\n 'sans-serif',\n 'cursive',\n 'fantasy',\n 'system-ui',\n 'ui-serif',\n 'ui-sans-serif',\n 'ui-monospace',\n 'ui-rounded',\n 'to',\n // misc\n 'strong',\n 'tight',\n 'icon',\n]);\n\n// ============================================================================\n// Classifier\n// ============================================================================\n\nfunction toSet(input?: Set<string> | string[]): Set<string> {\n if (!input) return new Set();\n return input instanceof Set ? input : new Set(input);\n}\n\nfunction classifyToken(\n raw: string,\n offset: number,\n errors: ValueError[],\n opts: ValueParserOptions,\n): ValueToken {\n const token = raw.trim();\n if (!token) return { type: 'keyword', value: '' };\n\n // Quoted strings\n if (\n (token.startsWith('\"') && token.endsWith('\"')) ||\n (token.startsWith(\"'\") && token.endsWith(\"'\"))\n ) {\n return { type: 'string', value: token.slice(1, -1), raw: token };\n }\n\n // !important\n if (token === '!important') {\n return { type: 'important', raw: token };\n }\n\n // Double prefix: $$name(...) is a Tasty `@function` call; bare $$name is the\n // custom-property reference `transition` uses. The call form must be checked\n // first, or the bare-name regex rejects `$$negative(10px)` outright.\n if (token.startsWith('$$') || token.startsWith('##')) {\n const call = classifyTastyFunctionCall(token, offset, errors);\n if (call) return call;\n }\n\n // Double prefix: $$name (custom property reference for transitions)\n if (token.startsWith('$$')) {\n const name = token.slice(2);\n if (/^[a-z_][a-z0-9-_]*$/i.test(name)) {\n return { type: 'custom-prop-ref', name, raw: token };\n }\n errors.push({\n message: `Invalid custom property reference '${token}'.`,\n offset,\n length: token.length,\n raw: token,\n });\n return { type: 'unknown', raw: token };\n }\n\n // Double prefix: ##name (color reference for transitions)\n if (token.startsWith('##')) {\n const name = token.slice(2);\n if (/^[a-z_][a-z0-9-_]*$/i.test(name)) {\n return { type: 'color-ref', name, raw: token };\n }\n errors.push({\n message: `Invalid color reference '${token}'.`,\n offset,\n length: token.length,\n raw: token,\n });\n return { type: 'unknown', raw: token };\n }\n\n // Custom property: $name\n if (token.startsWith('$')) {\n const name = token.slice(1);\n if (/^[a-z_][a-z0-9-_]*$/i.test(name)) {\n return { type: 'custom-prop', name, raw: token };\n }\n errors.push({\n message: `Invalid custom property syntax '${token}'.`,\n offset,\n length: token.length,\n raw: token,\n });\n return { type: 'unknown', raw: token };\n }\n\n // Color token: #name, #name.N, #name.$prop, or bare # (error)\n if (token.startsWith('#')) {\n if (token.length === 1) {\n errors.push({\n message: 'Empty color token name.',\n offset,\n length: 1,\n raw: token,\n });\n return { type: 'unknown', raw: token };\n }\n return classifyColorToken(token, offset, errors);\n }\n\n // Parenthesized expression: (expr) — fallback, auto-calc, etc.\n if (token.startsWith('(') && token.endsWith(')')) {\n return { type: 'group-expr', inner: token.slice(1, -1), raw: token };\n }\n\n // Function call: name(...)\n const openIdx = token.indexOf('(');\n if (openIdx > 0 && token.endsWith(')')) {\n const fname = token.slice(0, openIdx);\n const args = token.slice(openIdx + 1, -1);\n\n if (opts.skipFuncValidation) {\n return { type: 'css-function', name: fname, args, raw: token };\n }\n\n const knownFuncs = toSet(opts.knownFuncs);\n\n if (\n COLOR_FUNCS.has(fname) ||\n CSS_FUNCS.has(fname) ||\n knownFuncs.has(fname)\n ) {\n return { type: 'css-function', name: fname, args, raw: token };\n }\n\n // Unknown function — still return as css-function but flag it\n errors.push({\n message: `Unknown function '${fname}()'.`,\n offset,\n length: token.length,\n raw: token,\n });\n return { type: 'css-function', name: fname, args, raw: token };\n }\n\n // Unit number: 2x, 1.5r, 10px, 50%, 1fr\n const unitMatch = token.match(RE_UNIT_NUM);\n if (unitMatch) {\n const unit = unitMatch[1];\n const numVal = parseFloat(token.slice(0, -unit.length));\n\n if (opts.skipUnitValidation) {\n return { type: 'custom-unit', value: numVal, unit, raw: token };\n }\n\n const knownUnits = toSet(opts.knownUnits);\n\n if (BUILT_IN_UNITS.has(unit) || knownUnits.has(unit)) {\n return { type: 'custom-unit', value: numVal, unit, raw: token };\n }\n if (CSS_UNITS.has(unit)) {\n return { type: 'css-unit', value: numVal, unit, raw: token };\n }\n\n errors.push({\n message: `Unknown unit '${unit}' in '${token}'.`,\n offset,\n length: token.length,\n raw: token,\n });\n return { type: 'unknown', raw: token };\n }\n\n // CSS unit with % (RE_UNIT_NUM doesn't match % since it expects alpha)\n const cssUnitMatch = token.match(RE_CSS_UNIT_NUM);\n if (cssUnitMatch) {\n const unit = cssUnitMatch[1];\n const numVal = parseFloat(token.slice(0, -unit.length));\n if (CSS_UNITS.has(unit)) {\n return { type: 'css-unit', value: numVal, unit, raw: token };\n }\n }\n\n // Plain number\n if (RE_NUMBER.test(token)) {\n return { type: 'number', value: parseFloat(token), raw: token };\n }\n\n // Unspaced ratio: 16/10\n if (RE_RATIO.test(token)) {\n return { type: 'ratio', raw: token };\n }\n\n // Tasty merge directive (@inherit)\n if (token === '@inherit') {\n return { type: 'keyword', value: token };\n }\n\n // Known keyword\n if (VALUE_KEYWORDS.has(token) || VALUE_KEYWORDS.has(token.toLowerCase())) {\n return { type: 'keyword', value: token };\n }\n\n // Color keyword: no token syntax, so the keyword itself is the color.\n // Matched case-insensitively, as CSS and Tasty match them.\n const lower = token.toLowerCase();\n if (NAMED_CSS_COLORS.has(lower) || SYSTEM_COLORS.has(lower)) {\n return { type: 'color-keyword', raw: token };\n }\n\n // CSS custom property function var(--name)\n if (token.startsWith('var(') && token.endsWith(')')) {\n return {\n type: 'css-function',\n name: 'var',\n args: token.slice(4, -1),\n raw: token,\n };\n }\n\n // Unknown token\n return { type: 'unknown', raw: token };\n}\n\n/**\n * Classify a `$$name(...)` / `##name(...)` Tasty `@function` call.\n *\n * Returns `null` when the token is not a call, so the caller falls through to the\n * bare `$$name` / `##name` reference handling that `transition` relies on.\n */\nfunction classifyTastyFunctionCall(\n token: string,\n offset: number,\n errors: ValueError[],\n): ValueToken | null {\n const openIdx = token.indexOf('(');\n\n if (openIdx <= 2 || !token.endsWith(')')) return null;\n\n const name = token.slice(2, openIdx);\n const args = token.slice(openIdx + 1, -1);\n\n if (!/^[a-z_][a-z0-9-_]*$/i.test(name)) {\n errors.push({\n message: `Invalid function name in '${token}'.`,\n offset,\n length: token.length,\n raw: token,\n });\n return { type: 'unknown', raw: token };\n }\n\n return { type: 'tasty-function', name, args, raw: token };\n}\n\nfunction classifyColorToken(\n token: string,\n offset: number,\n errors: ValueError[],\n): ValueToken {\n const raw = token;\n\n // Strip leading #\n const name = token.slice(1);\n\n if (name.length === 0) {\n errors.push({\n message: 'Empty color token name.',\n offset,\n length: token.length,\n raw,\n });\n return { type: 'unknown', raw };\n }\n\n // Check for opacity suffix\n const dotIndex = name.indexOf('.');\n if (dotIndex !== -1) {\n const tokenName = name.slice(0, dotIndex);\n const opacitySuffix = name.slice(dotIndex + 1);\n\n if (tokenName.length === 0) {\n errors.push({\n message: 'Empty color token name before opacity.',\n offset,\n length: token.length,\n raw,\n });\n return { type: 'unknown', raw };\n }\n\n // Dynamic opacity from CSS custom property\n if (opacitySuffix.startsWith('$')) {\n return {\n type: 'color-token',\n name: tokenName,\n opacity: opacitySuffix,\n raw,\n };\n }\n\n if (opacitySuffix.length === 0) {\n errors.push({\n message: 'Trailing dot with no opacity value.',\n offset,\n length: token.length,\n raw,\n });\n return { type: 'unknown', raw };\n }\n\n // Tasty reads the digits as a decimal fraction (`.5` and `.50` are 50%,\n // `.150` is 15%), so any digit string is in range.\n if (!/^\\d+$/.test(opacitySuffix)) {\n errors.push({\n message: /^-\\d+$/.test(opacitySuffix)\n ? 'Opacity cannot be negative.'\n : `Invalid opacity value '${opacitySuffix}'.`,\n offset,\n length: token.length,\n raw,\n });\n return { type: 'unknown', raw };\n }\n\n return {\n type: 'color-token',\n name: tokenName,\n opacity: opacitySuffix,\n raw,\n };\n }\n\n return { type: 'color-token', name, raw };\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Parse a style value string into typed tokens organized by\n * comma-separated groups and slash-separated parts.\n */\nexport function parseValue(\n src: string,\n opts: ValueParserOptions = {},\n): ValueParseResult {\n const errors: ValueError[] = [];\n\n // Check bracket balance first\n const bracketError = checkBracketBalance(src);\n if (bracketError) {\n errors.push({\n message: bracketError.message,\n offset: bracketError.position,\n length: 1,\n });\n return { groups: [], errors };\n }\n\n const scanned = scanTokens(src);\n\n const groups: ValueGroup[] = [];\n let currentParts: ValuePart[] = [];\n let currentTokens: ValueToken[] = [];\n\n const endPart = () => {\n if (currentTokens.length > 0) {\n currentParts.push({ tokens: currentTokens });\n currentTokens = [];\n }\n };\n\n const endGroup = () => {\n endPart();\n if (currentParts.length > 0) {\n groups.push({ parts: currentParts });\n } else {\n groups.push({ parts: [{ tokens: [] }] });\n }\n currentParts = [];\n };\n\n for (const scanned_token of scanned) {\n if (scanned_token.value) {\n const classified = classifyToken(\n scanned_token.value,\n scanned_token.offset,\n errors,\n opts,\n );\n currentTokens.push(classified);\n }\n if (scanned_token.isSlash) endPart();\n if (scanned_token.isComma) endGroup();\n }\n\n // Push final group\n if (\n currentTokens.length > 0 ||\n currentParts.length > 0 ||\n groups.length === 0\n ) {\n endGroup();\n }\n\n return { groups, errors };\n}\n\n/**\n * Extract all tokens of a specific type from a parse result.\n */\nexport function extractTokensByType<T extends ValueToken['type']>(\n result: ValueParseResult,\n type: T,\n): Extract<ValueToken, { type: T }>[] {\n const tokens: Extract<ValueToken, { type: T }>[] = [];\n for (const group of result.groups) {\n for (const part of group.parts) {\n for (const token of part.tokens) {\n if (token.type === type) {\n tokens.push(token as Extract<ValueToken, { type: T }>);\n }\n }\n }\n }\n return tokens;\n}\n\n/**\n * Get a flat list of all tokens from a parse result.\n */\nexport function flattenTokens(result: ValueParseResult): ValueToken[] {\n const tokens: ValueToken[] = [];\n for (const group of result.groups) {\n for (const part of group.parts) {\n tokens.push(...part.tokens);\n }\n }\n return tokens;\n}\n"],"mappings":";;;;AAoEA,MAAM,cAAc;AACpB,MAAM,YAAY;AAClB,MAAM,kBAAkB;;AAExB,MAAM,WAAW;AAEjB,MAAM,cAAc,IAAI,IAAI;CAC1B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAEF,MAAM,YAAY,IAAI,IAAI;CACxB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAEF,MAAM,iBAAiB,IAAI,IAAI;CAC7B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CAEA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAC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"}
|
|
1
|
+
{"version":3,"file":"value-parser.js","names":[],"sources":["../../src/parsers/value-parser.ts"],"sourcesContent":["import { scanTokens, checkBracketBalance } from './utils.js';\nimport {\n BUILT_IN_UNITS,\n CSS_UNITS,\n NAMED_CSS_COLORS,\n SYSTEM_COLORS,\n} from '../constants.js';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type ValueToken =\n | { type: 'color-token'; name: string; opacity?: string; raw: string }\n | { type: 'color-ref'; name: string; raw: string }\n /** A CSS named color (`rebeccapurple`) or system color (`Canvas`). */\n | { type: 'color-keyword'; raw: string }\n | { type: 'custom-prop'; name: string; raw: string }\n | { type: 'custom-prop-ref'; name: string; raw: string }\n | { type: 'custom-unit'; value: number; unit: string; raw: string }\n | { type: 'css-unit'; value: number; unit: string; raw: string }\n | { type: 'number'; value: number; raw: string }\n /** An unspaced `<ratio>`, such as `16/10`; the spaced form is two parts. */\n | { type: 'ratio'; raw: string }\n | { type: 'keyword'; value: string }\n | { type: 'css-function'; name: string; args: string; raw: string }\n /**\n * A Tasty CSS `@function` call: `$$name(args)` (emits native `--name(args)`).\n * Distinct from `custom-prop-ref`, which is the bare `$$name` form that\n * `transition` uses to name a custom property.\n */\n | { type: 'tasty-function'; name: string; args: string; raw: string }\n | { type: 'string'; value: string; raw: string }\n | { type: 'important'; raw: string }\n | { type: 'group-expr'; inner: string; raw: string }\n | { type: 'unknown'; raw: string };\n\nexport interface ValueError {\n message: string;\n offset: number;\n length: number;\n raw?: string;\n}\n\nexport interface ValuePart {\n tokens: ValueToken[];\n}\n\nexport interface ValueGroup {\n parts: ValuePart[];\n}\n\nexport interface ValueParseResult {\n groups: ValueGroup[];\n errors: ValueError[];\n}\n\nexport interface ValueParserOptions {\n knownUnits?: Set<string> | string[];\n knownFuncs?: Set<string> | string[];\n skipUnitValidation?: boolean;\n skipFuncValidation?: boolean;\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst RE_UNIT_NUM = /^[+-]?(?:\\d*\\.\\d+|\\d+)([a-z][a-z0-9]*)$/;\nconst RE_NUMBER = /^[+-]?(?:\\d*\\.\\d+|\\d+)$/;\nconst RE_CSS_UNIT_NUM = /^[+-]?(?:\\d*\\.\\d+|\\d+)([a-z%]+)$/;\n/** An unspaced `<ratio>`, such as `16/10`. */\nconst RE_RATIO = /^(?:\\d*\\.\\d+|\\d+)\\/(?:\\d*\\.\\d+|\\d+)$/;\n\n/** Functions the parser classifies as colors. */\nexport const COLOR_FUNCS = new Set([\n 'rgb',\n 'rgba',\n 'hsl',\n 'hsla',\n 'hwb',\n 'lab',\n 'lch',\n 'oklab',\n 'oklch',\n 'color',\n 'device-cmyk',\n 'gray',\n 'color-mix',\n 'color-contrast',\n]);\n\nconst CSS_FUNCS = new Set([\n 'calc',\n 'calc-size',\n 'min',\n 'max',\n 'clamp',\n 'abs',\n 'sign',\n 'round',\n 'mod',\n 'rem',\n 'sin',\n 'cos',\n 'tan',\n 'asin',\n 'acos',\n 'atan',\n 'atan2',\n 'pow',\n 'sqrt',\n 'hypot',\n 'log',\n 'exp',\n 'var',\n 'env',\n 'attr',\n 'counter',\n 'counters',\n 'image-set',\n 'cross-fade',\n 'image',\n 'paint',\n 'element',\n 'linear-gradient',\n 'radial-gradient',\n 'conic-gradient',\n 'repeating-linear-gradient',\n 'repeating-radial-gradient',\n 'repeating-conic-gradient',\n 'url',\n 'fit-content',\n 'minmax',\n 'repeat',\n 'cubic-bezier',\n 'steps',\n 'linear',\n 'rotate',\n 'scale',\n 'translate',\n 'skew',\n 'matrix',\n 'perspective',\n 'rotate3d',\n 'rotateX',\n 'rotateY',\n 'rotateZ',\n 'scale3d',\n 'scaleX',\n 'scaleY',\n 'scaleZ',\n 'translate3d',\n 'translateX',\n 'translateY',\n 'translateZ',\n 'skewX',\n 'skewY',\n 'matrix3d',\n 'blur',\n 'brightness',\n 'contrast',\n 'drop-shadow',\n 'grayscale',\n 'hue-rotate',\n 'invert',\n 'opacity',\n 'saturate',\n 'sepia',\n 'polygon',\n 'circle',\n 'ellipse',\n 'inset',\n 'path',\n 'xywh',\n 'rect',\n 'ray',\n 'light-dark',\n 'anchor',\n 'anchor-size',\n 'scroll',\n 'view',\n 'symbols',\n]);\n\nconst VALUE_KEYWORDS = new Set([\n 'auto',\n 'none',\n 'normal',\n 'inherit',\n 'initial',\n 'unset',\n 'revert',\n 'revert-layer',\n 'max-content',\n 'min-content',\n 'fit-content',\n 'stretch',\n 'transparent',\n 'currentcolor',\n 'currentColor',\n // display\n 'block',\n 'inline',\n 'inline-block',\n 'flex',\n 'inline-flex',\n 'grid',\n 'inline-grid',\n 'contents',\n 'table',\n 'table-row',\n 'table-cell',\n 'list-item',\n 'flow-root',\n // position\n 'static',\n 'relative',\n 'absolute',\n 'fixed',\n 'sticky',\n // overflow\n 'visible',\n 'hidden',\n 'scroll',\n 'clip',\n 'overlay',\n // flex/align\n 'center',\n 'start',\n 'end',\n 'flex-start',\n 'flex-end',\n 'space-between',\n 'space-around',\n 'space-evenly',\n 'baseline',\n // flow\n 'row',\n 'column',\n 'row-reverse',\n 'column-reverse',\n 'wrap',\n 'nowrap',\n 'dense',\n // border\n 'solid',\n 'dashed',\n 'dotted',\n 'double',\n 'groove',\n 'ridge',\n 'outset',\n // directional\n 'top',\n 'right',\n 'bottom',\n 'left',\n 'top-left',\n 'top-right',\n 'bottom-left',\n 'bottom-right',\n // radius shapes\n 'round',\n 'ellipse',\n 'leaf',\n 'backleaf',\n // dimension modifiers\n 'min',\n 'max',\n // text\n 'bold',\n 'bolder',\n 'lighter',\n 'italic',\n 'oblique',\n 'uppercase',\n 'lowercase',\n 'capitalize',\n 'line-through',\n 'underline',\n 'overline',\n 'wavy',\n // cursor\n 'pointer',\n 'default',\n 'text',\n 'move',\n 'grab',\n 'grabbing',\n 'not-allowed',\n 'crosshair',\n 'wait',\n 'help',\n 'col-resize',\n 'row-resize',\n 'n-resize',\n 's-resize',\n 'e-resize',\n 'w-resize',\n 'ne-resize',\n 'nw-resize',\n 'se-resize',\n 'sw-resize',\n 'ew-resize',\n 'ns-resize',\n 'zoom-in',\n 'zoom-out',\n // misc\n 'cover',\n 'contain',\n 'fill',\n 'no-repeat',\n 'repeat-x',\n 'repeat-y',\n 'border-box',\n 'padding-box',\n 'content-box',\n 'break-word',\n 'break-all',\n 'keep-all',\n 'anywhere',\n 'pre',\n 'pre-wrap',\n 'pre-line',\n 'balance',\n 'smooth',\n 'horizontal',\n 'vertical',\n 'both',\n 'mandatory',\n 'proximity',\n // white-space\n 'collapse',\n 'preserve',\n 'preserve-breaks',\n 'break-spaces',\n // text-wrap\n 'pretty',\n 'stable',\n // will-change\n 'transform',\n 'opacity',\n // animation\n 'infinite',\n 'alternate',\n 'alternate-reverse',\n 'reverse',\n 'forwards',\n 'backwards',\n 'running',\n 'paused',\n 'ease',\n 'ease-in',\n 'ease-out',\n 'ease-in-out',\n 'step-start',\n 'step-end',\n // scrollbar\n 'thin',\n 'always',\n 'both-edges',\n // width/height\n 'fixed',\n // shadow\n 'inset',\n // textOverflow\n 'clip',\n // other\n 'ltr',\n 'rtl',\n 'embed',\n 'isolate',\n 'isolate-override',\n 'plaintext',\n 'horizontal-tb',\n 'vertical-rl',\n 'vertical-lr',\n 'sideways-rl',\n 'sideways-lr',\n 'monospace',\n 'serif',\n 'sans-serif',\n 'cursive',\n 'fantasy',\n 'system-ui',\n 'ui-serif',\n 'ui-sans-serif',\n 'ui-monospace',\n 'ui-rounded',\n 'to',\n // misc\n 'strong',\n 'tight',\n 'icon',\n]);\n\n// ============================================================================\n// Classifier\n// ============================================================================\n\nfunction toSet(input?: Set<string> | string[]): Set<string> {\n if (!input) return new Set();\n return input instanceof Set ? input : new Set(input);\n}\n\nfunction classifyToken(\n raw: string,\n offset: number,\n errors: ValueError[],\n opts: ValueParserOptions,\n): ValueToken {\n const token = raw.trim();\n if (!token) return { type: 'keyword', value: '' };\n\n // Quoted strings\n if (\n (token.startsWith('\"') && token.endsWith('\"')) ||\n (token.startsWith(\"'\") && token.endsWith(\"'\"))\n ) {\n return { type: 'string', value: token.slice(1, -1), raw: token };\n }\n\n // !important\n if (token === '!important') {\n return { type: 'important', raw: token };\n }\n\n // Double prefix: $$name(...) is a Tasty `@function` call; bare $$name is the\n // custom-property reference `transition` uses. The call form must be checked\n // first, or the bare-name regex rejects `$$negative(10px)` outright.\n if (token.startsWith('$$') || token.startsWith('##')) {\n const call = classifyTastyFunctionCall(token, offset, errors);\n if (call) return call;\n }\n\n // Double prefix: $$name (custom property reference for transitions)\n if (token.startsWith('$$')) {\n const name = token.slice(2);\n if (/^[a-z_][a-z0-9-_]*$/i.test(name)) {\n return { type: 'custom-prop-ref', name, raw: token };\n }\n errors.push({\n message: `Invalid custom property reference '${token}'.`,\n offset,\n length: token.length,\n raw: token,\n });\n return { type: 'unknown', raw: token };\n }\n\n // Double prefix: ##name (color reference for transitions)\n if (token.startsWith('##')) {\n const name = token.slice(2);\n if (/^[a-z_][a-z0-9-_]*$/i.test(name)) {\n return { type: 'color-ref', name, raw: token };\n }\n errors.push({\n message: `Invalid color reference '${token}'.`,\n offset,\n length: token.length,\n raw: token,\n });\n return { type: 'unknown', raw: token };\n }\n\n // Custom property: $name\n if (token.startsWith('$')) {\n const name = token.slice(1);\n if (/^[a-z_][a-z0-9-_]*$/i.test(name)) {\n return { type: 'custom-prop', name, raw: token };\n }\n errors.push({\n message: `Invalid custom property syntax '${token}'.`,\n offset,\n length: token.length,\n raw: token,\n });\n return { type: 'unknown', raw: token };\n }\n\n // Color token: #name, #name.N, #name.$prop, or bare # (error)\n if (token.startsWith('#')) {\n if (token.length === 1) {\n errors.push({\n message: 'Empty color token name.',\n offset,\n length: 1,\n raw: token,\n });\n return { type: 'unknown', raw: token };\n }\n 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;CA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"}
|
|
@@ -124,6 +124,16 @@ const PROPERTY_EXPECTATIONS = {
|
|
|
124
124
|
acceptsColor: true,
|
|
125
125
|
acceptsMods: ["none", "transparent"]
|
|
126
126
|
},
|
|
127
|
+
svgFill: {
|
|
128
|
+
acceptsColor: true,
|
|
129
|
+
acceptsMods: [
|
|
130
|
+
"none",
|
|
131
|
+
"transparent",
|
|
132
|
+
"context-fill",
|
|
133
|
+
"context-stroke"
|
|
134
|
+
],
|
|
135
|
+
paint: true
|
|
136
|
+
},
|
|
127
137
|
color: {
|
|
128
138
|
acceptsColor: true,
|
|
129
139
|
acceptsMods: ["none", "transparent"]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"property-expectations.js","names":[],"sources":["../src/property-expectations.ts"],"sourcesContent":["/**\n * Per-property expectations for parser bucket validation.\n *\n * After parsing a value through StyleParser.process(), each group contains\n * `colors`, `values`, and `mods` arrays. This map defines what is expected\n * for each tasty property so we can flag unexpected tokens.\n *\n * - `acceptsColor`: whether Color bucket tokens are valid\n * - `acceptsMods`: whether Mod bucket tokens are valid, and if so which ones\n * - `false` = no mods accepted (any mod is an error)\n * - `true` = any mod accepted (pass-through)\n * - `string[]` = only these specific mods are accepted\n *\n * Properties NOT listed here default to PASSTHROUGH (accept everything).\n * Only add properties that have actual restrictions.\n */\n\nexport interface PropertyExpectation {\n acceptsColor: boolean;\n acceptsMods: boolean | string[];\n /** Whether an unspaced `<ratio>` token (`16/10`) is a value here. */\n acceptsRatio?: boolean;\n}\n\nconst DIRECTIONAL_MODS = ['top', 'right', 'bottom', 'left'];\n// Output modifier accepted by every box property (`padding`, `margin`,\n// `inset`, `border`, `radius`): emit the individual CSS longhands instead of\n// the shorthand, so children can selectively inherit a single side/corner.\nconst LONGHAND_MOD = 'longhand';\n// `inset` additionally accepts `dock`: pin one edge and span its full\n// length (tasty >= 2.10). Other directional properties do not.\nconst INSET_MODS = [...DIRECTIONAL_MODS, 'dock', LONGHAND_MOD];\nconst BOX_DIRECTIONAL_MODS = [...DIRECTIONAL_MODS, LONGHAND_MOD];\nconst RADIUS_DIRECTIONAL_MODS = [\n ...DIRECTIONAL_MODS,\n 'top-left',\n 'top-right',\n 'bottom-left',\n 'bottom-right',\n LONGHAND_MOD,\n];\nconst BORDER_STYLE_MODS = [\n 'solid',\n 'dashed',\n 'dotted',\n 'double',\n 'groove',\n 'ridge',\n 'inset',\n 'outset',\n 'none',\n 'hidden',\n];\nconst DIMENSION_MODS = ['min', 'max', 'fixed'];\n// The logical axis handlers take `start`/`end` in place of the four physical\n// sides, plus the same `longhand` output modifier — it emits the two native\n// start/end declarations instead of the axis shorthand.\nconst LOGICAL_EDGE_MODS = ['start', 'end', LONGHAND_MOD];\nconst FLOW_MODS = [\n 'row',\n 'column',\n 'wrap',\n 'nowrap',\n 'dense',\n 'row-reverse',\n 'column-reverse',\n];\nconst OVERFLOW_MODS = [\n 'visible',\n 'hidden',\n 'scroll',\n 'clip',\n 'auto',\n 'overlay',\n];\nconst POSITION_MODS = ['static', 'relative', 'absolute', 'fixed', 'sticky'];\n\nconst COLOR_ONLY: PropertyExpectation = {\n acceptsColor: true,\n acceptsMods: false,\n};\n\nconst VALUE_ONLY: PropertyExpectation = {\n acceptsColor: false,\n acceptsMods: false,\n};\n\nconst PASSTHROUGH: PropertyExpectation = {\n acceptsColor: true,\n acceptsMods: true,\n};\n\n/**\n * Every `display` keyword, one word at a time: values are checked word by\n * word, so the two-value syntax (`block flex`, `inline flow-root`,\n * `list-item block`) is covered by its outside, inside and `list-item` words.\n * `grid-lanes` is CSS Grid Level 3 masonry.\n */\nconst DISPLAY_MODS = [\n // <display-outside>\n 'block',\n 'inline',\n 'run-in',\n // <display-inside>\n 'flow',\n 'flow-root',\n 'table',\n 'flex',\n 'grid',\n 'grid-lanes',\n 'ruby',\n 'math',\n // <display-listitem>\n 'list-item',\n // <display-internal>\n 'table-row-group',\n 'table-header-group',\n 'table-footer-group',\n 'table-row',\n 'table-cell',\n 'table-column-group',\n 'table-column',\n 'table-caption',\n 'ruby-base',\n 'ruby-text',\n 'ruby-base-container',\n 'ruby-text-container',\n // <display-box>\n 'contents',\n 'none',\n // <display-legacy>\n 'inline-block',\n 'inline-table',\n 'inline-flex',\n 'inline-grid',\n 'inline-grid-lanes',\n // The old flexbox boxes, kept because `-webkit-line-clamp` only truncates\n // inside one.\n '-webkit-box',\n '-webkit-inline-box',\n];\n\nexport const PROPERTY_EXPECTATIONS: Record<string, PropertyExpectation> = {\n fill: { acceptsColor: true, acceptsMods: ['none', 'transparent'] },\n color: { acceptsColor: true, acceptsMods: ['none', 'transparent'] },\n caretColor: COLOR_ONLY,\n accentColor: COLOR_ONLY,\n shadow: { acceptsColor: true, acceptsMods: ['inset'] },\n\n border: {\n acceptsColor: true,\n acceptsMods: [...BOX_DIRECTIONAL_MODS, ...BORDER_STYLE_MODS],\n },\n outline: {\n acceptsColor: true,\n acceptsMods: BORDER_STYLE_MODS,\n },\n\n radius: {\n acceptsColor: false,\n acceptsMods: [\n ...RADIUS_DIRECTIONAL_MODS,\n 'round',\n 'ellipse',\n 'leaf',\n 'backleaf',\n ],\n },\n\n padding: { acceptsColor: false, acceptsMods: BOX_DIRECTIONAL_MODS },\n margin: { acceptsColor: false, acceptsMods: BOX_DIRECTIONAL_MODS },\n fade: { acceptsColor: true, acceptsMods: DIRECTIONAL_MODS },\n inset: { acceptsColor: false, acceptsMods: INSET_MODS },\n scrollMargin: { acceptsColor: false, acceptsMods: BOX_DIRECTIONAL_MODS },\n scrollPadding: { acceptsColor: false, acceptsMods: BOX_DIRECTIONAL_MODS },\n\n // Native logical CSS shorthands. Tasty v3.8 stopped reading these in its\n // physical handlers, so they are ordinary CSS declarations now: one or two\n // lengths through the normal value parser, no colours and no modifiers. The\n // enhanced `blockPadding` / `inlinePadding` below are what take modifiers.\n paddingInline: VALUE_ONLY,\n paddingBlock: VALUE_ONLY,\n\n width: { acceptsColor: false, acceptsMods: DIMENSION_MODS },\n height: { acceptsColor: false, acceptsMods: DIMENSION_MODS },\n\n // Enhanced logical styles. Each axis/category pair mirrors its physical\n // counterpart's vocabulary with `start`/`end` in place of the four sides.\n blockPadding: { acceptsColor: false, acceptsMods: LOGICAL_EDGE_MODS },\n inlinePadding: { acceptsColor: false, acceptsMods: LOGICAL_EDGE_MODS },\n blockMargin: { acceptsColor: false, acceptsMods: LOGICAL_EDGE_MODS },\n inlineMargin: { acceptsColor: false, acceptsMods: LOGICAL_EDGE_MODS },\n blockInset: { acceptsColor: false, acceptsMods: LOGICAL_EDGE_MODS },\n inlineInset: { acceptsColor: false, acceptsMods: LOGICAL_EDGE_MODS },\n blockScrollMargin: { acceptsColor: false, acceptsMods: LOGICAL_EDGE_MODS },\n inlineScrollMargin: { acceptsColor: false, acceptsMods: LOGICAL_EDGE_MODS },\n blockScrollPadding: { acceptsColor: false, acceptsMods: LOGICAL_EDGE_MODS },\n inlineScrollPadding: { acceptsColor: false, acceptsMods: LOGICAL_EDGE_MODS },\n blockBorder: {\n acceptsColor: true,\n acceptsMods: [...LOGICAL_EDGE_MODS, ...BORDER_STYLE_MODS],\n },\n inlineBorder: {\n acceptsColor: true,\n acceptsMods: [...LOGICAL_EDGE_MODS, ...BORDER_STYLE_MODS],\n },\n blockSize: { acceptsColor: false, acceptsMods: DIMENSION_MODS },\n inlineSize: { acceptsColor: false, acceptsMods: DIMENSION_MODS },\n\n gap: VALUE_ONLY,\n columnGap: VALUE_ONLY,\n rowGap: VALUE_ONLY,\n flexBasis: VALUE_ONLY,\n flexGrow: VALUE_ONLY,\n flexShrink: VALUE_ONLY,\n flex: VALUE_ONLY,\n order: VALUE_ONLY,\n zIndex: VALUE_ONLY,\n opacity: VALUE_ONLY,\n // `<ratio>` allows optional whitespace around the slash: `16/10`, `16 / 10`.\n aspectRatio: { ...VALUE_ONLY, acceptsRatio: true },\n lineClamp: VALUE_ONLY,\n tabSize: VALUE_ONLY,\n\n flow: { acceptsColor: false, acceptsMods: FLOW_MODS },\n display: { acceptsColor: false, acceptsMods: DISPLAY_MODS },\n overflow: { acceptsColor: false, acceptsMods: OVERFLOW_MODS },\n position: { acceptsColor: false, acceptsMods: POSITION_MODS },\n};\n\n/**\n * Get expectations for a property. Properties not in the map\n * are treated as passthrough (accept everything).\n */\nexport function getExpectation(property: string): PropertyExpectation {\n return PROPERTY_EXPECTATIONS[property] ?? PASSTHROUGH;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"property-expectations.js","names":[],"sources":["../src/property-expectations.ts"],"sourcesContent":["/**\n * Per-property expectations for parser bucket validation.\n *\n * After parsing a value through StyleParser.process(), each group contains\n * `colors`, `values`, and `mods` arrays. This map defines what is expected\n * for each tasty property so we can flag unexpected tokens.\n *\n * - `acceptsColor`: whether Color bucket tokens are valid\n * - `acceptsMods`: whether Mod bucket tokens are valid, and if so which ones\n * - `false` = no mods accepted (any mod is an error)\n * - `true` = any mod accepted (pass-through)\n * - `string[]` = only these specific mods are accepted\n *\n * Properties NOT listed here default to PASSTHROUGH (accept everything).\n * Only add properties that have actual restrictions.\n */\n\nexport interface PropertyExpectation {\n acceptsColor: boolean;\n acceptsMods: boolean | string[];\n /** Whether an unspaced `<ratio>` token (`16/10`) is a value here. */\n acceptsRatio?: boolean;\n /**\n * An SVG `<paint>`, which Tasty reads as one value: it keeps the first color\n * and drops the rest, and passes a value led by `url()` through as plain CSS.\n */\n paint?: boolean;\n}\n\nconst DIRECTIONAL_MODS = ['top', 'right', 'bottom', 'left'];\n// Output modifier accepted by every box property (`padding`, `margin`,\n// `inset`, `border`, `radius`): emit the individual CSS longhands instead of\n// the shorthand, so children can selectively inherit a single side/corner.\nconst LONGHAND_MOD = 'longhand';\n// `inset` additionally accepts `dock`: pin one edge and span its full\n// length (tasty >= 2.10). Other directional properties do not.\nconst INSET_MODS = [...DIRECTIONAL_MODS, 'dock', LONGHAND_MOD];\nconst BOX_DIRECTIONAL_MODS = [...DIRECTIONAL_MODS, LONGHAND_MOD];\nconst RADIUS_DIRECTIONAL_MODS = [\n ...DIRECTIONAL_MODS,\n 'top-left',\n 'top-right',\n 'bottom-left',\n 'bottom-right',\n LONGHAND_MOD,\n];\nconst BORDER_STYLE_MODS = [\n 'solid',\n 'dashed',\n 'dotted',\n 'double',\n 'groove',\n 'ridge',\n 'inset',\n 'outset',\n 'none',\n 'hidden',\n];\nconst DIMENSION_MODS = ['min', 'max', 'fixed'];\n// The logical axis handlers take `start`/`end` in place of the four physical\n// sides, plus the same `longhand` output modifier — it emits the two native\n// start/end declarations instead of the axis shorthand.\nconst LOGICAL_EDGE_MODS = ['start', 'end', LONGHAND_MOD];\nconst FLOW_MODS = [\n 'row',\n 'column',\n 'wrap',\n 'nowrap',\n 'dense',\n 'row-reverse',\n 'column-reverse',\n];\nconst OVERFLOW_MODS = [\n 'visible',\n 'hidden',\n 'scroll',\n 'clip',\n 'auto',\n 'overlay',\n];\nconst POSITION_MODS = ['static', 'relative', 'absolute', 'fixed', 'sticky'];\n\nconst COLOR_ONLY: PropertyExpectation = {\n acceptsColor: true,\n acceptsMods: false,\n};\n\nconst VALUE_ONLY: PropertyExpectation = {\n acceptsColor: false,\n acceptsMods: false,\n};\n\nconst PASSTHROUGH: PropertyExpectation = {\n acceptsColor: true,\n acceptsMods: true,\n};\n\n/**\n * Every `display` keyword, one word at a time: values are checked word by\n * word, so the two-value syntax (`block flex`, `inline flow-root`,\n * `list-item block`) is covered by its outside, inside and `list-item` words.\n * `grid-lanes` is CSS Grid Level 3 masonry.\n */\nconst DISPLAY_MODS = [\n // <display-outside>\n 'block',\n 'inline',\n 'run-in',\n // <display-inside>\n 'flow',\n 'flow-root',\n 'table',\n 'flex',\n 'grid',\n 'grid-lanes',\n 'ruby',\n 'math',\n // <display-listitem>\n 'list-item',\n // <display-internal>\n 'table-row-group',\n 'table-header-group',\n 'table-footer-group',\n 'table-row',\n 'table-cell',\n 'table-column-group',\n 'table-column',\n 'table-caption',\n 'ruby-base',\n 'ruby-text',\n 'ruby-base-container',\n 'ruby-text-container',\n // <display-box>\n 'contents',\n 'none',\n // <display-legacy>\n 'inline-block',\n 'inline-table',\n 'inline-flex',\n 'inline-grid',\n 'inline-grid-lanes',\n // The old flexbox boxes, kept because `-webkit-line-clamp` only truncates\n // inside one.\n '-webkit-box',\n '-webkit-inline-box',\n];\n\nexport const PROPERTY_EXPECTATIONS: Record<string, PropertyExpectation> = {\n fill: { acceptsColor: true, acceptsMods: ['none', 'transparent'] },\n // Tasty's `fill` is the background; `svgFill` is the native SVG `fill`, so it\n // also takes the SVG paint keywords.\n svgFill: {\n acceptsColor: true,\n acceptsMods: ['none', 'transparent', 'context-fill', 'context-stroke'],\n paint: true,\n },\n color: { acceptsColor: true, acceptsMods: ['none', 'transparent'] },\n caretColor: COLOR_ONLY,\n accentColor: COLOR_ONLY,\n shadow: { acceptsColor: true, acceptsMods: ['inset'] },\n\n border: {\n acceptsColor: true,\n acceptsMods: [...BOX_DIRECTIONAL_MODS, ...BORDER_STYLE_MODS],\n },\n outline: {\n acceptsColor: true,\n acceptsMods: BORDER_STYLE_MODS,\n },\n\n radius: {\n acceptsColor: false,\n acceptsMods: [\n ...RADIUS_DIRECTIONAL_MODS,\n 'round',\n 'ellipse',\n 'leaf',\n 'backleaf',\n ],\n },\n\n padding: { acceptsColor: false, acceptsMods: BOX_DIRECTIONAL_MODS },\n margin: { acceptsColor: false, acceptsMods: BOX_DIRECTIONAL_MODS },\n fade: { acceptsColor: true, acceptsMods: DIRECTIONAL_MODS },\n inset: { acceptsColor: false, acceptsMods: INSET_MODS },\n scrollMargin: { acceptsColor: false, acceptsMods: BOX_DIRECTIONAL_MODS },\n scrollPadding: { acceptsColor: false, acceptsMods: BOX_DIRECTIONAL_MODS },\n\n // Native logical CSS shorthands. Tasty v3.8 stopped reading these in its\n // physical handlers, so they are ordinary CSS declarations now: one or two\n // lengths through the normal value parser, no colours and no modifiers. The\n // enhanced `blockPadding` / `inlinePadding` below are what take modifiers.\n paddingInline: VALUE_ONLY,\n paddingBlock: VALUE_ONLY,\n\n width: { acceptsColor: false, acceptsMods: DIMENSION_MODS },\n height: { acceptsColor: false, acceptsMods: DIMENSION_MODS },\n\n // Enhanced logical styles. Each axis/category pair mirrors its physical\n // counterpart's vocabulary with `start`/`end` in place of the four sides.\n blockPadding: { acceptsColor: false, acceptsMods: LOGICAL_EDGE_MODS },\n inlinePadding: { acceptsColor: false, acceptsMods: LOGICAL_EDGE_MODS },\n blockMargin: { acceptsColor: false, acceptsMods: LOGICAL_EDGE_MODS },\n inlineMargin: { acceptsColor: false, acceptsMods: LOGICAL_EDGE_MODS },\n blockInset: { acceptsColor: false, acceptsMods: LOGICAL_EDGE_MODS },\n inlineInset: { acceptsColor: false, acceptsMods: LOGICAL_EDGE_MODS },\n blockScrollMargin: { acceptsColor: false, acceptsMods: LOGICAL_EDGE_MODS },\n inlineScrollMargin: { acceptsColor: false, acceptsMods: LOGICAL_EDGE_MODS },\n blockScrollPadding: { acceptsColor: false, acceptsMods: LOGICAL_EDGE_MODS },\n inlineScrollPadding: { acceptsColor: false, acceptsMods: LOGICAL_EDGE_MODS },\n blockBorder: {\n acceptsColor: true,\n acceptsMods: [...LOGICAL_EDGE_MODS, ...BORDER_STYLE_MODS],\n },\n inlineBorder: {\n acceptsColor: true,\n acceptsMods: [...LOGICAL_EDGE_MODS, ...BORDER_STYLE_MODS],\n },\n blockSize: { acceptsColor: false, acceptsMods: DIMENSION_MODS },\n inlineSize: { acceptsColor: false, acceptsMods: DIMENSION_MODS },\n\n gap: VALUE_ONLY,\n columnGap: VALUE_ONLY,\n rowGap: VALUE_ONLY,\n flexBasis: VALUE_ONLY,\n flexGrow: VALUE_ONLY,\n flexShrink: VALUE_ONLY,\n flex: VALUE_ONLY,\n order: VALUE_ONLY,\n zIndex: VALUE_ONLY,\n opacity: VALUE_ONLY,\n // `<ratio>` allows optional whitespace around the slash: `16/10`, `16 / 10`.\n aspectRatio: { ...VALUE_ONLY, acceptsRatio: true },\n lineClamp: VALUE_ONLY,\n tabSize: VALUE_ONLY,\n\n flow: { acceptsColor: false, acceptsMods: FLOW_MODS },\n display: { acceptsColor: false, acceptsMods: DISPLAY_MODS },\n overflow: { acceptsColor: false, acceptsMods: OVERFLOW_MODS },\n position: { acceptsColor: false, acceptsMods: POSITION_MODS },\n};\n\n/**\n * Get expectations for a property. Properties not in the map\n * are treated as passthrough (accept everything).\n */\nexport function getExpectation(property: string): PropertyExpectation {\n return PROPERTY_EXPECTATIONS[property] ?? PASSTHROUGH;\n}\n"],"mappings":";AA6BA,MAAM,mBAAmB;CAAC;CAAO;CAAS;CAAU;CAAO;AAI3D,MAAM,eAAe;AAGrB,MAAM,aAAa;CAAC,GAAG;CAAkB;CAAQ;CAAa;AAC9D,MAAM,uBAAuB,CAAC,GAAG,kBAAkB,aAAa;AAChE,MAAM,0BAA0B;CAC9B,GAAG;CACH;CACA;CACA;CACA;CACA;CACD;AACD,MAAM,oBAAoB;CACxB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AACD,MAAM,iBAAiB;CAAC;CAAO;CAAO;CAAQ;AAI9C,MAAM,oBAAoB;CAAC;CAAS;CAAO;CAAa;AACxD,MAAM,YAAY;CAChB;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AACD,MAAM,gBAAgB;CACpB;CACA;CACA;CACA;CACA;CACA;CACD;AACD,MAAM,gBAAgB;CAAC;CAAU;CAAY;CAAY;CAAS;CAAS;AAE3E,MAAM,aAAkC;CACtC,cAAc;CACd,aAAa;CACd;AAED,MAAM,aAAkC;CACtC,cAAc;CACd,aAAa;CACd;AAED,MAAM,cAAmC;CACvC,cAAc;CACd,aAAa;CACd;;;;;;;AAQD,MAAM,eAAe;CAEnB;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CAEA;CACA;CACA;CACA;CACA;CAGA;CACA;CACD;AAED,MAAa,wBAA6D;CACxE,MAAM;EAAE,cAAc;EAAM,aAAa,CAAC,QAAQ,cAAc;EAAE;CAGlE,SAAS;EACP,cAAc;EACd,aAAa;GAAC;GAAQ;GAAe;GAAgB;GAAiB;EACtE,OAAO;EACR;CACD,OAAO;EAAE,cAAc;EAAM,aAAa,CAAC,QAAQ,cAAc;EAAE;CACnE,YAAY;CACZ,aAAa;CACb,QAAQ;EAAE,cAAc;EAAM,aAAa,CAAC,QAAQ;EAAE;CAEtD,QAAQ;EACN,cAAc;EACd,aAAa,CAAC,GAAG,sBAAsB,GAAG,kBAAkB;EAC7D;CACD,SAAS;EACP,cAAc;EACd,aAAa;EACd;CAED,QAAQ;EACN,cAAc;EACd,aAAa;GACX,GAAG;GACH;GACA;GACA;GACA;GACD;EACF;CAED,SAAS;EAAE,cAAc;EAAO,aAAa;EAAsB;CACnE,QAAQ;EAAE,cAAc;EAAO,aAAa;EAAsB;CAClE,MAAM;EAAE,cAAc;EAAM,aAAa;EAAkB;CAC3D,OAAO;EAAE,cAAc;EAAO,aAAa;EAAY;CACvD,cAAc;EAAE,cAAc;EAAO,aAAa;EAAsB;CACxE,eAAe;EAAE,cAAc;EAAO,aAAa;EAAsB;CAMzE,eAAe;CACf,cAAc;CAEd,OAAO;EAAE,cAAc;EAAO,aAAa;EAAgB;CAC3D,QAAQ;EAAE,cAAc;EAAO,aAAa;EAAgB;CAI5D,cAAc;EAAE,cAAc;EAAO,aAAa;EAAmB;CACrE,eAAe;EAAE,cAAc;EAAO,aAAa;EAAmB;CACtE,aAAa;EAAE,cAAc;EAAO,aAAa;EAAmB;CACpE,cAAc;EAAE,cAAc;EAAO,aAAa;EAAmB;CACrE,YAAY;EAAE,cAAc;EAAO,aAAa;EAAmB;CACnE,aAAa;EAAE,cAAc;EAAO,aAAa;EAAmB;CACpE,mBAAmB;EAAE,cAAc;EAAO,aAAa;EAAmB;CAC1E,oBAAoB;EAAE,cAAc;EAAO,aAAa;EAAmB;CAC3E,oBAAoB;EAAE,cAAc;EAAO,aAAa;EAAmB;CAC3E,qBAAqB;EAAE,cAAc;EAAO,aAAa;EAAmB;CAC5E,aAAa;EACX,cAAc;EACd,aAAa,CAAC,GAAG,mBAAmB,GAAG,kBAAkB;EAC1D;CACD,cAAc;EACZ,cAAc;EACd,aAAa,CAAC,GAAG,mBAAmB,GAAG,kBAAkB;EAC1D;CACD,WAAW;EAAE,cAAc;EAAO,aAAa;EAAgB;CAC/D,YAAY;EAAE,cAAc;EAAO,aAAa;EAAgB;CAEhE,KAAK;CACL,WAAW;CACX,QAAQ;CACR,WAAW;CACX,UAAU;CACV,YAAY;CACZ,MAAM;CACN,OAAO;CACP,QAAQ;CACR,SAAS;CAET,aAAa;EAAE,GAAG;EAAY,cAAc;EAAM;CAClD,WAAW;CACX,SAAS;CAET,MAAM;EAAE,cAAc;EAAO,aAAa;EAAW;CACrD,SAAS;EAAE,cAAc;EAAO,aAAa;EAAc;CAC3D,UAAU;EAAE,cAAc;EAAO,aAAa;EAAe;CAC7D,UAAU;EAAE,cAAc;EAAO,aAAa;EAAe;CAC9D;;;;;AAMD,SAAgB,eAAe,UAAuC;AACpE,QAAO,sBAAsB,aAAa"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { createRule } from "../create-rule.js";
|
|
2
|
+
import { getInstalledTastyVersion } from "../config.js";
|
|
3
|
+
import { TastyContext } from "../context.js";
|
|
4
|
+
import { stringStyleValueListeners } from "../style-values.js";
|
|
5
|
+
import { splitTopLevelCommas } from "../parsers/utils.js";
|
|
6
|
+
import { COLOR_FUNCS, parseValue } from "../parsers/value-parser.js";
|
|
7
|
+
import { replaceStringValue } from "../fix-utils.js";
|
|
8
|
+
import semver from "semver";
|
|
9
|
+
|
|
10
|
+
//#region src/rules/prefer-fill-mix.ts
|
|
11
|
+
/** The first `@tenphi/tasty` release that mixes comma-separated fill colors. */
|
|
12
|
+
const FILL_MIX_TASTY_VERSION = "3.15.0";
|
|
13
|
+
const FILL_PROPERTIES = new Set(["fill", "backgroundColor"]);
|
|
14
|
+
/** Color functions Tasty recognizes beyond the ones the parser classifies. */
|
|
15
|
+
const EXTRA_COLOR_FUNCS = new Set([
|
|
16
|
+
"light-dark",
|
|
17
|
+
"contrast-color",
|
|
18
|
+
"okhsl",
|
|
19
|
+
"okhst"
|
|
20
|
+
]);
|
|
21
|
+
/** Whether Tasty's parser takes this token for a color. */
|
|
22
|
+
function isColor(token) {
|
|
23
|
+
switch (token.type) {
|
|
24
|
+
case "color-token":
|
|
25
|
+
case "color-keyword": return true;
|
|
26
|
+
case "custom-prop": return token.name.endsWith("-color");
|
|
27
|
+
case "css-function": {
|
|
28
|
+
const name = token.name.toLowerCase();
|
|
29
|
+
return COLOR_FUNCS.has(name) || EXTRA_COLOR_FUNCS.has(name);
|
|
30
|
+
}
|
|
31
|
+
case "group-expr": return token.inner.includes("#");
|
|
32
|
+
default: return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** `#clear` and its fades compile to `transparent`. */
|
|
36
|
+
function isClear(token) {
|
|
37
|
+
return token.type === "color-token" && (token.raw === "#clear" || token.raw.startsWith("#clear."));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The weight a `#name.N` fade stands for, written the way Tasty compiles the
|
|
41
|
+
* opacity: `.04` is `4%`, `.5` is `50%`, `.$hover` is `($hover * 100%)`.
|
|
42
|
+
*/
|
|
43
|
+
function opacityWeight(opacity) {
|
|
44
|
+
if (opacity.startsWith("$")) return `(${opacity} * 100%)`;
|
|
45
|
+
return `${Number((Number(`0.${opacity}`) * 100).toFixed(6))}%`;
|
|
46
|
+
}
|
|
47
|
+
/** A faded color token split into the color and the weight its opacity is. */
|
|
48
|
+
function splitFade(token) {
|
|
49
|
+
if (token.type !== "color-token" || token.opacity === void 0) return null;
|
|
50
|
+
return {
|
|
51
|
+
color: token.raw.slice(0, -(token.opacity.length + 1)),
|
|
52
|
+
weight: opacityWeight(token.opacity)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The comma mix that paints what `'<base> <overlay>'` painted, or `null` when
|
|
57
|
+
* the overlay's alpha is not written on it. Exact when the overlay token holds
|
|
58
|
+
* an opaque color: drawing it at alpha `a` over the base is the base mixed with
|
|
59
|
+
* it at weight `a` in sRGB, which is what Tasty compiles the mix to.
|
|
60
|
+
*/
|
|
61
|
+
function rewriteOverlay(base, overlay) {
|
|
62
|
+
if (isClear(overlay)) return base;
|
|
63
|
+
const fade = splitFade(overlay);
|
|
64
|
+
if (!fade) return null;
|
|
65
|
+
if (fade.weight === "0%") return base;
|
|
66
|
+
return `${fade.color} ${fade.weight}, ${base}`;
|
|
67
|
+
}
|
|
68
|
+
/** A token as written. */
|
|
69
|
+
function tokenText(token) {
|
|
70
|
+
return "raw" in token ? token.raw : token.value;
|
|
71
|
+
}
|
|
72
|
+
var prefer_fill_mix_default = createRule({
|
|
73
|
+
name: "prefer-fill-mix",
|
|
74
|
+
meta: {
|
|
75
|
+
type: "suggestion",
|
|
76
|
+
fixable: "code",
|
|
77
|
+
hasSuggestions: true,
|
|
78
|
+
docs: { description: "Replace deprecated space-separated two-color fills with a comma mix ('#dark 4%, #surface'), and catch mix parts weighted by their opacity" },
|
|
79
|
+
messages: {
|
|
80
|
+
twoColorFill: "Two space-separated colors in '{{property}}' are deprecated: the second is painted as a background-image layer that contrast checkers cannot read. Use '{{suggestion}}'.",
|
|
81
|
+
twoColorFillManual: "Two space-separated colors in '{{property}}' are deprecated: the second is painted as a background-image layer that contrast checkers cannot read. Mix an opaque overlay color in by weight ('#overlay 4%, #base'), or use an opaque overlay alone.",
|
|
82
|
+
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}}'."
|
|
84
|
+
},
|
|
85
|
+
schema: []
|
|
86
|
+
},
|
|
87
|
+
defaultOptions: [],
|
|
88
|
+
create(context) {
|
|
89
|
+
const tastyVersion = getInstalledTastyVersion(context.filename);
|
|
90
|
+
if (!tastyVersion || !semver.satisfies(tastyVersion, `>=${FILL_MIX_TASTY_VERSION}`)) return {};
|
|
91
|
+
const ctx = new TastyContext(context);
|
|
92
|
+
function checkSpaceSeparated(tokens, colorCount, node, property) {
|
|
93
|
+
const suggestion = tokens.length === 2 && colorCount === 2 ? rewriteOverlay(tokenText(tokens[0]), tokens[1]) : null;
|
|
94
|
+
if (suggestion === null) {
|
|
95
|
+
context.report({
|
|
96
|
+
node,
|
|
97
|
+
messageId: "twoColorFillManual",
|
|
98
|
+
data: { property }
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
context.report({
|
|
103
|
+
node,
|
|
104
|
+
messageId: "twoColorFill",
|
|
105
|
+
data: {
|
|
106
|
+
property,
|
|
107
|
+
suggestion
|
|
108
|
+
},
|
|
109
|
+
fix: (fixer) => replaceStringValue(fixer, node, suggestion)
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function checkMix(value, groups, node) {
|
|
113
|
+
const parts = splitTopLevelCommas(value).map((part) => part.trim());
|
|
114
|
+
for (const [i, tokens] of groups.entries()) {
|
|
115
|
+
if (tokens.length !== 1) continue;
|
|
116
|
+
const [token] = tokens;
|
|
117
|
+
const other = parts[1 - i];
|
|
118
|
+
let replacement = null;
|
|
119
|
+
if (isClear(token)) replacement = other;
|
|
120
|
+
else {
|
|
121
|
+
const fade = splitFade(token);
|
|
122
|
+
if (fade) {
|
|
123
|
+
const weighted = [...parts];
|
|
124
|
+
weighted[i] = `${fade.color} ${fade.weight}`;
|
|
125
|
+
replacement = weighted.join(", ");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (replacement === null) continue;
|
|
129
|
+
const suggestion = replacement;
|
|
130
|
+
context.report({
|
|
131
|
+
node,
|
|
132
|
+
messageId: "translucentMixPart",
|
|
133
|
+
data: {
|
|
134
|
+
part: parts[i],
|
|
135
|
+
suggestion
|
|
136
|
+
},
|
|
137
|
+
suggest: [{
|
|
138
|
+
messageId: "useWeight",
|
|
139
|
+
data: { suggestion },
|
|
140
|
+
fix: (fixer) => replaceStringValue(fixer, node, suggestion)
|
|
141
|
+
}]
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function checkValue(value, node, property) {
|
|
147
|
+
const { groups } = parseValue(value);
|
|
148
|
+
if (groups.some((group) => group.parts.length !== 1)) return;
|
|
149
|
+
const tokenGroups = groups.map((group) => group.parts[0].tokens);
|
|
150
|
+
if (tokenGroups.length === 1) {
|
|
151
|
+
const colorCount = tokenGroups[0].filter(isColor).length;
|
|
152
|
+
if (colorCount >= 2) checkSpaceSeparated(tokenGroups[0], colorCount, node, property);
|
|
153
|
+
} else if (tokenGroups.length === 2) checkMix(value, tokenGroups, node);
|
|
154
|
+
}
|
|
155
|
+
return stringStyleValueListeners(ctx, (value, node, property) => {
|
|
156
|
+
if (FILL_PROPERTIES.has(property)) checkValue(value, node, property);
|
|
157
|
+
}, { requireTastyJSX: true });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
//#endregion
|
|
162
|
+
export { prefer_fill_mix_default as default };
|
|
163
|
+
//# sourceMappingURL=prefer-fill-mix.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefer-fill-mix.js","names":[],"sources":["../../src/rules/prefer-fill-mix.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport semver from 'semver';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { getInstalledTastyVersion } from '../config.js';\nimport { replaceStringValue } from '../fix-utils.js';\nimport { parseValue } from '../parsers/index.js';\nimport type { ValueToken } from '../parsers/index.js';\nimport { splitTopLevelCommas } from '../parsers/utils.js';\nimport { COLOR_FUNCS } from '../parsers/value-parser.js';\nimport { stringStyleValueListeners } from '../style-values.js';\n\ntype MessageIds =\n | 'twoColorFill'\n | 'twoColorFillManual'\n | 'translucentMixPart'\n | 'useWeight';\n\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":";;;;;;;;;;;AAmBA,MAAM,yBAAyB;AAE/B,MAAM,kBAAkB,IAAI,IAAI,CAAC,QAAQ,kBAAkB,CAAC;;AAG5D,MAAM,oBAAoB,IAAI,IAAI;CAChC;CACA;CACA;CACA;CACD,CAAC;;AAGF,SAAS,QAAQ,OAA4B;AAC3C,SAAQ,MAAM,MAAd;EACE,KAAK;EACL,KAAK,gBACH,QAAO;EACT,KAAK,cAEH,QAAO,MAAM,KAAK,SAAS,SAAS;EACtC,KAAK,gBAAgB;GACnB,MAAM,OAAO,MAAM,KAAK,aAAa;AACrC,UAAO,YAAY,IAAI,KAAK,IAAI,kBAAkB,IAAI,KAAK;;EAE7D,KAAK,aAEH,QAAO,MAAM,MAAM,SAAS,IAAI;EAClC,QACE,QAAO;;;;AAKb,SAAS,QAAQ,OAA4B;AAC3C,QACE,MAAM,SAAS,kBACd,MAAM,QAAQ,YAAY,MAAM,IAAI,WAAW,UAAU;;;;;;AAQ9D,SAAS,cAAc,SAAyB;AAC9C,KAAI,QAAQ,WAAW,IAAI,CAAE,QAAO,IAAI,QAAQ;AAChD,QAAO,GAAG,QAAQ,OAAO,KAAK,UAAU,GAAG,KAAK,QAAQ,EAAE,CAAC,CAAC;;;AAI9D,SAAS,UACP,OAC0C;AAC1C,KAAI,MAAM,SAAS,iBAAiB,MAAM,YAAY,OAAW,QAAO;AACxE,QAAO;EACL,OAAO,MAAM,IAAI,MAAM,GAAG,EAAE,MAAM,QAAQ,SAAS,GAAG;EACtD,QAAQ,cAAc,MAAM,QAAQ;EACrC;;;;;;;;AASH,SAAS,eAAe,MAAc,SAAoC;AACxE,KAAI,QAAQ,QAAQ,CAAE,QAAO;CAE7B,MAAM,OAAO,UAAU,QAAQ;AAC/B,KAAI,CAAC,KAAM,QAAO;AAClB,KAAI,KAAK,WAAW,KAAM,QAAO;AAEjC,QAAO,GAAG,KAAK,MAAM,GAAG,KAAK,OAAO,IAAI;;;AAI1C,SAAS,UAAU,OAA2B;AAC5C,QAAO,SAAS,QAAQ,MAAM,MAAM,MAAM;;AAG5C,8BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,gBAAgB;EAChB,MAAM,EACJ,aACE,6IACH;EACD,UAAU;GACR,cACE;GACF,oBACE;GACF,oBACE;GACF,WAAW;GACZ;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EAGd,MAAM,eAAe,yBAAyB,QAAQ,SAAS;AAC/D,MACE,CAAC,gBACD,CAAC,OAAO,UAAU,cAAc,KAAK,yBAAyB,CAE9D,QAAO,EAAE;EAGX,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,oBACP,QACA,YACA,MACA,UACM;GACN,MAAM,aACJ,OAAO,WAAW,KAAK,eAAe,IAClC,eAAe,UAAU,OAAO,GAAG,EAAE,OAAO,GAAG,GAC/C;AAEN,OAAI,eAAe,MAAM;AACvB,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM,EAAE,UAAU;KACnB,CAAC;AACF;;AAGF,WAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM;KAAE;KAAU;KAAY;IAC9B,MAAM,UAAU,mBAAmB,OAAO,MAAM,WAAW;IAC5D,CAAC;;EAGJ,SAAS,SACP,OACA,QACA,MACM;GACN,MAAM,QAAQ,oBAAoB,MAAM,CAAC,KAAK,SAAS,KAAK,MAAM,CAAC;AAEnE,QAAK,MAAM,CAAC,GAAG,WAAW,OAAO,SAAS,EAAE;AAC1C,QAAI,OAAO,WAAW,EAAG;IAEzB,MAAM,CAAC,SAAS;IAChB,MAAM,QAAQ,MAAM,IAAI;IACxB,IAAI,cAA6B;AAEjC,QAAI,QAAQ,MAAM,CAEhB,eAAc;SACT;KACL,MAAM,OAAO,UAAU,MAAM;AAC7B,SAAI,MAAM;MACR,MAAM,WAAW,CAAC,GAAG,MAAM;AAC3B,eAAS,KAAK,GAAG,KAAK,MAAM,GAAG,KAAK;AACpC,oBAAc,SAAS,KAAK,KAAK;;;AAIrC,QAAI,gBAAgB,KAAM;IAE1B,MAAM,aAAa;AACnB,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE,MAAM,MAAM;MAAI;MAAY;KACpC,SAAS,CACP;MACE,WAAW;MACX,MAAM,EAAE,YAAY;MACpB,MAAM,UAAU,mBAAmB,OAAO,MAAM,WAAW;MAC5D,CACF;KACF,CAAC;AACF;;;EAIJ,SAAS,WACP,OACA,MACA,UACM;GACN,MAAM,EAAE,WAAW,WAAW,MAAM;AAGpC,OAAI,OAAO,MAAM,UAAU,MAAM,MAAM,WAAW,EAAE,CAAE;GACtD,MAAM,cAAc,OAAO,KAAK,UAAU,MAAM,MAAM,GAAG,OAAO;AAEhE,OAAI,YAAY,WAAW,GAAG;IAC5B,MAAM,aAAa,YAAY,GAAG,OAAO,QAAQ,CAAC;AAClD,QAAI,cAAc,EAChB,qBAAoB,YAAY,IAAI,YAAY,MAAM,SAAS;cAExD,YAAY,WAAW,EAChC,UAAS,OAAO,aAAa,KAAK;;AAItC,SAAO,0BACL,MACC,OAAO,MAAM,aAAa;AACzB,OAAI,gBAAgB,IAAI,SAAS,CAAE,YAAW,OAAO,MAAM,SAAS;KAEtE,EAAE,iBAAiB,MAAM,CAC1B;;CAEJ,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRule } from "../create-rule.js";
|
|
2
|
+
import { isRawHexColor } from "../utils.js";
|
|
2
3
|
import { TastyContext } from "../context.js";
|
|
3
4
|
import { stringStyleValueListeners } from "../style-values.js";
|
|
4
5
|
import { parseValue } from "../parsers/value-parser.js";
|
|
@@ -44,6 +45,8 @@ var valid_value_default = createRule({
|
|
|
44
45
|
unexpectedColor: "Property '{{property}}' does not accept color tokens, but found '{{color}}'.",
|
|
45
46
|
invalidMod: "Modifier '{{mod}}' is not valid for '{{property}}'. Accepted: {{accepted}}.",
|
|
46
47
|
unknownToken: "Unknown token '{{token}}' in '{{property}}' value. Check for a typo.",
|
|
48
|
+
multiplePaints: "'{{property}}' takes a single paint, but '{{value}}' has more than one value. Tasty keeps one color and drops the rest.",
|
|
49
|
+
unexpandedPaintToken: "'{{token}}' is not expanded after url(): Tasty passes a url() paint through as plain CSS. Use a CSS color or a $custom-property for the fallback.",
|
|
47
50
|
parseError: "{{message}}"
|
|
48
51
|
},
|
|
49
52
|
schema: []
|
|
@@ -80,6 +83,7 @@ var valid_value_default = createRule({
|
|
|
80
83
|
}
|
|
81
84
|
if (!property) return;
|
|
82
85
|
const expectation = getExpectation(property);
|
|
86
|
+
if (expectation.paint) checkPaint(result, value, property, node);
|
|
83
87
|
for (const group of result.groups) for (const part of group.parts) for (const token of part.tokens) {
|
|
84
88
|
if (token.type === "important") {
|
|
85
89
|
context.report({
|
|
@@ -136,6 +140,26 @@ var valid_value_default = createRule({
|
|
|
136
140
|
}
|
|
137
141
|
}
|
|
138
142
|
}
|
|
143
|
+
function checkPaint(result, value, property, node) {
|
|
144
|
+
const tokens = result.groups.flatMap((group) => group.parts.flatMap((part) => part.tokens)).filter((token) => token.type !== "important");
|
|
145
|
+
const [first] = tokens;
|
|
146
|
+
const url = first?.type === "css-function" && first.name === "url";
|
|
147
|
+
if (result.groups.length > 1 || tokens.length > (url ? 2 : 1)) context.report({
|
|
148
|
+
node,
|
|
149
|
+
messageId: "multiplePaints",
|
|
150
|
+
data: {
|
|
151
|
+
property,
|
|
152
|
+
value
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
if (url) {
|
|
156
|
+
for (const token of tokens) if (token.type === "color-token" && (token.opacity !== void 0 || !isRawHexColor(token.raw)) || token.type === "color-ref") context.report({
|
|
157
|
+
node,
|
|
158
|
+
messageId: "unexpandedPaintToken",
|
|
159
|
+
data: { token: token.raw }
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
139
163
|
return stringStyleValueListeners(ctx, (value, node, property) => {
|
|
140
164
|
if (!/^[#$]/.test(property)) checkValue(value, property, node);
|
|
141
165
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"valid-value.js","names":[],"sources":["../../src/rules/valid-value.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { stringStyleValueListeners } from '../style-values.js';\nimport { parseValue } from '../parsers/value-parser.js';\nimport type { ValueParserOptions } from '../parsers/value-parser.js';\nimport { getExpectation } from '../property-expectations.js';\n\nconst SKIP_PROPERTIES = new Set([\n 'recipe',\n 'preset',\n 'transition',\n '@keyframes',\n '@property',\n '@font-face',\n '@counter-style',\n '@function',\n 'content',\n 'animation',\n 'animationName',\n 'gridArea',\n 'gridAreas',\n 'gridColumn',\n 'gridColumnStart',\n 'gridColumnEnd',\n 'gridRow',\n 'gridRowStart',\n 'gridRowEnd',\n 'gridTemplate',\n 'gridTemplateAreas',\n 'gridTemplateColumns',\n 'gridTemplateRows',\n 'listStyle',\n 'willChange',\n]);\n\ntype MessageIds =\n | 'unbalancedParens'\n | 'importantNotAllowed'\n | 'unexpectedMod'\n | 'unexpectedColor'\n | 'invalidMod'\n | 'unknownToken'\n | 'parseError';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-value',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Parse style values through the value parser and validate against per-property expectations',\n },\n messages: {\n unbalancedParens: 'Unbalanced parentheses in value.',\n importantNotAllowed:\n 'Do not use !important in tasty styles. Use state specificity instead.',\n unexpectedMod:\n \"Unrecognized token '{{mod}}' in '{{property}}' value. This may be a typo.\",\n unexpectedColor:\n \"Property '{{property}}' does not accept color tokens, but found '{{color}}'.\",\n invalidMod:\n \"Modifier '{{mod}}' is not valid for '{{property}}'. Accepted: {{accepted}}.\",\n unknownToken:\n \"Unknown token '{{token}}' in '{{property}}' value. Check for a typo.\",\n parseError: '{{message}}',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function getParserOpts(): ValueParserOptions {\n const opts: ValueParserOptions = {};\n if (ctx.config.units === false) {\n opts.skipUnitValidation = true;\n } else if (Array.isArray(ctx.config.units)) {\n opts.knownUnits = new Set(ctx.config.units);\n }\n if (ctx.config.functions === false) {\n opts.skipFuncValidation = true;\n } else if (Array.isArray(ctx.config.functions)) {\n opts.knownFuncs = new Set(ctx.config.functions);\n }\n return opts;\n }\n\n function checkValue(\n value: string,\n property: string | null,\n node: TSESTree.Node,\n ): void {\n if (property && SKIP_PROPERTIES.has(property)) return;\n\n const result = parseValue(value, getParserOpts());\n\n const erroredRaws = new Set<string>();\n\n // Report parser-level errors (bracket balance, unknown units/functions)\n for (const error of result.errors) {\n if (error.message.includes('parenthes')) {\n context.report({ node, messageId: 'unbalancedParens' });\n return;\n }\n context.report({\n node,\n messageId: 'parseError',\n data: { message: error.message },\n });\n if (error.raw) {\n erroredRaws.add(error.raw);\n }\n }\n\n if (!property) return;\n\n const expectation = getExpectation(property);\n\n for (const group of result.groups) {\n for (const part of group.parts) {\n for (const token of part.tokens) {\n // Check !important\n if (token.type === 'important') {\n context.report({ node, messageId: 'importantNotAllowed' });\n continue;\n }\n\n // Check color tokens and keywords in non-color properties\n if (\n !expectation.acceptsColor &&\n (token.type === 'color-token' ||\n token.type === 'color-ref' ||\n token.type === 'color-keyword')\n ) {\n const colorName =\n token.type === 'color-token'\n ? `#${token.name}`\n : token.type === 'color-ref'\n ? `##${token.name}`\n : token.raw;\n context.report({\n node,\n messageId: 'unexpectedColor',\n data: { property, color: colorName },\n });\n continue;\n }\n\n // Check color functions in non-color properties\n if (\n !expectation.acceptsColor &&\n token.type === 'css-function' &&\n isColorFunction(token.name)\n ) {\n context.report({\n node,\n messageId: 'unexpectedColor',\n data: { property, color: `${token.name}()` },\n });\n continue;\n }\n\n // Skip tokens already reported via parser errors\n if (\n (token.type === 'unknown' || token.type === 'css-function') &&\n 'raw' in token &&\n token.raw &&\n erroredRaws.has(token.raw)\n ) {\n continue;\n }\n\n // Check unknown tokens against property expectations. A ratio\n // outside `aspectRatio` is as unrecognized as any other word.\n if (\n token.type === 'unknown' ||\n (token.type === 'ratio' && !expectation.acceptsRatio)\n ) {\n const raw = token.raw;\n\n if (expectation.acceptsMods === true) {\n // Passthrough: accept any unknown token\n } else if (expectation.acceptsMods === false) {\n context.report({\n node,\n messageId: 'unexpectedMod',\n data: { property, mod: raw },\n });\n } else if (Array.isArray(expectation.acceptsMods)) {\n if (!expectation.acceptsMods.includes(raw)) {\n context.report({\n node,\n messageId: 'invalidMod',\n data: {\n property,\n mod: raw,\n accepted: expectation.acceptsMods.join(', '),\n },\n });\n }\n }\n }\n }\n }\n }\n }\n\n return stringStyleValueListeners(ctx, (value, node, property) => {\n if (!/^[#$]/.test(property)) checkValue(value, property, node);\n });\n },\n});\n\nconst COLOR_FUNC_NAMES = new Set([\n 'rgb',\n 'rgba',\n 'hsl',\n 'hsla',\n 'hwb',\n 'lab',\n 'lch',\n 'oklab',\n 'oklch',\n 'color',\n 'color-mix',\n 'color-contrast',\n]);\n\nfunction isColorFunction(name: string): boolean {\n return COLOR_FUNC_NAMES.has(name);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"valid-value.js","names":[],"sources":["../../src/rules/valid-value.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext } from '../context.js';\nimport { stringStyleValueListeners } from '../style-values.js';\nimport { parseValue } from '../parsers/value-parser.js';\nimport type {\n ValueParseResult,\n ValueParserOptions,\n} from '../parsers/value-parser.js';\nimport { getExpectation } from '../property-expectations.js';\nimport { isRawHexColor } from '../utils.js';\n\nconst SKIP_PROPERTIES = new Set([\n 'recipe',\n 'preset',\n 'transition',\n '@keyframes',\n '@property',\n '@font-face',\n '@counter-style',\n '@function',\n 'content',\n 'animation',\n 'animationName',\n 'gridArea',\n 'gridAreas',\n 'gridColumn',\n 'gridColumnStart',\n 'gridColumnEnd',\n 'gridRow',\n 'gridRowStart',\n 'gridRowEnd',\n 'gridTemplate',\n 'gridTemplateAreas',\n 'gridTemplateColumns',\n 'gridTemplateRows',\n 'listStyle',\n 'willChange',\n]);\n\ntype MessageIds =\n | 'unbalancedParens'\n | 'importantNotAllowed'\n | 'unexpectedMod'\n | 'unexpectedColor'\n | 'invalidMod'\n | 'unknownToken'\n | 'multiplePaints'\n | 'unexpandedPaintToken'\n | 'parseError';\n\nexport default createRule<[], MessageIds>({\n name: 'valid-value',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Parse style values through the value parser and validate against per-property expectations',\n },\n messages: {\n unbalancedParens: 'Unbalanced parentheses in value.',\n importantNotAllowed:\n 'Do not use !important in tasty styles. Use state specificity instead.',\n unexpectedMod:\n \"Unrecognized token '{{mod}}' in '{{property}}' value. This may be a typo.\",\n unexpectedColor:\n \"Property '{{property}}' does not accept color tokens, but found '{{color}}'.\",\n invalidMod:\n \"Modifier '{{mod}}' is not valid for '{{property}}'. Accepted: {{accepted}}.\",\n unknownToken:\n \"Unknown token '{{token}}' in '{{property}}' value. Check for a typo.\",\n multiplePaints:\n \"'{{property}}' takes a single paint, but '{{value}}' has more than one value. Tasty keeps one color and drops the rest.\",\n unexpandedPaintToken:\n \"'{{token}}' is not expanded after url(): Tasty passes a url() paint through as plain CSS. Use a CSS color or a $custom-property for the fallback.\",\n parseError: '{{message}}',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function getParserOpts(): ValueParserOptions {\n const opts: ValueParserOptions = {};\n if (ctx.config.units === false) {\n opts.skipUnitValidation = true;\n } else if (Array.isArray(ctx.config.units)) {\n opts.knownUnits = new Set(ctx.config.units);\n }\n if (ctx.config.functions === false) {\n opts.skipFuncValidation = true;\n } else if (Array.isArray(ctx.config.functions)) {\n opts.knownFuncs = new Set(ctx.config.functions);\n }\n return opts;\n }\n\n function checkValue(\n value: string,\n property: string | null,\n node: TSESTree.Node,\n ): void {\n if (property && SKIP_PROPERTIES.has(property)) return;\n\n const result = parseValue(value, getParserOpts());\n\n const erroredRaws = new Set<string>();\n\n // Report parser-level errors (bracket balance, unknown units/functions)\n for (const error of result.errors) {\n if (error.message.includes('parenthes')) {\n context.report({ node, messageId: 'unbalancedParens' });\n return;\n }\n context.report({\n node,\n messageId: 'parseError',\n data: { message: error.message },\n });\n if (error.raw) {\n erroredRaws.add(error.raw);\n }\n }\n\n if (!property) return;\n\n const expectation = getExpectation(property);\n if (expectation.paint) checkPaint(result, value, property, node);\n\n for (const group of result.groups) {\n for (const part of group.parts) {\n for (const token of part.tokens) {\n // Check !important\n if (token.type === 'important') {\n context.report({ node, messageId: 'importantNotAllowed' });\n continue;\n }\n\n // Check color tokens and keywords in non-color properties\n if (\n !expectation.acceptsColor &&\n (token.type === 'color-token' ||\n token.type === 'color-ref' ||\n token.type === 'color-keyword')\n ) {\n const colorName =\n token.type === 'color-token'\n ? `#${token.name}`\n : token.type === 'color-ref'\n ? `##${token.name}`\n : token.raw;\n context.report({\n node,\n messageId: 'unexpectedColor',\n data: { property, color: colorName },\n });\n continue;\n }\n\n // Check color functions in non-color properties\n if (\n !expectation.acceptsColor &&\n token.type === 'css-function' &&\n isColorFunction(token.name)\n ) {\n context.report({\n node,\n messageId: 'unexpectedColor',\n data: { property, color: `${token.name}()` },\n });\n continue;\n }\n\n // Skip tokens already reported via parser errors\n if (\n (token.type === 'unknown' || token.type === 'css-function') &&\n 'raw' in token &&\n token.raw &&\n erroredRaws.has(token.raw)\n ) {\n continue;\n }\n\n // Check unknown tokens against property expectations. A ratio\n // outside `aspectRatio` is as unrecognized as any other word.\n if (\n token.type === 'unknown' ||\n (token.type === 'ratio' && !expectation.acceptsRatio)\n ) {\n const raw = token.raw;\n\n if (expectation.acceptsMods === true) {\n // Passthrough: accept any unknown token\n } else if (expectation.acceptsMods === false) {\n context.report({\n node,\n messageId: 'unexpectedMod',\n data: { property, mod: raw },\n });\n } else if (Array.isArray(expectation.acceptsMods)) {\n if (!expectation.acceptsMods.includes(raw)) {\n context.report({\n node,\n messageId: 'invalidMod',\n data: {\n property,\n mod: raw,\n accepted: expectation.acceptsMods.join(', '),\n },\n });\n }\n }\n }\n }\n }\n }\n }\n\n function checkPaint(\n result: ValueParseResult,\n value: string,\n property: string,\n node: TSESTree.Node,\n ): void {\n const tokens = result.groups\n .flatMap((group) => group.parts.flatMap((part) => part.tokens))\n .filter((token) => token.type !== 'important');\n const [first] = tokens;\n const url = first?.type === 'css-function' && first.name === 'url';\n\n // CSS allows one fallback after a url() paint.\n if (result.groups.length > 1 || tokens.length > (url ? 2 : 1)) {\n context.report({\n node,\n messageId: 'multiplePaints',\n data: { property, value },\n });\n }\n\n if (url) {\n for (const token of tokens) {\n if (\n (token.type === 'color-token' &&\n (token.opacity !== undefined || !isRawHexColor(token.raw))) ||\n token.type === 'color-ref'\n ) {\n context.report({\n node,\n messageId: 'unexpandedPaintToken',\n data: { token: token.raw },\n });\n }\n }\n }\n }\n\n return stringStyleValueListeners(ctx, (value, node, property) => {\n if (!/^[#$]/.test(property)) checkValue(value, property, node);\n });\n },\n});\n\nconst COLOR_FUNC_NAMES = new Set([\n 'rgb',\n 'rgba',\n 'hsl',\n 'hsla',\n 'hwb',\n 'lab',\n 'lch',\n 'oklab',\n 'oklch',\n 'color',\n 'color-mix',\n 'color-contrast',\n]);\n\nfunction isColorFunction(name: string): boolean {\n return COLOR_FUNC_NAMES.has(name);\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.14.0",
|
|
4
4
|
"description": "ESLint plugin for validating tasty() and tastyStatic() style objects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@changesets/changelog-github": "^0.5.2",
|
|
54
54
|
"@changesets/cli": "^2.29.8",
|
|
55
55
|
"@eslint/js": "^10.0.1",
|
|
56
|
-
"@tenphi/tasty": "^3.
|
|
56
|
+
"@tenphi/tasty": "^3.14.0",
|
|
57
57
|
"@types/node": "^22.0.0",
|
|
58
58
|
"@types/semver": "^7.8.0",
|
|
59
59
|
"@typescript-eslint/parser": "^8.56.1",
|