@tenphi/eslint-plugin-tasty 0.8.0 → 0.9.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 +2 -2
- package/dist/configs.js +1 -1
- package/dist/configs.js.map +1 -1
- package/dist/constants.js +191 -3
- package/dist/constants.js.map +1 -1
- package/dist/fix-utils.js +92 -0
- package/dist/fix-utils.js.map +1 -0
- package/dist/rules/consistent-token-usage.js +59 -12
- package/dist/rules/consistent-token-usage.js.map +1 -1
- package/dist/rules/no-important.js +12 -4
- package/dist/rules/no-important.js.map +1 -1
- package/dist/rules/no-nested-selector.js +2 -2
- package/dist/rules/no-nested-selector.js.map +1 -1
- package/dist/rules/no-own-at-root.js +9 -1
- package/dist/rules/no-own-at-root.js.map +1 -1
- package/dist/rules/no-raw-color-values.js +23 -7
- package/dist/rules/no-raw-color-values.js.map +1 -1
- package/dist/rules/prefer-auto-calc.js +23 -1
- package/dist/rules/prefer-auto-calc.js.map +1 -1
- package/dist/rules/prefer-custom-property-syntax.js +79 -4
- package/dist/rules/prefer-custom-property-syntax.js.map +1 -1
- package/dist/rules/prefer-directional-shorthand.js +5 -0
- package/dist/rules/prefer-directional-shorthand.js.map +1 -1
- package/dist/rules/prefer-hide.js +15 -9
- package/dist/rules/prefer-hide.js.map +1 -1
- package/dist/rules/prefer-shorthand-property.js +5 -0
- package/dist/rules/prefer-shorthand-property.js.map +1 -1
- package/dist/rules/valid-default-state-order.js +36 -5
- package/dist/rules/valid-default-state-order.js.map +1 -1
- package/dist/rules/valid-radius-shape.js +18 -2
- package/dist/rules/valid-radius-shape.js.map +1 -1
- package/dist/rules/valid-transition.js +37 -5
- package/dist/rules/valid-transition.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consistent-token-usage.js","names":[],"sources":["../../src/rules/consistent-token-usage.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\n\ntype MessageIds = 'preferToken';\n\nconst PX_TO_UNIT: Record<string, string> = {\n '8px': '1x',\n '16px': '2x',\n '24px': '3x',\n '32px': '4x',\n '40px': '5x',\n '48px': '6x',\n '56px': '7x',\n '64px': '8x',\n};\n\nexport default createRule<[], MessageIds>({\n name: 'consistent-token-usage',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Suggest using design tokens and custom units instead of raw CSS values',\n },\n messages: {\n preferToken: \"Consider using '{{suggestion}}' instead of '{{raw}}'.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkValue(\n property: string,\n value: string,\n node: TSESTree.Node,\n ): void {\n const trimmed = value.trim();\n\n // Check pixel values that map to gap multiples\n if (trimmed in PX_TO_UNIT) {\n context.report({\n node,\n messageId: 'preferToken',\n data: { suggestion:
|
|
1
|
+
{"version":3,"file":"consistent-token-usage.js","names":[],"sources":["../../src/rules/consistent-token-usage.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { replaceStringValue, replaceInStringValue } from '../fix-utils.js';\n\ntype MessageIds = 'preferToken' | 'replaceWithToken';\n\nconst PX_TO_UNIT: Record<string, string> = {\n '8px': '1x',\n '16px': '2x',\n '24px': '3x',\n '32px': '4x',\n '40px': '5x',\n '48px': '6x',\n '56px': '7x',\n '64px': '8x',\n};\n\nexport default createRule<[], MessageIds>({\n name: 'consistent-token-usage',\n meta: {\n type: 'suggestion',\n hasSuggestions: true,\n docs: {\n description:\n 'Suggest using design tokens and custom units instead of raw CSS values',\n },\n messages: {\n preferToken: \"Consider using '{{suggestion}}' instead of '{{raw}}'.\",\n replaceWithToken: \"Replace '{{raw}}' with '{{suggestion}}'\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkValue(\n property: string,\n value: string,\n node: TSESTree.Node,\n ): void {\n const trimmed = value.trim();\n\n // Check pixel values that map to gap multiples\n if (trimmed in PX_TO_UNIT) {\n const suggestion = PX_TO_UNIT[trimmed];\n context.report({\n node,\n messageId: 'preferToken',\n data: { suggestion, raw: trimmed },\n suggest: [\n {\n messageId: 'replaceWithToken',\n data: { raw: trimmed, suggestion },\n fix(fixer) {\n return replaceStringValue(fixer, node, suggestion);\n },\n },\n ],\n });\n return;\n }\n\n // Check 6px in radius context\n if (property === 'radius' && trimmed === '6px') {\n context.report({\n node,\n messageId: 'preferToken',\n data: { suggestion: '1r', raw: '6px' },\n suggest: [\n {\n messageId: 'replaceWithToken',\n data: { raw: '6px', suggestion: '1r' },\n fix(fixer) {\n return replaceStringValue(fixer, node, '1r');\n },\n },\n ],\n });\n return;\n }\n\n // Check 1px in border context\n if (property === 'border' && trimmed.includes('1px')) {\n const edits: { start: number; end: number; replacement: string }[] = [];\n const re = /\\b1px\\b/g;\n let match: RegExpExecArray | null;\n while ((match = re.exec(value)) !== null) {\n edits.push({\n start: match.index,\n end: match.index + match[0].length,\n replacement: '1bw',\n });\n }\n if (edits.length === 0) return;\n context.report({\n node,\n messageId: 'preferToken',\n data: { suggestion: '1bw', raw: '1px' },\n suggest: [\n {\n messageId: 'replaceWithToken',\n data: { raw: '1px', suggestion: '1bw' },\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n edits,\n context.sourceCode,\n );\n },\n },\n ],\n });\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkValue(key, str, prop.value);\n continue;\n }\n\n if (prop.value.type === 'ObjectExpression') {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr) {\n checkValue(key, stateStr, stateProp.value);\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAQA,MAAM,aAAqC;CACzC,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACT;AAED,qCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,gBAAgB;EAChB,MAAM,EACJ,aACE,0EACH;EACD,UAAU;GACR,aAAa;GACb,kBAAkB;GACnB;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,WACP,UACA,OACA,MACM;GACN,MAAM,UAAU,MAAM,MAAM;AAG5B,OAAI,WAAW,YAAY;IACzB,MAAM,aAAa,WAAW;AAC9B,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE;MAAY,KAAK;MAAS;KAClC,SAAS,CACP;MACE,WAAW;MACX,MAAM;OAAE,KAAK;OAAS;OAAY;MAClC,IAAI,OAAO;AACT,cAAO,mBAAmB,OAAO,MAAM,WAAW;;MAErD,CACF;KACF,CAAC;AACF;;AAIF,OAAI,aAAa,YAAY,YAAY,OAAO;AAC9C,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE,YAAY;MAAM,KAAK;MAAO;KACtC,SAAS,CACP;MACE,WAAW;MACX,MAAM;OAAE,KAAK;OAAO,YAAY;OAAM;MACtC,IAAI,OAAO;AACT,cAAO,mBAAmB,OAAO,MAAM,KAAK;;MAE/C,CACF;KACF,CAAC;AACF;;AAIF,OAAI,aAAa,YAAY,QAAQ,SAAS,MAAM,EAAE;IACpD,MAAM,QAA+D,EAAE;IACvE,MAAM,KAAK;IACX,IAAI;AACJ,YAAQ,QAAQ,GAAG,KAAK,MAAM,MAAM,KAClC,OAAM,KAAK;KACT,OAAO,MAAM;KACb,KAAK,MAAM,QAAQ,MAAM,GAAG;KAC5B,aAAa;KACd,CAAC;AAEJ,QAAI,MAAM,WAAW,EAAG;AACxB,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE,YAAY;MAAO,KAAK;MAAO;KACvC,SAAS,CACP;MACE,WAAW;MACX,MAAM;OAAE,KAAK;OAAO,YAAY;OAAO;MACvC,IAAI,OAAO;AACT,cAAO,qBACL,OACA,MACA,OACA,QAAQ,WACT;;MAEJ,CACF;KACF,CAAC;;;EAIN,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,QAAQ,KAAM;IAElB,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,KAAK;AACP,gBAAW,KAAK,KAAK,KAAK,MAAM;AAChC;;AAGF,QAAI,KAAK,MAAM,SAAS,mBACtB,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,WAAW,eAAe,UAAU,MAAM;AAChD,SAAI,SACF,YAAW,KAAK,UAAU,UAAU,MAAM;;;;AAOpD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { createRule } from "../create-rule.js";
|
|
2
2
|
import { getStringValue } from "../utils.js";
|
|
3
3
|
import { TastyContext, styleObjectListeners } from "../context.js";
|
|
4
|
+
import { replaceStringValue } from "../fix-utils.js";
|
|
4
5
|
|
|
5
6
|
//#region src/rules/no-important.ts
|
|
6
7
|
var no_important_default = createRule({
|
|
7
8
|
name: "no-important",
|
|
8
9
|
meta: {
|
|
9
10
|
type: "problem",
|
|
11
|
+
fixable: "code",
|
|
10
12
|
docs: { description: "Disallow !important in tasty style values" },
|
|
11
13
|
messages: { noImportant: "Do not use !important in tasty styles. The tasty system manages specificity via doubled selectors and state ordering." },
|
|
12
14
|
schema: []
|
|
@@ -16,10 +18,16 @@ var no_important_default = createRule({
|
|
|
16
18
|
const ctx = new TastyContext(context);
|
|
17
19
|
function checkNode(node) {
|
|
18
20
|
const str = getStringValue(node);
|
|
19
|
-
if (str && str.includes("!important"))
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
if (str && str.includes("!important")) {
|
|
22
|
+
const stripped = str.replace(/\s*!important\s*/g, "").trim();
|
|
23
|
+
context.report({
|
|
24
|
+
node,
|
|
25
|
+
messageId: "noImportant",
|
|
26
|
+
fix(fixer) {
|
|
27
|
+
return replaceStringValue(fixer, node, stripped);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
23
31
|
}
|
|
24
32
|
function handleStyleObject(node) {
|
|
25
33
|
if (!ctx.isStyleObject(node)) return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-important.js","names":[],"sources":["../../src/rules/no-important.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getStringValue } from '../utils.js';\n\ntype MessageIds = 'noImportant';\n\nexport default createRule<[], MessageIds>({\n name: 'no-important',\n meta: {\n type: 'problem',\n docs: {\n description: 'Disallow !important in tasty style values',\n },\n messages: {\n noImportant:\n 'Do not use !important in tasty styles. The tasty system manages specificity via doubled selectors and state ordering.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkNode(node: TSESTree.Node): void {\n const str = getStringValue(node);\n if (str && str.includes('!important')) {\n context.report({
|
|
1
|
+
{"version":3,"file":"no-important.js","names":[],"sources":["../../src/rules/no-important.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getStringValue } from '../utils.js';\nimport { replaceStringValue } from '../fix-utils.js';\n\ntype MessageIds = 'noImportant';\n\nexport default createRule<[], MessageIds>({\n name: 'no-important',\n meta: {\n type: 'problem',\n fixable: 'code',\n docs: {\n description: 'Disallow !important in tasty style values',\n },\n messages: {\n noImportant:\n 'Do not use !important in tasty styles. The tasty system manages specificity via doubled selectors and state ordering.',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkNode(node: TSESTree.Node): void {\n const str = getStringValue(node);\n if (str && str.includes('!important')) {\n const stripped = str.replace(/\\s*!important\\s*/g, '').trim();\n context.report({\n node,\n messageId: 'noImportant',\n fix(fixer) {\n return replaceStringValue(fixer, node, stripped);\n },\n });\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property') continue;\n\n checkNode(prop.value);\n\n if (prop.value.type === 'ObjectExpression') {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type === 'Property') {\n checkNode(stateProp.value);\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAQA,2BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aAAa,6CACd;EACD,UAAU,EACR,aACE,yHACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,UAAU,MAA2B;GAC5C,MAAM,MAAM,eAAe,KAAK;AAChC,OAAI,OAAO,IAAI,SAAS,aAAa,EAAE;IACrC,MAAM,WAAW,IAAI,QAAQ,qBAAqB,GAAG,CAAC,MAAM;AAC5D,YAAQ,OAAO;KACb;KACA,WAAW;KACX,IAAI,OAAO;AACT,aAAO,mBAAmB,OAAO,MAAM,SAAS;;KAEnD,CAAC;;;EAIN,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,WAAY;AAE9B,cAAU,KAAK,MAAM;AAErB,QAAI,KAAK,MAAM,SAAS,oBACtB;UAAK,MAAM,aAAa,KAAK,MAAM,WACjC,KAAI,UAAU,SAAS,WACrB,WAAU,UAAU,MAAM;;;;AAOpC,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -6,8 +6,8 @@ import { TastyContext, styleObjectListeners } from "../context.js";
|
|
|
6
6
|
var no_nested_selector_default = createRule({
|
|
7
7
|
name: "no-nested-selector",
|
|
8
8
|
meta: {
|
|
9
|
-
type: "
|
|
10
|
-
docs: { description: "
|
|
9
|
+
type: "problem",
|
|
10
|
+
docs: { description: "Disallow &-prefixed nested selectors in favor of sub-element styling" },
|
|
11
11
|
messages: { noNestedSelector: "Avoid nested selectors ('{{key}}'). Use sub-element styling with capitalized keys and data-element attributes instead." },
|
|
12
12
|
schema: []
|
|
13
13
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-nested-selector.js","names":[],"sources":["../../src/rules/no-nested-selector.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName } from '../utils.js';\n\ntype MessageIds = 'noNestedSelector';\n\nexport default createRule<[], MessageIds>({\n name: 'no-nested-selector',\n meta: {\n type: '
|
|
1
|
+
{"version":3,"file":"no-nested-selector.js","names":[],"sources":["../../src/rules/no-nested-selector.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName } from '../utils.js';\n\ntype MessageIds = 'noNestedSelector';\n\nexport default createRule<[], MessageIds>({\n name: 'no-nested-selector',\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Disallow &-prefixed nested selectors in favor of sub-element styling',\n },\n messages: {\n noNestedSelector:\n \"Avoid nested selectors ('{{key}}'). Use sub-element styling with capitalized keys and data-element attributes instead.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n\n if (key.startsWith('&') && !key.startsWith('&::')) {\n context.report({\n node: prop.key,\n messageId: 'noNestedSelector',\n data: { key },\n });\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;AAOA,iCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,wEACH;EACD,UAAU,EACR,kBACE,0HACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,QAAQ,KAAM;AAElB,QAAI,IAAI,WAAW,IAAI,IAAI,CAAC,IAAI,WAAW,MAAM,CAC/C,SAAQ,OAAO;KACb,MAAM,KAAK;KACX,WAAW;KACX,MAAM,EAAE,KAAK;KACd,CAAC;;;AAKR,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -2,12 +2,14 @@ import { createRule } from "../create-rule.js";
|
|
|
2
2
|
import { getKeyName, getStringValue } from "../utils.js";
|
|
3
3
|
import { TastyContext, styleObjectListeners } from "../context.js";
|
|
4
4
|
import { parseStateKey } from "../parsers/state-key-parser.js";
|
|
5
|
+
import { replaceStringValue } from "../fix-utils.js";
|
|
5
6
|
|
|
6
7
|
//#region src/rules/no-own-at-root.ts
|
|
7
8
|
var no_own_at_root_default = createRule({
|
|
8
9
|
name: "no-own-at-root",
|
|
9
10
|
meta: {
|
|
10
11
|
type: "suggestion",
|
|
12
|
+
fixable: "code",
|
|
11
13
|
docs: { description: "Warn when @own() is used outside sub-element styles where it is redundant" },
|
|
12
14
|
messages: { ownAtRoot: "@own() is redundant outside sub-element styles. Use the inner selector directly instead (e.g. :hover instead of @own(:hover))." },
|
|
13
15
|
schema: []
|
|
@@ -38,7 +40,13 @@ var no_own_at_root_default = createRule({
|
|
|
38
40
|
if (stateKey === null || stateKey === "" || stateKey === "_") continue;
|
|
39
41
|
if (parseStateKey(stateKey).hasOwn) context.report({
|
|
40
42
|
node: stateProp.key,
|
|
41
|
-
messageId: "ownAtRoot"
|
|
43
|
+
messageId: "ownAtRoot",
|
|
44
|
+
fix(fixer) {
|
|
45
|
+
if (!stateKey.startsWith("@own(") || !stateKey.endsWith(")")) return null;
|
|
46
|
+
const inner = stateKey.slice(5, -1);
|
|
47
|
+
if (!inner) return null;
|
|
48
|
+
return replaceStringValue(fixer, stateProp.key, inner);
|
|
49
|
+
}
|
|
42
50
|
});
|
|
43
51
|
}
|
|
44
52
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-own-at-root.js","names":[],"sources":["../../src/rules/no-own-at-root.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { parseStateKey } from '../parsers/state-key-parser.js';\n\ntype MessageIds = 'ownAtRoot';\n\nexport default createRule<[], MessageIds>({\n name: 'no-own-at-root',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Warn when @own() is used outside sub-element styles where it is redundant',\n },\n messages: {\n ownAtRoot:\n '@own() is redundant outside sub-element styles. Use the inner selector directly instead (e.g. :hover instead of @own(:hover)).',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function isInsideSubElement(node: TSESTree.Node): boolean {\n let current: TSESTree.Node | undefined = node.parent;\n while (current) {\n if (\n current.type === 'Property' &&\n !current.computed &&\n current.key.type === 'Identifier' &&\n /^[A-Z]/.test(current.key.name)\n ) {\n return true;\n }\n current = current.parent;\n }\n return false;\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n const insideSubElement = isInsideSubElement(node);\n\n if (insideSubElement) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n\n if (/^[A-Z]/.test(key) || key.startsWith('@') || key.startsWith('&'))\n continue;\n\n if (prop.value.type !== 'ObjectExpression') continue;\n\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n\n const stateKey = !stateProp.computed\n ? getKeyName(stateProp.key)\n : getStringValue(stateProp.key);\n if (stateKey === null || stateKey === '' || stateKey === '_')\n continue;\n\n const result = parseStateKey(stateKey);\n\n if (result.hasOwn) {\n context.report({\n node: stateProp.key,\n messageId: 'ownAtRoot',\n });\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"no-own-at-root.js","names":[],"sources":["../../src/rules/no-own-at-root.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { parseStateKey } from '../parsers/state-key-parser.js';\nimport { replaceStringValue } from '../fix-utils.js';\n\ntype MessageIds = 'ownAtRoot';\n\nexport default createRule<[], MessageIds>({\n name: 'no-own-at-root',\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description:\n 'Warn when @own() is used outside sub-element styles where it is redundant',\n },\n messages: {\n ownAtRoot:\n '@own() is redundant outside sub-element styles. Use the inner selector directly instead (e.g. :hover instead of @own(:hover)).',\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function isInsideSubElement(node: TSESTree.Node): boolean {\n let current: TSESTree.Node | undefined = node.parent;\n while (current) {\n if (\n current.type === 'Property' &&\n !current.computed &&\n current.key.type === 'Identifier' &&\n /^[A-Z]/.test(current.key.name)\n ) {\n return true;\n }\n current = current.parent;\n }\n return false;\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n const insideSubElement = isInsideSubElement(node);\n\n if (insideSubElement) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n\n if (/^[A-Z]/.test(key) || key.startsWith('@') || key.startsWith('&'))\n continue;\n\n if (prop.value.type !== 'ObjectExpression') continue;\n\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n\n const stateKey = !stateProp.computed\n ? getKeyName(stateProp.key)\n : getStringValue(stateProp.key);\n if (stateKey === null || stateKey === '' || stateKey === '_')\n continue;\n\n const result = parseStateKey(stateKey);\n\n if (result.hasOwn) {\n context.report({\n node: stateProp.key,\n messageId: 'ownAtRoot',\n fix(fixer) {\n if (!stateKey.startsWith('@own(') || !stateKey.endsWith(')')) {\n return null;\n }\n const inner = stateKey.slice(5, -1);\n if (!inner) return null;\n return replaceStringValue(fixer, stateProp.key, inner);\n },\n });\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;;AASA,6BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aACE,6EACH;EACD,UAAU,EACR,WACE,kIACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,mBAAmB,MAA8B;GACxD,IAAI,UAAqC,KAAK;AAC9C,UAAO,SAAS;AACd,QACE,QAAQ,SAAS,cACjB,CAAC,QAAQ,YACT,QAAQ,IAAI,SAAS,gBACrB,SAAS,KAAK,QAAQ,IAAI,KAAK,CAE/B,QAAO;AAET,cAAU,QAAQ;;AAEpB,UAAO;;EAGT,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAI9B,OAFyB,mBAAmB,KAAK,CAE3B;AAEtB,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,QAAQ,KAAM;AAElB,QAAI,SAAS,KAAK,IAAI,IAAI,IAAI,WAAW,IAAI,IAAI,IAAI,WAAW,IAAI,CAClE;AAEF,QAAI,KAAK,MAAM,SAAS,mBAAoB;AAE5C,SAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KAEnC,MAAM,WAAW,CAAC,UAAU,WACxB,WAAW,UAAU,IAAI,GACzB,eAAe,UAAU,IAAI;AACjC,SAAI,aAAa,QAAQ,aAAa,MAAM,aAAa,IACvD;AAIF,SAFe,cAAc,SAAS,CAE3B,OACT,SAAQ,OAAO;MACb,MAAM,UAAU;MAChB,WAAW;MACX,IAAI,OAAO;AACT,WAAI,CAAC,SAAS,WAAW,QAAQ,IAAI,CAAC,SAAS,SAAS,IAAI,CAC1D,QAAO;OAET,MAAM,QAAQ,SAAS,MAAM,GAAG,GAAG;AACnC,WAAI,CAAC,MAAO,QAAO;AACnB,cAAO,mBAAmB,OAAO,UAAU,KAAK,MAAM;;MAEzD,CAAC;;;;AAMV,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAGvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { createRule } from "../create-rule.js";
|
|
2
|
-
import {
|
|
2
|
+
import { COLOR_BEARING_PROPERTIES, NAMED_CSS_COLORS } from "../constants.js";
|
|
3
|
+
import { getKeyName, getStringValue } from "../utils.js";
|
|
3
4
|
import { TastyContext, styleObjectListeners } from "../context.js";
|
|
4
5
|
|
|
5
6
|
//#region src/rules/no-raw-color-values.ts
|
|
6
7
|
const HEX_COLOR_REGEX = /#([0-9a-fA-F]{3,8})\b/g;
|
|
7
|
-
const COLOR_FUNC_REGEX = /\b(rgb|rgba|hsl|hsla)\s*\(/gi;
|
|
8
|
+
const COLOR_FUNC_REGEX = /\b(rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|okhsl|okhsv|okhst|color|device-cmyk|light-dark)\s*\(/gi;
|
|
9
|
+
const NAMED_COLOR_REGEX = new RegExp(`\\b(${[...NAMED_CSS_COLORS].sort((a, b) => b.length - a.length).join("|")})\\b`, "gi");
|
|
8
10
|
var no_raw_color_values_default = createRule({
|
|
9
11
|
name: "no-raw-color-values",
|
|
10
12
|
meta: {
|
|
11
13
|
type: "suggestion",
|
|
12
|
-
docs: { description: "Suggest using color tokens instead of raw hex/rgb
|
|
14
|
+
docs: { description: "Suggest using color tokens instead of raw hex/rgb/okhsl/named colors" },
|
|
13
15
|
messages: {
|
|
14
16
|
rawHexColor: "Use a color token instead of raw hex color '{{value}}'.",
|
|
15
|
-
rawColorFunction: "Use a color token instead of raw {{func}}() color."
|
|
17
|
+
rawColorFunction: "Use a color token instead of raw {{func}}() color.",
|
|
18
|
+
rawNamedColor: "Use a color token instead of raw named color '{{name}}'."
|
|
16
19
|
},
|
|
17
20
|
schema: []
|
|
18
21
|
},
|
|
@@ -34,7 +37,7 @@ var no_raw_color_values_default = createRule({
|
|
|
34
37
|
}
|
|
35
38
|
return false;
|
|
36
39
|
}
|
|
37
|
-
function checkValue(value, node) {
|
|
40
|
+
function checkValue(value, node, propertyKey) {
|
|
38
41
|
if (isInTokenDefinition(node)) return;
|
|
39
42
|
HEX_COLOR_REGEX.lastIndex = 0;
|
|
40
43
|
let match;
|
|
@@ -57,17 +60,30 @@ var no_raw_color_values_default = createRule({
|
|
|
57
60
|
messageId: "rawColorFunction",
|
|
58
61
|
data: { func: match[1] }
|
|
59
62
|
});
|
|
63
|
+
if (propertyKey && COLOR_BEARING_PROPERTIES.has(propertyKey)) {
|
|
64
|
+
NAMED_COLOR_REGEX.lastIndex = 0;
|
|
65
|
+
while ((match = NAMED_COLOR_REGEX.exec(value)) !== null) {
|
|
66
|
+
const prev = match.index > 0 ? value[match.index - 1] : "";
|
|
67
|
+
if (prev === "#" || prev === "$") continue;
|
|
68
|
+
context.report({
|
|
69
|
+
node,
|
|
70
|
+
messageId: "rawNamedColor",
|
|
71
|
+
data: { name: match[1] }
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
60
75
|
}
|
|
61
76
|
function handleStyleObject(node) {
|
|
62
77
|
if (!ctx.isStyleObject(node)) return;
|
|
63
78
|
for (const prop of node.properties) {
|
|
64
79
|
if (prop.type !== "Property") continue;
|
|
80
|
+
const key = getKeyName(prop.key);
|
|
65
81
|
const str = getStringValue(prop.value);
|
|
66
|
-
if (str) checkValue(str, prop.value);
|
|
82
|
+
if (str) checkValue(str, prop.value, key);
|
|
67
83
|
if (prop.value.type === "ObjectExpression") for (const stateProp of prop.value.properties) {
|
|
68
84
|
if (stateProp.type !== "Property") continue;
|
|
69
85
|
const stateStr = getStringValue(stateProp.value);
|
|
70
|
-
if (stateStr) checkValue(stateStr, stateProp.value);
|
|
86
|
+
if (stateStr) checkValue(stateStr, stateProp.value, key);
|
|
71
87
|
}
|
|
72
88
|
}
|
|
73
89
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-raw-color-values.js","names":[],"sources":["../../src/rules/no-raw-color-values.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getStringValue } from '../utils.js';\n\ntype MessageIds = 'rawHexColor' | 'rawColorFunction';\n\nconst HEX_COLOR_REGEX = /#([0-9a-fA-F]{3,8})\\b/g;\nconst COLOR_FUNC_REGEX
|
|
1
|
+
{"version":3,"file":"no-raw-color-values.js","names":[],"sources":["../../src/rules/no-raw-color-values.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getStringValue, getKeyName } from '../utils.js';\nimport { COLOR_BEARING_PROPERTIES, NAMED_CSS_COLORS } from '../constants.js';\n\ntype MessageIds = 'rawHexColor' | 'rawColorFunction' | 'rawNamedColor';\n\nconst HEX_COLOR_REGEX = /#([0-9a-fA-F]{3,8})\\b/g;\nconst COLOR_FUNC_REGEX =\n /\\b(rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|okhsl|okhsv|okhst|color|device-cmyk|light-dark)\\s*\\(/gi;\nconst NAMED_COLOR_REGEX = new RegExp(\n `\\\\b(${[...NAMED_CSS_COLORS].sort((a, b) => b.length - a.length).join('|')})\\\\b`,\n 'gi',\n);\n\nexport default createRule<[], MessageIds>({\n name: 'no-raw-color-values',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Suggest using color tokens instead of raw hex/rgb/okhsl/named colors',\n },\n messages: {\n rawHexColor: \"Use a color token instead of raw hex color '{{value}}'.\",\n rawColorFunction: 'Use a color token instead of raw {{func}}() color.',\n rawNamedColor: \"Use a color token instead of raw named color '{{name}}'.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function isInTokenDefinition(node: TSESTree.Node): boolean {\n // Check if this is inside a :root or token-defining context\n let current: TSESTree.Node | undefined = node;\n while (current) {\n if (current.type === 'CallExpression') {\n const imp = ctx.isTastyCall(current);\n if (imp && imp.importedName === 'tastyStatic') {\n const firstArg = current.arguments[0];\n const selectorStr = getStringValue(firstArg);\n if (selectorStr === ':root') return true;\n }\n break;\n }\n current = current.parent;\n }\n return false;\n }\n\n function checkValue(\n value: string,\n node: TSESTree.Node,\n propertyKey?: string | null,\n ): void {\n if (isInTokenDefinition(node)) return;\n\n // Check hex colors\n HEX_COLOR_REGEX.lastIndex = 0;\n let match;\n while ((match = HEX_COLOR_REGEX.exec(value)) !== null) {\n const hex = match[1];\n if ([3, 4, 6, 8].includes(hex.length)) {\n context.report({\n node,\n messageId: 'rawHexColor',\n data: { value: match[0] },\n });\n }\n }\n\n // Check color functions (rgb, hsl, okhsl, okhst, oklch, …)\n COLOR_FUNC_REGEX.lastIndex = 0;\n while ((match = COLOR_FUNC_REGEX.exec(value)) !== null) {\n context.report({\n node,\n messageId: 'rawColorFunction',\n data: { func: match[1] },\n });\n }\n\n // Check named CSS colors — scoped to color-bearing properties to avoid\n // false positives on string properties like `content` or `cursor`. Skip\n // matches that are part of a `#token` or `$prop` reference (e.g. `#purple`,\n // `$red`) — those are Tasty tokens/custom properties, not raw colors.\n if (propertyKey && COLOR_BEARING_PROPERTIES.has(propertyKey)) {\n NAMED_COLOR_REGEX.lastIndex = 0;\n while ((match = NAMED_COLOR_REGEX.exec(value)) !== null) {\n const prev = match.index > 0 ? value[match.index - 1] : '';\n if (prev === '#' || prev === '$') continue;\n context.report({\n node,\n messageId: 'rawNamedColor',\n data: { name: match[1] },\n });\n }\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property') continue;\n const key = getKeyName(prop.key);\n\n const str = getStringValue(prop.value);\n if (str) checkValue(str, prop.value, key);\n\n if (prop.value.type === 'ObjectExpression') {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr) checkValue(stateStr, stateProp.value, key);\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAQA,MAAM,kBAAkB;AACxB,MAAM,mBACJ;AACF,MAAM,oBAAoB,IAAI,OAC5B,OAAO,CAAC,GAAG,iBAAiB,CAAC,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC,OAC3E,KACD;AAED,kCAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,wEACH;EACD,UAAU;GACR,aAAa;GACb,kBAAkB;GAClB,eAAe;GAChB;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,oBAAoB,MAA8B;GAEzD,IAAI,UAAqC;AACzC,UAAO,SAAS;AACd,QAAI,QAAQ,SAAS,kBAAkB;KACrC,MAAM,MAAM,IAAI,YAAY,QAAQ;AACpC,SAAI,OAAO,IAAI,iBAAiB,eAAe;MAC7C,MAAM,WAAW,QAAQ,UAAU;AAEnC,UADoB,eAAe,SAAS,KACxB,QAAS,QAAO;;AAEtC;;AAEF,cAAU,QAAQ;;AAEpB,UAAO;;EAGT,SAAS,WACP,OACA,MACA,aACM;AACN,OAAI,oBAAoB,KAAK,CAAE;AAG/B,mBAAgB,YAAY;GAC5B,IAAI;AACJ,WAAQ,QAAQ,gBAAgB,KAAK,MAAM,MAAM,MAAM;IACrD,MAAM,MAAM,MAAM;AAClB,QAAI;KAAC;KAAG;KAAG;KAAG;KAAE,CAAC,SAAS,IAAI,OAAO,CACnC,SAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM,EAAE,OAAO,MAAM,IAAI;KAC1B,CAAC;;AAKN,oBAAiB,YAAY;AAC7B,WAAQ,QAAQ,iBAAiB,KAAK,MAAM,MAAM,KAChD,SAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM,EAAE,MAAM,MAAM,IAAI;IACzB,CAAC;AAOJ,OAAI,eAAe,yBAAyB,IAAI,YAAY,EAAE;AAC5D,sBAAkB,YAAY;AAC9B,YAAQ,QAAQ,kBAAkB,KAAK,MAAM,MAAM,MAAM;KACvD,MAAM,OAAO,MAAM,QAAQ,IAAI,MAAM,MAAM,QAAQ,KAAK;AACxD,SAAI,SAAS,OAAO,SAAS,IAAK;AAClC,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM,EAAE,MAAM,MAAM,IAAI;MACzB,CAAC;;;;EAKR,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,WAAY;IAC9B,MAAM,MAAM,WAAW,KAAK,IAAI;IAEhC,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,IAAK,YAAW,KAAK,KAAK,OAAO,IAAI;AAEzC,QAAI,KAAK,MAAM,SAAS,mBACtB,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,WAAW,eAAe,UAAU,MAAM;AAChD,SAAI,SAAU,YAAW,UAAU,UAAU,OAAO,IAAI;;;;AAMhE,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -2,12 +2,14 @@ import { createRule } from "../create-rule.js";
|
|
|
2
2
|
import { getKeyName, getStringValue } from "../utils.js";
|
|
3
3
|
import { TastyContext, styleObjectListeners } from "../context.js";
|
|
4
4
|
import { parseValue } from "../parsers/value-parser.js";
|
|
5
|
+
import { replaceInStringValue } from "../fix-utils.js";
|
|
5
6
|
|
|
6
7
|
//#region src/rules/prefer-auto-calc.ts
|
|
7
8
|
var prefer_auto_calc_default = createRule({
|
|
8
9
|
name: "prefer-auto-calc",
|
|
9
10
|
meta: {
|
|
10
11
|
type: "suggestion",
|
|
12
|
+
fixable: "code",
|
|
11
13
|
docs: { description: "Suggest Tasty auto-calc parentheses instead of explicit calc()" },
|
|
12
14
|
messages: { preferAutoCalc: "Replace 'calc({{inner}})' with Tasty auto-calc '({{inner}})' (calc() is added automatically)." },
|
|
13
15
|
schema: []
|
|
@@ -17,12 +19,32 @@ var prefer_auto_calc_default = createRule({
|
|
|
17
19
|
const ctx = new TastyContext(context);
|
|
18
20
|
function checkValue(value, node) {
|
|
19
21
|
const result = parseValue(value, { skipUnitValidation: true });
|
|
22
|
+
let cursor = 0;
|
|
20
23
|
for (const group of result.groups) for (const part of group.parts) for (const token of part.tokens) {
|
|
21
24
|
if (token.type !== "css-function" || token.name !== "calc") continue;
|
|
25
|
+
const start = value.indexOf(token.raw, cursor);
|
|
26
|
+
if (start === -1) {
|
|
27
|
+
context.report({
|
|
28
|
+
node,
|
|
29
|
+
messageId: "preferAutoCalc",
|
|
30
|
+
data: { inner: token.args }
|
|
31
|
+
});
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
cursor = start + token.raw.length;
|
|
35
|
+
const end = cursor;
|
|
36
|
+
const replacement = `(${token.args})`;
|
|
22
37
|
context.report({
|
|
23
38
|
node,
|
|
24
39
|
messageId: "preferAutoCalc",
|
|
25
|
-
data: { inner: token.args }
|
|
40
|
+
data: { inner: token.args },
|
|
41
|
+
fix(fixer) {
|
|
42
|
+
return replaceInStringValue(fixer, node, [{
|
|
43
|
+
start,
|
|
44
|
+
end,
|
|
45
|
+
replacement
|
|
46
|
+
}], context.sourceCode);
|
|
47
|
+
}
|
|
26
48
|
});
|
|
27
49
|
}
|
|
28
50
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-auto-calc.js","names":[],"sources":["../../src/rules/prefer-auto-calc.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { parseValue } from '../parsers/value-parser.js';\n\ntype MessageIds = 'preferAutoCalc';\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-auto-calc',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Suggest Tasty auto-calc parentheses instead of explicit calc()',\n },\n messages: {\n preferAutoCalc:\n \"Replace 'calc({{inner}})' with Tasty auto-calc '({{inner}})' (calc() is added automatically).\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkValue(value: string, node: TSESTree.Node): void {\n const result = parseValue(value, { skipUnitValidation: true });\n\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 !== 'css-function' || token.name !== 'calc') {\n continue;\n }\n\n context.report({\n node,\n messageId: 'preferAutoCalc',\n data: { inner: token.args },\n });\n }\n }\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n if (/^[A-Z@&$#]/.test(key)) continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkValue(str, prop.value);\n continue;\n }\n\n if (prop.value.type === 'ObjectExpression') {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr) {\n checkValue(stateStr, stateProp.value);\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"prefer-auto-calc.js","names":[],"sources":["../../src/rules/prefer-auto-calc.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { parseValue } from '../parsers/value-parser.js';\nimport { replaceInStringValue } from '../fix-utils.js';\n\ntype MessageIds = 'preferAutoCalc';\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-auto-calc',\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description:\n 'Suggest Tasty auto-calc parentheses instead of explicit calc()',\n },\n messages: {\n preferAutoCalc:\n \"Replace 'calc({{inner}})' with Tasty auto-calc '({{inner}})' (calc() is added automatically).\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkValue(value: string, node: TSESTree.Node): void {\n const result = parseValue(value, { skipUnitValidation: true });\n\n let cursor = 0;\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 !== 'css-function' || token.name !== 'calc') {\n continue;\n }\n\n const start = value.indexOf(token.raw, cursor);\n if (start === -1) {\n context.report({\n node,\n messageId: 'preferAutoCalc',\n data: { inner: token.args },\n });\n continue;\n }\n cursor = start + token.raw.length;\n const end = cursor;\n const replacement = `(${token.args})`;\n\n context.report({\n node,\n messageId: 'preferAutoCalc',\n data: { inner: token.args },\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement }],\n context.sourceCode,\n );\n },\n });\n }\n }\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n if (/^[A-Z@&$#]/.test(key)) continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkValue(str, prop.value);\n continue;\n }\n\n if (prop.value.type === 'ObjectExpression') {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr) {\n checkValue(stateStr, stateProp.value);\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;;AASA,+BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aACE,kEACH;EACD,UAAU,EACR,gBACE,iGACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,WAAW,OAAe,MAA2B;GAC5D,MAAM,SAAS,WAAW,OAAO,EAAE,oBAAoB,MAAM,CAAC;GAE9D,IAAI,SAAS;AACb,QAAK,MAAM,SAAS,OAAO,OACzB,MAAK,MAAM,QAAQ,MAAM,MACvB,MAAK,MAAM,SAAS,KAAK,QAAQ;AAC/B,QAAI,MAAM,SAAS,kBAAkB,MAAM,SAAS,OAClD;IAGF,MAAM,QAAQ,MAAM,QAAQ,MAAM,KAAK,OAAO;AAC9C,QAAI,UAAU,IAAI;AAChB,aAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM,EAAE,OAAO,MAAM,MAAM;MAC5B,CAAC;AACF;;AAEF,aAAS,QAAQ,MAAM,IAAI;IAC3B,MAAM,MAAM;IACZ,MAAM,cAAc,IAAI,MAAM,KAAK;AAEnC,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM,EAAE,OAAO,MAAM,MAAM;KAC3B,IAAI,OAAO;AACT,aAAO,qBACL,OACA,MACA,CAAC;OAAE;OAAO;OAAK;OAAa,CAAC,EAC7B,QAAQ,WACT;;KAEJ,CAAC;;;EAMV,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,QAAQ,KAAM;AAClB,QAAI,aAAa,KAAK,IAAI,CAAE;IAE5B,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,KAAK;AACP,gBAAW,KAAK,KAAK,MAAM;AAC3B;;AAGF,QAAI,KAAK,MAAM,SAAS,mBACtB,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,WAAW,eAAe,UAAU,MAAM;AAChD,SAAI,SACF,YAAW,UAAU,UAAU,MAAM;;;;AAO/C,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -1,31 +1,52 @@
|
|
|
1
1
|
import { createRule } from "../create-rule.js";
|
|
2
2
|
import { getKeyName, getStringValue } from "../utils.js";
|
|
3
3
|
import { TastyContext, styleObjectListeners } from "../context.js";
|
|
4
|
+
import { replaceInStringValue } from "../fix-utils.js";
|
|
4
5
|
|
|
5
6
|
//#region src/rules/prefer-custom-property-syntax.ts
|
|
6
7
|
const VAR_REGEX = /var\(\s*--([a-zA-Z0-9_-]+)\s*(?:,\s*([^)]+))?\s*\)/g;
|
|
8
|
+
const COLOR_PROP_REGEX = /(?<![$#])\$([a-zA-Z][a-zA-Z0-9_-]*)-color\b(\.[0-9]+|\.\$[a-zA-Z][a-zA-Z0-9_-]*)?/g;
|
|
9
|
+
const KEYWORD_REGEX = /\b(transparent|currentColor)\b/gi;
|
|
10
|
+
function normalizeFallback(fallback) {
|
|
11
|
+
const trimmed = fallback.trim();
|
|
12
|
+
const lower = trimmed.toLowerCase();
|
|
13
|
+
if (lower === "transparent") return "#clear";
|
|
14
|
+
if (lower === "currentcolor") return "#current";
|
|
15
|
+
return trimmed;
|
|
16
|
+
}
|
|
7
17
|
function suggestVarSyntax(name, fallback) {
|
|
8
18
|
if (name.endsWith("-color")) {
|
|
9
19
|
const tastyToken = `#${name.slice(0, -6)}`;
|
|
10
|
-
if (fallback) return `(${tastyToken}, ${fallback
|
|
20
|
+
if (fallback) return `(${tastyToken}, ${normalizeFallback(fallback)})`;
|
|
11
21
|
return tastyToken;
|
|
12
22
|
}
|
|
13
23
|
const propToken = `$${name}`;
|
|
14
|
-
if (fallback) return `(${propToken}, ${fallback
|
|
24
|
+
if (fallback) return `(${propToken}, ${normalizeFallback(fallback)})`;
|
|
15
25
|
return propToken;
|
|
16
26
|
}
|
|
27
|
+
function isInsideSpans(start, end, spans) {
|
|
28
|
+
for (const span of spans) if (start >= span.start && end <= span.end) return true;
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
17
31
|
var prefer_custom_property_syntax_default = createRule({
|
|
18
32
|
name: "prefer-custom-property-syntax",
|
|
19
33
|
meta: {
|
|
20
34
|
type: "suggestion",
|
|
21
|
-
|
|
22
|
-
|
|
35
|
+
fixable: "code",
|
|
36
|
+
docs: { description: "Prefer Tasty token syntax ($prop, #color, #clear, #current) over raw CSS (var(), $x-color, transparent, currentColor)" },
|
|
37
|
+
messages: {
|
|
38
|
+
preferCustomPropertySyntax: "Use Tasty token syntax '{{suggestion}}' instead of '{{raw}}'.",
|
|
39
|
+
preferColorToken: "Use color token '{{suggestion}}' instead of custom property reference '{{raw}}'.",
|
|
40
|
+
preferClearToken: "Use the '#clear' token instead of the 'transparent' keyword.",
|
|
41
|
+
preferCurrentToken: "Use the '#current' token instead of the 'currentColor' keyword."
|
|
42
|
+
},
|
|
23
43
|
schema: []
|
|
24
44
|
},
|
|
25
45
|
defaultOptions: [],
|
|
26
46
|
create(context) {
|
|
27
47
|
const ctx = new TastyContext(context);
|
|
28
48
|
function checkValue(value, node) {
|
|
49
|
+
const varSpans = [];
|
|
29
50
|
VAR_REGEX.lastIndex = 0;
|
|
30
51
|
let match;
|
|
31
52
|
while ((match = VAR_REGEX.exec(value)) !== null) {
|
|
@@ -33,12 +54,66 @@ var prefer_custom_property_syntax_default = createRule({
|
|
|
33
54
|
const name = match[1];
|
|
34
55
|
const fallback = match[2];
|
|
35
56
|
const suggestion = suggestVarSyntax(name, fallback);
|
|
57
|
+
const start = match.index;
|
|
58
|
+
const end = start + raw.length;
|
|
59
|
+
if (match.index !== void 0) varSpans.push({
|
|
60
|
+
start: match.index,
|
|
61
|
+
end: match.index + raw.length
|
|
62
|
+
});
|
|
36
63
|
context.report({
|
|
37
64
|
node,
|
|
38
65
|
messageId: "preferCustomPropertySyntax",
|
|
39
66
|
data: {
|
|
40
67
|
raw,
|
|
41
68
|
suggestion
|
|
69
|
+
},
|
|
70
|
+
fix(fixer) {
|
|
71
|
+
return replaceInStringValue(fixer, node, [{
|
|
72
|
+
start,
|
|
73
|
+
end,
|
|
74
|
+
replacement: suggestion
|
|
75
|
+
}], context.sourceCode);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
COLOR_PROP_REGEX.lastIndex = 0;
|
|
80
|
+
while ((match = COLOR_PROP_REGEX.exec(value)) !== null) {
|
|
81
|
+
const raw = match[0];
|
|
82
|
+
const suggestion = `#${match[1]}${match[2] ?? ""}`;
|
|
83
|
+
const start = match.index;
|
|
84
|
+
const end = start + raw.length;
|
|
85
|
+
context.report({
|
|
86
|
+
node,
|
|
87
|
+
messageId: "preferColorToken",
|
|
88
|
+
data: {
|
|
89
|
+
raw,
|
|
90
|
+
suggestion
|
|
91
|
+
},
|
|
92
|
+
fix(fixer) {
|
|
93
|
+
return replaceInStringValue(fixer, node, [{
|
|
94
|
+
start,
|
|
95
|
+
end,
|
|
96
|
+
replacement: suggestion
|
|
97
|
+
}], context.sourceCode);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
KEYWORD_REGEX.lastIndex = 0;
|
|
102
|
+
while ((match = KEYWORD_REGEX.exec(value)) !== null) {
|
|
103
|
+
const start = match.index;
|
|
104
|
+
const end = start + match[0].length;
|
|
105
|
+
if (isInsideSpans(start, end, varSpans)) continue;
|
|
106
|
+
const lower = match[0].toLowerCase();
|
|
107
|
+
const replacement = lower === "transparent" ? "#clear" : "#current";
|
|
108
|
+
context.report({
|
|
109
|
+
node,
|
|
110
|
+
messageId: lower === "transparent" ? "preferClearToken" : "preferCurrentToken",
|
|
111
|
+
fix(fixer) {
|
|
112
|
+
return replaceInStringValue(fixer, node, [{
|
|
113
|
+
start,
|
|
114
|
+
end,
|
|
115
|
+
replacement
|
|
116
|
+
}], context.sourceCode);
|
|
42
117
|
}
|
|
43
118
|
});
|
|
44
119
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-custom-property-syntax.js","names":[],"sources":["../../src/rules/prefer-custom-property-syntax.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\n\ntype MessageIds
|
|
1
|
+
{"version":3,"file":"prefer-custom-property-syntax.js","names":[],"sources":["../../src/rules/prefer-custom-property-syntax.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { replaceInStringValue } from '../fix-utils.js';\n\ntype MessageIds =\n | 'preferCustomPropertySyntax'\n | 'preferColorToken'\n | 'preferClearToken'\n | 'preferCurrentToken';\n\nconst VAR_REGEX = /var\\(\\s*--([a-zA-Z0-9_-]+)\\s*(?:,\\s*([^)]+))?\\s*\\)/g;\n// `$x-color` → `#x`. Negative lookbehind avoids `$$func` / `##transition`.\n// `\\b` after `-color` avoids matching `$x-colorful`. Optional opacity suffix.\nconst COLOR_PROP_REGEX =\n /(?<![$#])\\$([a-zA-Z][a-zA-Z0-9_-]*)-color\\b(\\.[0-9]+|\\.\\$[a-zA-Z][a-zA-Z0-9_-]*)?/g;\nconst KEYWORD_REGEX = /\\b(transparent|currentColor)\\b/gi;\n\nfunction normalizeFallback(fallback: string): string {\n const trimmed = fallback.trim();\n const lower = trimmed.toLowerCase();\n if (lower === 'transparent') return '#clear';\n if (lower === 'currentcolor') return '#current';\n return trimmed;\n}\n\nfunction suggestVarSyntax(name: string, fallback?: string): string {\n if (name.endsWith('-color')) {\n const tokenName = name.slice(0, -'-color'.length);\n const tastyToken = `#${tokenName}`;\n if (fallback) {\n return `(${tastyToken}, ${normalizeFallback(fallback)})`;\n }\n return tastyToken;\n }\n\n const propToken = `$${name}`;\n if (fallback) {\n return `(${propToken}, ${normalizeFallback(fallback)})`;\n }\n return propToken;\n}\n\ninterface Span {\n start: number;\n end: number;\n}\n\nfunction isInsideSpans(start: number, end: number, spans: Span[]): boolean {\n for (const span of spans) {\n if (start >= span.start && end <= span.end) return true;\n }\n return false;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-custom-property-syntax',\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description:\n 'Prefer Tasty token syntax ($prop, #color, #clear, #current) over raw CSS (var(), $x-color, transparent, currentColor)',\n },\n messages: {\n preferCustomPropertySyntax:\n \"Use Tasty token syntax '{{suggestion}}' instead of '{{raw}}'.\",\n preferColorToken:\n \"Use color token '{{suggestion}}' instead of custom property reference '{{raw}}'.\",\n preferClearToken:\n \"Use the '#clear' token instead of the 'transparent' keyword.\",\n preferCurrentToken:\n \"Use the '#current' token instead of the 'currentColor' keyword.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkValue(value: string, node: TSESTree.Node): void {\n // Pass 1: var(--x) / var(--x, fallback) → $x / (#x, fallback).\n // Records spans so standalone keywords inside var() aren't double-reported.\n const varSpans: Span[] = [];\n VAR_REGEX.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = VAR_REGEX.exec(value)) !== null) {\n const raw = match[0];\n const name = match[1];\n const fallback = match[2];\n const suggestion = suggestVarSyntax(name, fallback);\n const start = match.index;\n const end = start + raw.length;\n\n if (match.index !== undefined) {\n varSpans.push({\n start: match.index,\n end: match.index + raw.length,\n });\n }\n\n context.report({\n node,\n messageId: 'preferCustomPropertySyntax',\n data: { raw, suggestion },\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement: suggestion }],\n context.sourceCode,\n );\n },\n });\n }\n\n // Pass 2: $x-color → #x.\n COLOR_PROP_REGEX.lastIndex = 0;\n while ((match = COLOR_PROP_REGEX.exec(value)) !== null) {\n const raw = match[0];\n const name = match[1];\n const opacity = match[2] ?? '';\n const suggestion = `#${name}${opacity}`;\n const start = match.index;\n const end = start + raw.length;\n\n context.report({\n node,\n messageId: 'preferColorToken',\n data: { raw, suggestion },\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement: suggestion }],\n context.sourceCode,\n );\n },\n });\n }\n\n // Pass 3: standalone transparent / currentColor (skip inside var()).\n KEYWORD_REGEX.lastIndex = 0;\n while ((match = KEYWORD_REGEX.exec(value)) !== null) {\n const start = match.index;\n const end = start + match[0].length;\n if (isInsideSpans(start, end, varSpans)) continue;\n\n const lower = match[0].toLowerCase();\n const replacement = lower === 'transparent' ? '#clear' : '#current';\n context.report({\n node,\n messageId:\n lower === 'transparent' ? 'preferClearToken' : 'preferCurrentToken',\n fix(fixer) {\n return replaceInStringValue(\n fixer,\n node,\n [{ start, end, replacement }],\n context.sourceCode,\n );\n },\n });\n }\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n if (/^[A-Z@&$#]/.test(key)) continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkValue(str, prop.value);\n continue;\n }\n\n if (prop.value.type === 'ObjectExpression') {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr) {\n checkValue(stateStr, stateProp.value);\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;AAYA,MAAM,YAAY;AAGlB,MAAM,mBACJ;AACF,MAAM,gBAAgB;AAEtB,SAAS,kBAAkB,UAA0B;CACnD,MAAM,UAAU,SAAS,MAAM;CAC/B,MAAM,QAAQ,QAAQ,aAAa;AACnC,KAAI,UAAU,cAAe,QAAO;AACpC,KAAI,UAAU,eAAgB,QAAO;AACrC,QAAO;;AAGT,SAAS,iBAAiB,MAAc,UAA2B;AACjE,KAAI,KAAK,SAAS,SAAS,EAAE;EAE3B,MAAM,aAAa,IADD,KAAK,MAAM,GAAG,GAAiB;AAEjD,MAAI,SACF,QAAO,IAAI,WAAW,IAAI,kBAAkB,SAAS,CAAC;AAExD,SAAO;;CAGT,MAAM,YAAY,IAAI;AACtB,KAAI,SACF,QAAO,IAAI,UAAU,IAAI,kBAAkB,SAAS,CAAC;AAEvD,QAAO;;AAQT,SAAS,cAAc,OAAe,KAAa,OAAwB;AACzE,MAAK,MAAM,QAAQ,MACjB,KAAI,SAAS,KAAK,SAAS,OAAO,KAAK,IAAK,QAAO;AAErD,QAAO;;AAGT,4CAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aACE,yHACH;EACD,UAAU;GACR,4BACE;GACF,kBACE;GACF,kBACE;GACF,oBACE;GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,WAAW,OAAe,MAA2B;GAG5D,MAAM,WAAmB,EAAE;AAC3B,aAAU,YAAY;GACtB,IAAI;AACJ,WAAQ,QAAQ,UAAU,KAAK,MAAM,MAAM,MAAM;IAC/C,MAAM,MAAM,MAAM;IAClB,MAAM,OAAO,MAAM;IACnB,MAAM,WAAW,MAAM;IACvB,MAAM,aAAa,iBAAiB,MAAM,SAAS;IACnD,MAAM,QAAQ,MAAM;IACpB,MAAM,MAAM,QAAQ,IAAI;AAExB,QAAI,MAAM,UAAU,OAClB,UAAS,KAAK;KACZ,OAAO,MAAM;KACb,KAAK,MAAM,QAAQ,IAAI;KACxB,CAAC;AAGJ,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE;MAAK;MAAY;KACzB,IAAI,OAAO;AACT,aAAO,qBACL,OACA,MACA,CAAC;OAAE;OAAO;OAAK,aAAa;OAAY,CAAC,EACzC,QAAQ,WACT;;KAEJ,CAAC;;AAIJ,oBAAiB,YAAY;AAC7B,WAAQ,QAAQ,iBAAiB,KAAK,MAAM,MAAM,MAAM;IACtD,MAAM,MAAM,MAAM;IAGlB,MAAM,aAAa,IAFN,MAAM,KACH,MAAM,MAAM;IAE5B,MAAM,QAAQ,MAAM;IACpB,MAAM,MAAM,QAAQ,IAAI;AAExB,YAAQ,OAAO;KACb;KACA,WAAW;KACX,MAAM;MAAE;MAAK;MAAY;KACzB,IAAI,OAAO;AACT,aAAO,qBACL,OACA,MACA,CAAC;OAAE;OAAO;OAAK,aAAa;OAAY,CAAC,EACzC,QAAQ,WACT;;KAEJ,CAAC;;AAIJ,iBAAc,YAAY;AAC1B,WAAQ,QAAQ,cAAc,KAAK,MAAM,MAAM,MAAM;IACnD,MAAM,QAAQ,MAAM;IACpB,MAAM,MAAM,QAAQ,MAAM,GAAG;AAC7B,QAAI,cAAc,OAAO,KAAK,SAAS,CAAE;IAEzC,MAAM,QAAQ,MAAM,GAAG,aAAa;IACpC,MAAM,cAAc,UAAU,gBAAgB,WAAW;AACzD,YAAQ,OAAO;KACb;KACA,WACE,UAAU,gBAAgB,qBAAqB;KACjD,IAAI,OAAO;AACT,aAAO,qBACL,OACA,MACA,CAAC;OAAE;OAAO;OAAK;OAAa,CAAC,EAC7B,QAAQ,WACT;;KAEJ,CAAC;;;EAIN,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,QAAQ,KAAM;AAClB,QAAI,aAAa,KAAK,IAAI,CAAE;IAE5B,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,KAAK;AACP,gBAAW,KAAK,KAAK,MAAM;AAC3B;;AAGF,QAAI,KAAK,MAAM,SAAS,mBACtB,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,WAAW,eAAe,UAAU,MAAM;AAChD,SAAI,SACF,YAAW,UAAU,UAAU,MAAM;;;;AAO/C,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -2,6 +2,7 @@ import { createRule } from "../create-rule.js";
|
|
|
2
2
|
import { DIRECTIONAL_MODIFIERS } from "../constants.js";
|
|
3
3
|
import { getKeyName, getStringValue } from "../utils.js";
|
|
4
4
|
import { TastyContext, styleObjectListeners } from "../context.js";
|
|
5
|
+
import { replaceStringValue } from "../fix-utils.js";
|
|
5
6
|
|
|
6
7
|
//#region src/rules/prefer-directional-shorthand.ts
|
|
7
8
|
const SIDES = [
|
|
@@ -30,6 +31,7 @@ var prefer_directional_shorthand_default = createRule({
|
|
|
30
31
|
name: "prefer-directional-shorthand",
|
|
31
32
|
meta: {
|
|
32
33
|
type: "suggestion",
|
|
34
|
+
fixable: "code",
|
|
33
35
|
docs: { description: "Suggest Tasty directional shorthand instead of 4-value CSS box syntax with zero placeholders" },
|
|
34
36
|
messages: { preferDirectionalShorthand: "'{{property}}' value '{{raw}}' only sets one side. Use the directional shorthand '{{suggestion}}' (e.g. '1x bottom') instead." },
|
|
35
37
|
schema: []
|
|
@@ -47,6 +49,9 @@ var prefer_directional_shorthand_default = createRule({
|
|
|
47
49
|
property,
|
|
48
50
|
raw: value,
|
|
49
51
|
suggestion
|
|
52
|
+
},
|
|
53
|
+
fix(fixer) {
|
|
54
|
+
return replaceStringValue(fixer, node, suggestion);
|
|
50
55
|
}
|
|
51
56
|
});
|
|
52
57
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-directional-shorthand.js","names":[],"sources":["../../src/rules/prefer-directional-shorthand.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { DIRECTIONAL_MODIFIERS } from '../constants.js';\n\ntype MessageIds = 'preferDirectionalShorthand';\n\nconst SIDES = ['top', 'right', 'bottom', 'left'] as const;\n\nfunction isZeroLike(token: string): boolean {\n const trimmed = token.trim();\n if (trimmed === '0') return true;\n return /^0+(?:\\.0+)?(?:px|em|rem|%|x|r|cr|bw|ow|lh|sf)?$/i.test(trimmed);\n}\n\nfunction suggestDirectional(value: string): string | null {\n const tokens = value.trim().split(/\\s+/);\n if (tokens.length !== 4) return null;\n\n const nonZero = tokens\n .map((token, index) => ({ token, side: SIDES[index] }))\n .filter(({ token }) => !isZeroLike(token));\n\n if (nonZero.length !== 1) return null;\n\n const { token, side } = nonZero[0];\n return `${token} ${side}`;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-directional-shorthand',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Suggest Tasty directional shorthand instead of 4-value CSS box syntax with zero placeholders',\n },\n messages: {\n preferDirectionalShorthand:\n \"'{{property}}' value '{{raw}}' only sets one side. Use the directional shorthand '{{suggestion}}' (e.g. '1x bottom') instead.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkValue(\n property: string,\n value: string,\n node: TSESTree.Node,\n ): void {\n const suggestion = suggestDirectional(value);\n if (!suggestion) return;\n\n context.report({\n node,\n messageId: 'preferDirectionalShorthand',\n data: { property, raw: value, suggestion },\n });\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n if (!(key in DIRECTIONAL_MODIFIERS)) continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkValue(key, str, prop.value);\n continue;\n }\n\n if (prop.value.type === 'ObjectExpression') {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr) {\n checkValue(key, stateStr, stateProp.value);\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"prefer-directional-shorthand.js","names":[],"sources":["../../src/rules/prefer-directional-shorthand.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\nimport { DIRECTIONAL_MODIFIERS } from '../constants.js';\nimport { replaceStringValue } from '../fix-utils.js';\n\ntype MessageIds = 'preferDirectionalShorthand';\n\nconst SIDES = ['top', 'right', 'bottom', 'left'] as const;\n\nfunction isZeroLike(token: string): boolean {\n const trimmed = token.trim();\n if (trimmed === '0') return true;\n return /^0+(?:\\.0+)?(?:px|em|rem|%|x|r|cr|bw|ow|lh|sf)?$/i.test(trimmed);\n}\n\nfunction suggestDirectional(value: string): string | null {\n const tokens = value.trim().split(/\\s+/);\n if (tokens.length !== 4) return null;\n\n const nonZero = tokens\n .map((token, index) => ({ token, side: SIDES[index] }))\n .filter(({ token }) => !isZeroLike(token));\n\n if (nonZero.length !== 1) return null;\n\n const { token, side } = nonZero[0];\n return `${token} ${side}`;\n}\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-directional-shorthand',\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description:\n 'Suggest Tasty directional shorthand instead of 4-value CSS box syntax with zero placeholders',\n },\n messages: {\n preferDirectionalShorthand:\n \"'{{property}}' value '{{raw}}' only sets one side. Use the directional shorthand '{{suggestion}}' (e.g. '1x bottom') instead.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function checkValue(\n property: string,\n value: string,\n node: TSESTree.Node,\n ): void {\n const suggestion = suggestDirectional(value);\n if (!suggestion) return;\n\n context.report({\n node,\n messageId: 'preferDirectionalShorthand',\n data: { property, raw: value, suggestion },\n fix(fixer) {\n return replaceStringValue(fixer, node, suggestion);\n },\n });\n }\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key === null) continue;\n if (!(key in DIRECTIONAL_MODIFIERS)) continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n checkValue(key, str, prop.value);\n continue;\n }\n\n if (prop.value.type === 'ObjectExpression') {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr) {\n checkValue(key, stateStr, stateProp.value);\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;;;AASA,MAAM,QAAQ;CAAC;CAAO;CAAS;CAAU;CAAO;AAEhD,SAAS,WAAW,OAAwB;CAC1C,MAAM,UAAU,MAAM,MAAM;AAC5B,KAAI,YAAY,IAAK,QAAO;AAC5B,QAAO,oDAAoD,KAAK,QAAQ;;AAG1E,SAAS,mBAAmB,OAA8B;CACxD,MAAM,SAAS,MAAM,MAAM,CAAC,MAAM,MAAM;AACxC,KAAI,OAAO,WAAW,EAAG,QAAO;CAEhC,MAAM,UAAU,OACb,KAAK,OAAO,WAAW;EAAE;EAAO,MAAM,MAAM;EAAQ,EAAE,CACtD,QAAQ,EAAE,YAAY,CAAC,WAAW,MAAM,CAAC;AAE5C,KAAI,QAAQ,WAAW,EAAG,QAAO;CAEjC,MAAM,EAAE,OAAO,SAAS,QAAQ;AAChC,QAAO,GAAG,MAAM,GAAG;;AAGrB,2CAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aACE,gGACH;EACD,UAAU,EACR,4BACE,iIACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,WACP,UACA,OACA,MACM;GACN,MAAM,aAAa,mBAAmB,MAAM;AAC5C,OAAI,CAAC,WAAY;AAEjB,WAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM;KAAE;KAAU,KAAK;KAAO;KAAY;IAC1C,IAAI,OAAO;AACT,YAAO,mBAAmB,OAAO,MAAM,WAAW;;IAErD,CAAC;;EAGJ,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;IAE/C,MAAM,MAAM,WAAW,KAAK,IAAI;AAChC,QAAI,QAAQ,KAAM;AAClB,QAAI,EAAE,OAAO,uBAAwB;IAErC,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,KAAK;AACP,gBAAW,KAAK,KAAK,KAAK,MAAM;AAChC;;AAGF,QAAI,KAAK,MAAM,SAAS,mBACtB,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,WAAW,eAAe,UAAU,MAAM;AAChD,SAAI,SACF,YAAW,KAAK,UAAU,UAAU,MAAM;;;;AAOpD,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|
|
@@ -3,10 +3,14 @@ import { getKeyName, getStringValue } from "../utils.js";
|
|
|
3
3
|
import { TastyContext, styleObjectListeners } from "../context.js";
|
|
4
4
|
|
|
5
5
|
//#region src/rules/prefer-hide.ts
|
|
6
|
+
function isNone(value) {
|
|
7
|
+
return value.trim().toLowerCase() === "none";
|
|
8
|
+
}
|
|
6
9
|
var prefer_hide_default = createRule({
|
|
7
10
|
name: "prefer-hide",
|
|
8
11
|
meta: {
|
|
9
12
|
type: "suggestion",
|
|
13
|
+
fixable: "code",
|
|
10
14
|
docs: { description: "Suggest hide: true instead of display: 'none' for Tasty's display/hide priority handling" },
|
|
11
15
|
messages: { preferHide: "Use 'hide: true' instead of \"display: 'none'\" so it composes with Tasty's display/hide priority handling." },
|
|
12
16
|
schema: []
|
|
@@ -14,13 +18,6 @@ var prefer_hide_default = createRule({
|
|
|
14
18
|
defaultOptions: [],
|
|
15
19
|
create(context) {
|
|
16
20
|
const ctx = new TastyContext(context);
|
|
17
|
-
function checkDisplayValue(value, node) {
|
|
18
|
-
if (value.trim().toLowerCase() !== "none") return;
|
|
19
|
-
context.report({
|
|
20
|
-
node,
|
|
21
|
-
messageId: "preferHide"
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
21
|
function handleStyleObject(node) {
|
|
25
22
|
if (!ctx.isStyleObject(node)) return;
|
|
26
23
|
for (const prop of node.properties) {
|
|
@@ -28,13 +25,22 @@ var prefer_hide_default = createRule({
|
|
|
28
25
|
if (getKeyName(prop.key) !== "display") continue;
|
|
29
26
|
const str = getStringValue(prop.value);
|
|
30
27
|
if (str) {
|
|
31
|
-
|
|
28
|
+
if (isNone(str)) context.report({
|
|
29
|
+
node: prop.value,
|
|
30
|
+
messageId: "preferHide",
|
|
31
|
+
fix(fixer) {
|
|
32
|
+
return [fixer.replaceText(prop.key, "hide"), fixer.replaceText(prop.value, "true")];
|
|
33
|
+
}
|
|
34
|
+
});
|
|
32
35
|
continue;
|
|
33
36
|
}
|
|
34
37
|
if (prop.value.type === "ObjectExpression") for (const stateProp of prop.value.properties) {
|
|
35
38
|
if (stateProp.type !== "Property") continue;
|
|
36
39
|
const stateStr = getStringValue(stateProp.value);
|
|
37
|
-
if (stateStr
|
|
40
|
+
if (stateStr && isNone(stateStr)) context.report({
|
|
41
|
+
node: stateProp.value,
|
|
42
|
+
messageId: "preferHide"
|
|
43
|
+
});
|
|
38
44
|
}
|
|
39
45
|
}
|
|
40
46
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-hide.js","names":[],"sources":["../../src/rules/prefer-hide.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\n\ntype MessageIds = 'preferHide';\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-hide',\n meta: {\n type: 'suggestion',\n docs: {\n description:\n \"Suggest hide: true instead of display: 'none' for Tasty's display/hide priority handling\",\n },\n messages: {\n preferHide:\n \"Use 'hide: true' instead of \\\"display: 'none'\\\" so it composes with Tasty's display/hide priority handling.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function
|
|
1
|
+
{"version":3,"file":"prefer-hide.js","names":[],"sources":["../../src/rules/prefer-hide.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { createRule } from '../create-rule.js';\nimport { TastyContext, styleObjectListeners } from '../context.js';\nimport { getKeyName, getStringValue } from '../utils.js';\n\ntype MessageIds = 'preferHide';\n\nfunction isNone(value: string): boolean {\n return value.trim().toLowerCase() === 'none';\n}\n\nexport default createRule<[], MessageIds>({\n name: 'prefer-hide',\n meta: {\n type: 'suggestion',\n fixable: 'code',\n docs: {\n description:\n \"Suggest hide: true instead of display: 'none' for Tasty's display/hide priority handling\",\n },\n messages: {\n preferHide:\n \"Use 'hide: true' instead of \\\"display: 'none'\\\" so it composes with Tasty's display/hide priority handling.\",\n },\n schema: [],\n },\n defaultOptions: [],\n create(context) {\n const ctx = new TastyContext(context);\n\n function handleStyleObject(node: TSESTree.ObjectExpression) {\n if (!ctx.isStyleObject(node)) return;\n\n for (const prop of node.properties) {\n if (prop.type !== 'Property' || prop.computed) continue;\n\n const key = getKeyName(prop.key);\n if (key !== 'display') continue;\n\n const str = getStringValue(prop.value);\n if (str) {\n if (isNone(str)) {\n context.report({\n node: prop.value,\n messageId: 'preferHide',\n fix(fixer) {\n return [\n fixer.replaceText(prop.key, 'hide'),\n fixer.replaceText(prop.value, 'true'),\n ];\n },\n });\n }\n continue;\n }\n\n if (prop.value.type === 'ObjectExpression') {\n for (const stateProp of prop.value.properties) {\n if (stateProp.type !== 'Property') continue;\n const stateStr = getStringValue(stateProp.value);\n if (stateStr && isNone(stateStr)) {\n context.report({\n node: stateProp.value,\n messageId: 'preferHide',\n });\n }\n }\n }\n }\n }\n\n return {\n ImportDeclaration(node) {\n ctx.trackImport(node);\n },\n ...styleObjectListeners(handleStyleObject),\n };\n },\n});\n"],"mappings":";;;;;AAOA,SAAS,OAAO,OAAwB;AACtC,QAAO,MAAM,MAAM,CAAC,aAAa,KAAK;;AAGxC,0BAAe,WAA2B;CACxC,MAAM;CACN,MAAM;EACJ,MAAM;EACN,SAAS;EACT,MAAM,EACJ,aACE,4FACH;EACD,UAAU,EACR,YACE,+GACH;EACD,QAAQ,EAAE;EACX;CACD,gBAAgB,EAAE;CAClB,OAAO,SAAS;EACd,MAAM,MAAM,IAAI,aAAa,QAAQ;EAErC,SAAS,kBAAkB,MAAiC;AAC1D,OAAI,CAAC,IAAI,cAAc,KAAK,CAAE;AAE9B,QAAK,MAAM,QAAQ,KAAK,YAAY;AAClC,QAAI,KAAK,SAAS,cAAc,KAAK,SAAU;AAG/C,QADY,WAAW,KAAK,IAAI,KACpB,UAAW;IAEvB,MAAM,MAAM,eAAe,KAAK,MAAM;AACtC,QAAI,KAAK;AACP,SAAI,OAAO,IAAI,CACb,SAAQ,OAAO;MACb,MAAM,KAAK;MACX,WAAW;MACX,IAAI,OAAO;AACT,cAAO,CACL,MAAM,YAAY,KAAK,KAAK,OAAO,EACnC,MAAM,YAAY,KAAK,OAAO,OAAO,CACtC;;MAEJ,CAAC;AAEJ;;AAGF,QAAI,KAAK,MAAM,SAAS,mBACtB,MAAK,MAAM,aAAa,KAAK,MAAM,YAAY;AAC7C,SAAI,UAAU,SAAS,WAAY;KACnC,MAAM,WAAW,eAAe,UAAU,MAAM;AAChD,SAAI,YAAY,OAAO,SAAS,CAC9B,SAAQ,OAAO;MACb,MAAM,UAAU;MAChB,WAAW;MACZ,CAAC;;;;AAOZ,SAAO;GACL,kBAAkB,MAAM;AACtB,QAAI,YAAY,KAAK;;GAEvB,GAAG,qBAAqB,kBAAkB;GAC3C;;CAEJ,CAAC"}
|