@tenphi/eslint-plugin-tasty 1.8.3 → 1.8.4

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.
@@ -3,6 +3,31 @@ function isWhitespace(ch) {
3
3
  return ch === " " || ch === "\n" || ch === " " || ch === "\r" || ch === "\f";
4
4
  }
5
5
  /**
6
+ * Split on commas outside parentheses and quotes, so a comma inside a function
7
+ * (`steps(2, start)`, `cubic-bezier(0.5, 0, 0.3, 1)`) stays in its part. Like
8
+ * `split(',')`, the parts joined with `,` give back the input.
9
+ */
10
+ function splitTopLevelCommas(src) {
11
+ const parts = [];
12
+ let depth = 0;
13
+ let inQuote = 0;
14
+ let start = 0;
15
+ for (let i = 0; i < src.length; i++) {
16
+ const ch = src[i];
17
+ if (inQuote) {
18
+ if (ch === inQuote) inQuote = 0;
19
+ } else if (ch === "\"" || ch === "'") inQuote = ch;
20
+ else if (ch === "(") depth++;
21
+ else if (ch === ")") depth = Math.max(0, depth - 1);
22
+ else if (ch === "," && depth === 0) {
23
+ parts.push(src.slice(start, i));
24
+ start = i + 1;
25
+ }
26
+ }
27
+ parts.push(src.slice(start));
28
+ return parts;
29
+ }
30
+ /**
6
31
  * Check that all brackets/parentheses are balanced in a source string.
7
32
  * Returns null if balanced, or an error with position if not.
8
33
  */
@@ -124,5 +149,5 @@ function scanTokens(src) {
124
149
  }
125
150
 
126
151
  //#endregion
127
- export { checkBracketBalance, scanTokens };
152
+ export { checkBracketBalance, scanTokens, splitTopLevelCommas };
128
153
  //# sourceMappingURL=utils.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","names":[],"sources":["../../src/parsers/utils.ts"],"sourcesContent":["export function isWhitespace(ch: string | undefined): boolean {\n return ch === ' ' || ch === '\\n' || ch === '\\t' || ch === '\\r' || ch === '\\f';\n}\n\nexport function isDigit(ch: string): boolean {\n return ch >= '0' && ch <= '9';\n}\n\nexport function isAlpha(ch: string): boolean {\n return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z');\n}\n\nexport interface BracketError {\n position: number;\n message: string;\n}\n\n/**\n * Check that all brackets/parentheses are balanced in a source string.\n * Returns null if balanced, or an error with position if not.\n */\nexport function checkBracketBalance(src: string): BracketError | null {\n let depth = 0;\n let inQuote: string | 0 = 0;\n\n for (let i = 0; i < src.length; i++) {\n const ch = src[i];\n\n if (inQuote) {\n if (ch === inQuote && src[i - 1] !== '\\\\') inQuote = 0;\n continue;\n }\n if (ch === '\"' || ch === \"'\") {\n inQuote = ch;\n continue;\n }\n\n if (ch === '(') depth++;\n if (ch === ')') {\n depth--;\n if (depth < 0) {\n return { position: i, message: 'Unexpected closing parenthesis' };\n }\n }\n }\n\n if (depth !== 0) {\n return {\n position: src.length - 1,\n message: 'Unbalanced parentheses: missing closing parenthesis',\n };\n }\n\n return null;\n}\n\nexport interface ScannedToken {\n value: string;\n isComma: boolean;\n isSlash: boolean;\n offset: number;\n}\n\n/**\n * Tokenize a style value string following tasty's scan() semantics:\n * - Whitespace splits tokens\n * - Commas separate groups\n * - Spaced slashes (`a / b`) separate parts\n * - Non-spaced slashes (`center/cover`) stay inside the token\n * - Parenthesized content stays as a single token\n * - Quoted strings stay as a single token\n * - `url(...)` content is never split\n */\nexport function scanTokens(src: string): ScannedToken[] {\n const result: ScannedToken[] = [];\n let depth = 0;\n let inUrl = false;\n let inQuote: string | 0 = 0;\n let start = 0;\n let i = 0;\n let pendingSlash = false;\n\n const flush = (isComma: boolean, isSlash: boolean) => {\n const actualSlash = isSlash || pendingSlash;\n pendingSlash = false;\n\n if (start < i) {\n result.push({\n value: src.slice(start, i),\n isComma,\n isSlash: actualSlash,\n offset: start,\n });\n } else if (isComma) {\n result.push({ value: '', isComma: true, isSlash: false, offset: i });\n } else if (actualSlash) {\n result.push({ value: '', isComma: false, isSlash: true, offset: i });\n }\n start = i + 1;\n };\n\n for (; i < src.length; i++) {\n const ch = src[i];\n\n if (inQuote) {\n if (ch === inQuote && src[i - 1] !== '\\\\') inQuote = 0;\n continue;\n }\n if (ch === '\"' || ch === \"'\") {\n inQuote = ch;\n continue;\n }\n\n if (ch === '(') {\n if (!depth) {\n const maybe = src.slice(Math.max(0, i - 3), i + 1);\n if (maybe === 'url(') inUrl = true;\n }\n depth++;\n continue;\n }\n if (ch === ')') {\n depth = Math.max(0, depth - 1);\n if (inUrl && depth === 0) inUrl = false;\n continue;\n }\n\n if (inUrl) continue;\n\n if (!depth) {\n if (ch === ',') {\n flush(true, false);\n continue;\n }\n if (ch === '/') {\n const prevIsWs = isWhitespace(src[i - 1]);\n const nextIsWs = isWhitespace(src[i + 1]);\n if (prevIsWs && nextIsWs) {\n pendingSlash = true;\n start = i + 1;\n continue;\n }\n continue;\n }\n if (isWhitespace(ch)) {\n flush(false, false);\n continue;\n }\n }\n }\n flush(false, false);\n\n return result;\n}\n"],"mappings":";AAAA,SAAgB,aAAa,IAAiC;AAC5D,QAAO,OAAO,OAAO,OAAO,QAAQ,OAAO,OAAQ,OAAO,QAAQ,OAAO;;;;;;AAoB3E,SAAgB,oBAAoB,KAAkC;CACpE,IAAI,QAAQ;CACZ,IAAI,UAAsB;AAE1B,MAAK,IAAI,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;EACnC,MAAM,KAAK,IAAI;AAEf,MAAI,SAAS;AACX,OAAI,OAAO,WAAW,IAAI,IAAI,OAAO,KAAM,WAAU;AACrD;;AAEF,MAAI,OAAO,QAAO,OAAO,KAAK;AAC5B,aAAU;AACV;;AAGF,MAAI,OAAO,IAAK;AAChB,MAAI,OAAO,KAAK;AACd;AACA,OAAI,QAAQ,EACV,QAAO;IAAE,UAAU;IAAG,SAAS;IAAkC;;;AAKvE,KAAI,UAAU,EACZ,QAAO;EACL,UAAU,IAAI,SAAS;EACvB,SAAS;EACV;AAGH,QAAO;;;;;;;;;;;;AAoBT,SAAgB,WAAW,KAA6B;CACtD,MAAM,SAAyB,EAAE;CACjC,IAAI,QAAQ;CACZ,IAAI,QAAQ;CACZ,IAAI,UAAsB;CAC1B,IAAI,QAAQ;CACZ,IAAI,IAAI;CACR,IAAI,eAAe;CAEnB,MAAM,SAAS,SAAkB,YAAqB;EACpD,MAAM,cAAc,WAAW;AAC/B,iBAAe;AAEf,MAAI,QAAQ,EACV,QAAO,KAAK;GACV,OAAO,IAAI,MAAM,OAAO,EAAE;GAC1B;GACA,SAAS;GACT,QAAQ;GACT,CAAC;WACO,QACT,QAAO,KAAK;GAAE,OAAO;GAAI,SAAS;GAAM,SAAS;GAAO,QAAQ;GAAG,CAAC;WAC3D,YACT,QAAO,KAAK;GAAE,OAAO;GAAI,SAAS;GAAO,SAAS;GAAM,QAAQ;GAAG,CAAC;AAEtE,UAAQ,IAAI;;AAGd,QAAO,IAAI,IAAI,QAAQ,KAAK;EAC1B,MAAM,KAAK,IAAI;AAEf,MAAI,SAAS;AACX,OAAI,OAAO,WAAW,IAAI,IAAI,OAAO,KAAM,WAAU;AACrD;;AAEF,MAAI,OAAO,QAAO,OAAO,KAAK;AAC5B,aAAU;AACV;;AAGF,MAAI,OAAO,KAAK;AACd,OAAI,CAAC,OAEH;QADc,IAAI,MAAM,KAAK,IAAI,GAAG,IAAI,EAAE,EAAE,IAAI,EAAE,KACpC,OAAQ,SAAQ;;AAEhC;AACA;;AAEF,MAAI,OAAO,KAAK;AACd,WAAQ,KAAK,IAAI,GAAG,QAAQ,EAAE;AAC9B,OAAI,SAAS,UAAU,EAAG,SAAQ;AAClC;;AAGF,MAAI,MAAO;AAEX,MAAI,CAAC,OAAO;AACV,OAAI,OAAO,KAAK;AACd,UAAM,MAAM,MAAM;AAClB;;AAEF,OAAI,OAAO,KAAK;IACd,MAAM,WAAW,aAAa,IAAI,IAAI,GAAG;IACzC,MAAM,WAAW,aAAa,IAAI,IAAI,GAAG;AACzC,QAAI,YAAY,UAAU;AACxB,oBAAe;AACf,aAAQ,IAAI;AACZ;;AAEF;;AAEF,OAAI,aAAa,GAAG,EAAE;AACpB,UAAM,OAAO,MAAM;AACnB;;;;AAIN,OAAM,OAAO,MAAM;AAEnB,QAAO"}
1
+ {"version":3,"file":"utils.js","names":[],"sources":["../../src/parsers/utils.ts"],"sourcesContent":["export function isWhitespace(ch: string | undefined): boolean {\n return ch === ' ' || ch === '\\n' || ch === '\\t' || ch === '\\r' || ch === '\\f';\n}\n\nexport function isDigit(ch: string): boolean {\n return ch >= '0' && ch <= '9';\n}\n\nexport function isAlpha(ch: string): boolean {\n return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z');\n}\n\n/**\n * Split on commas outside parentheses and quotes, so a comma inside a function\n * (`steps(2, start)`, `cubic-bezier(0.5, 0, 0.3, 1)`) stays in its part. Like\n * `split(',')`, the parts joined with `,` give back the input.\n */\nexport function splitTopLevelCommas(src: string): string[] {\n const parts: string[] = [];\n let depth = 0;\n let inQuote: string | 0 = 0;\n let start = 0;\n for (let i = 0; i < src.length; i++) {\n const ch = src[i];\n if (inQuote) {\n if (ch === inQuote) inQuote = 0;\n } else if (ch === '\"' || ch === \"'\") inQuote = ch;\n else if (ch === '(') depth++;\n else if (ch === ')') depth = Math.max(0, depth - 1);\n else if (ch === ',' && depth === 0) {\n parts.push(src.slice(start, i));\n start = i + 1;\n }\n }\n parts.push(src.slice(start));\n return parts;\n}\n\nexport interface BracketError {\n position: number;\n message: string;\n}\n\n/**\n * Check that all brackets/parentheses are balanced in a source string.\n * Returns null if balanced, or an error with position if not.\n */\nexport function checkBracketBalance(src: string): BracketError | null {\n let depth = 0;\n let inQuote: string | 0 = 0;\n\n for (let i = 0; i < src.length; i++) {\n const ch = src[i];\n\n if (inQuote) {\n if (ch === inQuote && src[i - 1] !== '\\\\') inQuote = 0;\n continue;\n }\n if (ch === '\"' || ch === \"'\") {\n inQuote = ch;\n continue;\n }\n\n if (ch === '(') depth++;\n if (ch === ')') {\n depth--;\n if (depth < 0) {\n return { position: i, message: 'Unexpected closing parenthesis' };\n }\n }\n }\n\n if (depth !== 0) {\n return {\n position: src.length - 1,\n message: 'Unbalanced parentheses: missing closing parenthesis',\n };\n }\n\n return null;\n}\n\nexport interface ScannedToken {\n value: string;\n isComma: boolean;\n isSlash: boolean;\n offset: number;\n}\n\n/**\n * Tokenize a style value string following tasty's scan() semantics:\n * - Whitespace splits tokens\n * - Commas separate groups\n * - Spaced slashes (`a / b`) separate parts\n * - Non-spaced slashes (`center/cover`) stay inside the token\n * - Parenthesized content stays as a single token\n * - Quoted strings stay as a single token\n * - `url(...)` content is never split\n */\nexport function scanTokens(src: string): ScannedToken[] {\n const result: ScannedToken[] = [];\n let depth = 0;\n let inUrl = false;\n let inQuote: string | 0 = 0;\n let start = 0;\n let i = 0;\n let pendingSlash = false;\n\n const flush = (isComma: boolean, isSlash: boolean) => {\n const actualSlash = isSlash || pendingSlash;\n pendingSlash = false;\n\n if (start < i) {\n result.push({\n value: src.slice(start, i),\n isComma,\n isSlash: actualSlash,\n offset: start,\n });\n } else if (isComma) {\n result.push({ value: '', isComma: true, isSlash: false, offset: i });\n } else if (actualSlash) {\n result.push({ value: '', isComma: false, isSlash: true, offset: i });\n }\n start = i + 1;\n };\n\n for (; i < src.length; i++) {\n const ch = src[i];\n\n if (inQuote) {\n if (ch === inQuote && src[i - 1] !== '\\\\') inQuote = 0;\n continue;\n }\n if (ch === '\"' || ch === \"'\") {\n inQuote = ch;\n continue;\n }\n\n if (ch === '(') {\n if (!depth) {\n const maybe = src.slice(Math.max(0, i - 3), i + 1);\n if (maybe === 'url(') inUrl = true;\n }\n depth++;\n continue;\n }\n if (ch === ')') {\n depth = Math.max(0, depth - 1);\n if (inUrl && depth === 0) inUrl = false;\n continue;\n }\n\n if (inUrl) continue;\n\n if (!depth) {\n if (ch === ',') {\n flush(true, false);\n continue;\n }\n if (ch === '/') {\n const prevIsWs = isWhitespace(src[i - 1]);\n const nextIsWs = isWhitespace(src[i + 1]);\n if (prevIsWs && nextIsWs) {\n pendingSlash = true;\n start = i + 1;\n continue;\n }\n continue;\n }\n if (isWhitespace(ch)) {\n flush(false, false);\n continue;\n }\n }\n }\n flush(false, false);\n\n return result;\n}\n"],"mappings":"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"}
@@ -2,6 +2,7 @@ import { createRule } from "../create-rule.js";
2
2
  import { KNOWN_CSS_PROPERTIES, SEMANTIC_TRANSITIONS, TRANSITION_SEMANTIC_MAPPING } from "../constants.js";
3
3
  import { TastyContext } from "../context.js";
4
4
  import { stringStyleValueListeners } from "../style-values.js";
5
+ import { splitTopLevelCommas } from "../parsers/utils.js";
5
6
  import { replaceInStringValue } from "../fix-utils.js";
6
7
 
7
8
  //#region src/rules/valid-transition.ts
@@ -44,7 +45,7 @@ var valid_transition_default = createRule({
44
45
  create(context) {
45
46
  const ctx = new TastyContext(context);
46
47
  function checkTransitionValue(value, node) {
47
- const groups = value.split(",");
48
+ const groups = splitTopLevelCommas(value);
48
49
  let offset = 0;
49
50
  for (const group of groups) {
50
51
  const parts = group.trim().split(/\s+/);
@@ -1 +1 @@
1
- {"version":3,"file":"valid-transition.js","names":[],"sources":["../../src/rules/valid-transition.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 {\n SEMANTIC_TRANSITIONS,\n KNOWN_CSS_PROPERTIES,\n TRANSITION_SEMANTIC_MAPPING,\n} from '../constants.js';\nimport { replaceInStringValue } from '../fix-utils.js';\n\ntype MessageIds =\n | 'unknownTransition'\n | 'preferSemanticTransition'\n | 'useSemantic'\n | 'camelCaseTransition'\n | 'useKebabCase';\n\n/**\n * `KNOWN_CSS_PROPERTIES` is keyed by camelCase style keys, but a `transition`\n * value names *CSS* properties, which are kebab-case (`text-decoration-color`).\n * Tasty emits an unmapped name verbatim into the CSS `transition`, so the\n * kebab form is the correct spelling — normalize before the lookup.\n */\nfunction kebabToCamel(name: string): string {\n return name.replace(/-([a-z])/g, (_, char: string) => char.toUpperCase());\n}\n\n/** `gridTemplateColumns` -> `grid-template-columns`. */\nfunction camelToKebab(name: string): string {\n return name.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);\n}\n\n/**\n * Tasty style names that `KNOWN_CSS_PROPERTIES` also lists, but that name no\n * property a browser implements. The `flow` style sets `flex-direction` /\n * `grid-auto-flow`, yet Tasty has no `flow` transition: it emits the name\n * verbatim (`transition: flow …`), which animates nothing.\n */\nconst NON_TRANSITION_STYLE_NAMES = new Set(['flow']);\n\nexport default createRule<[], MessageIds>({\n name: 'valid-transition',\n meta: {\n type: 'suggestion',\n hasSuggestions: true,\n docs: {\n description:\n 'Validate transition property values use valid semantic transition names',\n },\n messages: {\n unknownTransition:\n \"Unknown transition name '{{name}}'. Use a semantic name ({{known}}) or a CSS property name.\",\n preferSemanticTransition:\n \"Transition '{{native}}' has a Tasty semantic equivalent — use '{{semantic}}' instead.\",\n useSemantic: \"Use '{{semantic}}' instead of '{{native}}'\",\n camelCaseTransition:\n \"Transition '{{name}}' compiles to '{{lowered}}', which is not a CSS property. Use the CSS name '{{kebab}}'.\",\n useKebabCase: \"Use '{{kebab}}' instead of '{{name}}'\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkTransitionValue(value: string, node: TSESTree.Node): void {\n const groups = value.split(',');\n let offset = 0;\n\n for (const group of groups) {\n const parts = group.trim().split(/\\s+/);\n if (parts.length === 0 || parts[0] === '') {\n offset += group.length + 1;\n continue;\n }\n\n const name = parts[0];\n\n // $$ prefix is always valid (custom property reference: $$name -> --name)\n if (name.startsWith('$$')) {\n offset += group.length + 1;\n continue;\n }\n\n // ## prefix is always valid (color property reference: ##name -> --name-color)\n if (name.startsWith('##')) {\n offset += group.length + 1;\n continue;\n }\n\n const start = offset + group.indexOf(name);\n const end = start + name.length;\n\n const semantic = TRANSITION_SEMANTIC_MAPPING[name];\n if (semantic) {\n context.report({\n node,\n messageId: 'preferSemanticTransition',\n data: { native: name, semantic },\n suggest: [\n {\n messageId: 'useSemantic',\n data: { native: name, semantic },\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement: semantic }],\n context.sourceCode,\n );\n },\n },\n ],\n });\n offset += group.length + 1;\n continue;\n }\n\n // Tasty lowercases the name and emits it verbatim, so a camelCase CSS\n // property comes out as `gridtemplatecolumns`, which is no property.\n // Only the semantic names (`zIndex` -> `z-index`) are translated.\n if (\n /[A-Z]/.test(name) &&\n !SEMANTIC_TRANSITIONS.has(name) &&\n KNOWN_CSS_PROPERTIES.has(name)\n ) {\n const kebab = camelToKebab(name);\n const data = { name, lowered: name.toLowerCase(), kebab };\n context.report({\n node,\n messageId: 'camelCaseTransition',\n data,\n suggest: [\n {\n messageId: 'useKebabCase',\n data,\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement: kebab }],\n context.sourceCode,\n );\n },\n },\n ],\n });\n offset += group.length + 1;\n continue;\n }\n\n if (\n NON_TRANSITION_STYLE_NAMES.has(name) ||\n (!SEMANTIC_TRANSITIONS.has(name) &&\n !KNOWN_CSS_PROPERTIES.has(name) &&\n !KNOWN_CSS_PROPERTIES.has(kebabToCamel(name)) &&\n !name.startsWith('--') &&\n name !== 'all' &&\n name !== 'none')\n ) {\n context.report({\n node,\n messageId: 'unknownTransition',\n data: {\n name,\n known: [...SEMANTIC_TRANSITIONS].join(', '),\n },\n });\n }\n\n offset += group.length + 1;\n }\n }\n\n return stringStyleValueListeners(\n ctx,\n (value, node, property) => {\n if (property === 'transition') checkTransitionValue(value, node);\n },\n { requireTastyJSX: true },\n );\n },\n});\n"],"mappings":";;;;;;;;;;;;;AAwBA,SAAS,aAAa,MAAsB;AAC1C,QAAO,KAAK,QAAQ,cAAc,GAAG,SAAiB,KAAK,aAAa,CAAC;;;AAI3E,SAAS,aAAa,MAAsB;AAC1C,QAAO,KAAK,QAAQ,WAAW,SAAS,IAAI,KAAK,aAAa,GAAG;;;;;;;;AASnE,MAAM,6BAA6B,IAAI,IAAI,CAAC,OAAO,CAAC;AAEpD,+BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,gBAAgB;EAChB,MAAM,EACJ,aACE,2EACH;EACD,UAAU;GACR,mBACE;GACF,0BACE;GACF,aAAa;GACb,qBACE;GACF,cAAc;GACf;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,qBAAqB,OAAe,MAA2B;GACtE,MAAM,SAAS,MAAM,MAAM,IAAI;GAC/B,IAAI,SAAS;AAEb,QAAK,MAAM,SAAS,QAAQ;IAC1B,MAAM,QAAQ,MAAM,MAAM,CAAC,MAAM,MAAM;AACvC,QAAI,MAAM,WAAW,KAAK,MAAM,OAAO,IAAI;AACzC,eAAU,MAAM,SAAS;AACzB;;IAGF,MAAM,OAAO,MAAM;AAGnB,QAAI,KAAK,WAAW,KAAK,EAAE;AACzB,eAAU,MAAM,SAAS;AACzB;;AAIF,QAAI,KAAK,WAAW,KAAK,EAAE;AACzB,eAAU,MAAM,SAAS;AACzB;;IAGF,MAAM,QAAQ,SAAS,MAAM,QAAQ,KAAK;IAC1C,MAAM,MAAM,QAAQ,KAAK;IAEzB,MAAM,WAAW,4BAA4B;AAC7C,QAAI,UAAU;AACZ,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM;OAAE,QAAQ;OAAM;OAAU;MAChC,SAAS,CACP;OACE,WAAW;OACX,MAAM;QAAE,QAAQ;QAAM;QAAU;OAChC,IAAI,OAAO;AACT,eAAO,qBACL,OACA,MACA,CAAC;SAAE;SAAO;SAAK,aAAa;SAAU,CAAC,EACvC,QAAQ,WACT;;OAEJ,CACF;MACF,CAAC;AACF,eAAU,MAAM,SAAS;AACzB;;AAMF,QACE,QAAQ,KAAK,KAAK,IAClB,CAAC,qBAAqB,IAAI,KAAK,IAC/B,qBAAqB,IAAI,KAAK,EAC9B;KACA,MAAM,QAAQ,aAAa,KAAK;KAChC,MAAM,OAAO;MAAE;MAAM,SAAS,KAAK,aAAa;MAAE;MAAO;AACzD,aAAQ,OAAO;MACb;MACA,WAAW;MACX;MACA,SAAS,CACP;OACE,WAAW;OACX;OACA,IAAI,OAAO;AACT,eAAO,qBACL,OACA,MACA,CAAC;SAAE;SAAO;SAAK,aAAa;SAAO,CAAC,EACpC,QAAQ,WACT;;OAEJ,CACF;MACF,CAAC;AACF,eAAU,MAAM,SAAS;AACzB;;AAGF,QACE,2BAA2B,IAAI,KAAK,IACnC,CAAC,qBAAqB,IAAI,KAAK,IAC9B,CAAC,qBAAqB,IAAI,KAAK,IAC/B,CAAC,qBAAqB,IAAI,aAAa,KAAK,CAAC,IAC7C,CAAC,KAAK,WAAW,KAAK,IACtB,SAAS,SACT,SAAS,OAEX,SAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MACJ;MACA,OAAO,CAAC,GAAG,qBAAqB,CAAC,KAAK,KAAK;MAC5C;KACF,CAAC;AAGJ,cAAU,MAAM,SAAS;;;AAI7B,SAAO,0BACL,MACC,OAAO,MAAM,aAAa;AACzB,OAAI,aAAa,aAAc,sBAAqB,OAAO,KAAK;KAElE,EAAE,iBAAiB,MAAM,CAC1B;;CAEJ,CAAC"}
1
+ {"version":3,"file":"valid-transition.js","names":[],"sources":["../../src/rules/valid-transition.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 {\n SEMANTIC_TRANSITIONS,\n KNOWN_CSS_PROPERTIES,\n TRANSITION_SEMANTIC_MAPPING,\n} from '../constants.js';\nimport { replaceInStringValue } from '../fix-utils.js';\nimport { splitTopLevelCommas } from '../parsers/utils.js';\n\ntype MessageIds =\n | 'unknownTransition'\n | 'preferSemanticTransition'\n | 'useSemantic'\n | 'camelCaseTransition'\n | 'useKebabCase';\n\n/**\n * `KNOWN_CSS_PROPERTIES` is keyed by camelCase style keys, but a `transition`\n * value names *CSS* properties, which are kebab-case (`text-decoration-color`).\n * Tasty emits an unmapped name verbatim into the CSS `transition`, so the\n * kebab form is the correct spelling — normalize before the lookup.\n */\nfunction kebabToCamel(name: string): string {\n return name.replace(/-([a-z])/g, (_, char: string) => char.toUpperCase());\n}\n\n/** `gridTemplateColumns` -> `grid-template-columns`. */\nfunction camelToKebab(name: string): string {\n return name.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);\n}\n\n/**\n * Tasty style names that `KNOWN_CSS_PROPERTIES` also lists, but that name no\n * property a browser implements. The `flow` style sets `flex-direction` /\n * `grid-auto-flow`, yet Tasty has no `flow` transition: it emits the name\n * verbatim (`transition: flow …`), which animates nothing.\n */\nconst NON_TRANSITION_STYLE_NAMES = new Set(['flow']);\n\nexport default createRule<[], MessageIds>({\n name: 'valid-transition',\n meta: {\n type: 'suggestion',\n hasSuggestions: true,\n docs: {\n description:\n 'Validate transition property values use valid semantic transition names',\n },\n messages: {\n unknownTransition:\n \"Unknown transition name '{{name}}'. Use a semantic name ({{known}}) or a CSS property name.\",\n preferSemanticTransition:\n \"Transition '{{native}}' has a Tasty semantic equivalent — use '{{semantic}}' instead.\",\n useSemantic: \"Use '{{semantic}}' instead of '{{native}}'\",\n camelCaseTransition:\n \"Transition '{{name}}' compiles to '{{lowered}}', which is not a CSS property. Use the CSS name '{{kebab}}'.\",\n useKebabCase: \"Use '{{kebab}}' instead of '{{name}}'\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkTransitionValue(value: string, node: TSESTree.Node): void {\n const groups = splitTopLevelCommas(value);\n let offset = 0;\n\n for (const group of groups) {\n const parts = group.trim().split(/\\s+/);\n if (parts.length === 0 || parts[0] === '') {\n offset += group.length + 1;\n continue;\n }\n\n const name = parts[0];\n\n // $$ prefix is always valid (custom property reference: $$name -> --name)\n if (name.startsWith('$$')) {\n offset += group.length + 1;\n continue;\n }\n\n // ## prefix is always valid (color property reference: ##name -> --name-color)\n if (name.startsWith('##')) {\n offset += group.length + 1;\n continue;\n }\n\n const start = offset + group.indexOf(name);\n const end = start + name.length;\n\n const semantic = TRANSITION_SEMANTIC_MAPPING[name];\n if (semantic) {\n context.report({\n node,\n messageId: 'preferSemanticTransition',\n data: { native: name, semantic },\n suggest: [\n {\n messageId: 'useSemantic',\n data: { native: name, semantic },\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement: semantic }],\n context.sourceCode,\n );\n },\n },\n ],\n });\n offset += group.length + 1;\n continue;\n }\n\n // Tasty lowercases the name and emits it verbatim, so a camelCase CSS\n // property comes out as `gridtemplatecolumns`, which is no property.\n // Only the semantic names (`zIndex` -> `z-index`) are translated.\n if (\n /[A-Z]/.test(name) &&\n !SEMANTIC_TRANSITIONS.has(name) &&\n KNOWN_CSS_PROPERTIES.has(name)\n ) {\n const kebab = camelToKebab(name);\n const data = { name, lowered: name.toLowerCase(), kebab };\n context.report({\n node,\n messageId: 'camelCaseTransition',\n data,\n suggest: [\n {\n messageId: 'useKebabCase',\n data,\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement: kebab }],\n context.sourceCode,\n );\n },\n },\n ],\n });\n offset += group.length + 1;\n continue;\n }\n\n if (\n NON_TRANSITION_STYLE_NAMES.has(name) ||\n (!SEMANTIC_TRANSITIONS.has(name) &&\n !KNOWN_CSS_PROPERTIES.has(name) &&\n !KNOWN_CSS_PROPERTIES.has(kebabToCamel(name)) &&\n !name.startsWith('--') &&\n name !== 'all' &&\n name !== 'none')\n ) {\n context.report({\n node,\n messageId: 'unknownTransition',\n data: {\n name,\n known: [...SEMANTIC_TRANSITIONS].join(', '),\n },\n });\n }\n\n offset += group.length + 1;\n }\n }\n\n return stringStyleValueListeners(\n ctx,\n (value, node, property) => {\n if (property === 'transition') checkTransitionValue(value, node);\n },\n { requireTastyJSX: true },\n );\n },\n});\n"],"mappings":";;;;;;;;;;;;;;AAyBA,SAAS,aAAa,MAAsB;AAC1C,QAAO,KAAK,QAAQ,cAAc,GAAG,SAAiB,KAAK,aAAa,CAAC;;;AAI3E,SAAS,aAAa,MAAsB;AAC1C,QAAO,KAAK,QAAQ,WAAW,SAAS,IAAI,KAAK,aAAa,GAAG;;;;;;;;AASnE,MAAM,6BAA6B,IAAI,IAAI,CAAC,OAAO,CAAC;AAEpD,+BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,gBAAgB;EAChB,MAAM,EACJ,aACE,2EACH;EACD,UAAU;GACR,mBACE;GACF,0BACE;GACF,aAAa;GACb,qBACE;GACF,cAAc;GACf;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,qBAAqB,OAAe,MAA2B;GACtE,MAAM,SAAS,oBAAoB,MAAM;GACzC,IAAI,SAAS;AAEb,QAAK,MAAM,SAAS,QAAQ;IAC1B,MAAM,QAAQ,MAAM,MAAM,CAAC,MAAM,MAAM;AACvC,QAAI,MAAM,WAAW,KAAK,MAAM,OAAO,IAAI;AACzC,eAAU,MAAM,SAAS;AACzB;;IAGF,MAAM,OAAO,MAAM;AAGnB,QAAI,KAAK,WAAW,KAAK,EAAE;AACzB,eAAU,MAAM,SAAS;AACzB;;AAIF,QAAI,KAAK,WAAW,KAAK,EAAE;AACzB,eAAU,MAAM,SAAS;AACzB;;IAGF,MAAM,QAAQ,SAAS,MAAM,QAAQ,KAAK;IAC1C,MAAM,MAAM,QAAQ,KAAK;IAEzB,MAAM,WAAW,4BAA4B;AAC7C,QAAI,UAAU;AACZ,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM;OAAE,QAAQ;OAAM;OAAU;MAChC,SAAS,CACP;OACE,WAAW;OACX,MAAM;QAAE,QAAQ;QAAM;QAAU;OAChC,IAAI,OAAO;AACT,eAAO,qBACL,OACA,MACA,CAAC;SAAE;SAAO;SAAK,aAAa;SAAU,CAAC,EACvC,QAAQ,WACT;;OAEJ,CACF;MACF,CAAC;AACF,eAAU,MAAM,SAAS;AACzB;;AAMF,QACE,QAAQ,KAAK,KAAK,IAClB,CAAC,qBAAqB,IAAI,KAAK,IAC/B,qBAAqB,IAAI,KAAK,EAC9B;KACA,MAAM,QAAQ,aAAa,KAAK;KAChC,MAAM,OAAO;MAAE;MAAM,SAAS,KAAK,aAAa;MAAE;MAAO;AACzD,aAAQ,OAAO;MACb;MACA,WAAW;MACX;MACA,SAAS,CACP;OACE,WAAW;OACX;OACA,IAAI,OAAO;AACT,eAAO,qBACL,OACA,MACA,CAAC;SAAE;SAAO;SAAK,aAAa;SAAO,CAAC,EACpC,QAAQ,WACT;;OAEJ,CACF;MACF,CAAC;AACF,eAAU,MAAM,SAAS;AACzB;;AAGF,QACE,2BAA2B,IAAI,KAAK,IACnC,CAAC,qBAAqB,IAAI,KAAK,IAC9B,CAAC,qBAAqB,IAAI,KAAK,IAC/B,CAAC,qBAAqB,IAAI,aAAa,KAAK,CAAC,IAC7C,CAAC,KAAK,WAAW,KAAK,IACtB,SAAS,SACT,SAAS,OAEX,SAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MACJ;MACA,OAAO,CAAC,GAAG,qBAAqB,CAAC,KAAK,KAAK;MAC5C;KACF,CAAC;AAGJ,cAAU,MAAM,SAAS;;;AAI7B,SAAO,0BACL,MACC,OAAO,MAAM,aAAa;AACzB,OAAI,aAAa,aAAc,sBAAqB,OAAO,KAAK;KAElE,EAAE,iBAAiB,MAAM,CAC1B;;CAEJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenphi/eslint-plugin-tasty",
3
- "version": "1.8.3",
3
+ "version": "1.8.4",
4
4
  "description": "ESLint plugin for validating tasty() and tastyStatic() style objects",
5
5
  "type": "module",
6
6
  "exports": {