@master/eslint-plugin-css 2.0.0-rc.75 → 2.0.0-rc.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -60,6 +60,8 @@ npm install -D @master/eslint-plugin-css
60
60
 
61
61
  The recommended config is also available through `@master/eslint-config-css`.
62
62
 
63
+ In Visual Studio Code, install the VS Code ESLint extension to surface these diagnostics and run sort-on-save. The Master CSS extension provides completion, hover, colors, highlighting, directive diagnostics, manifest diagnostics, and directive formatting; it does not duplicate class policy warnings by default.
64
+
63
65
  ## Usage
64
66
 
65
67
  Use ESLint flat configuration:
@@ -83,7 +85,7 @@ export default [
83
85
  parser: tsParser
84
86
  }
85
87
  },
86
- css,
88
+ ...css,
87
89
  {
88
90
  rules: {
89
91
  '@master/css/no-invalid-classes': ['error', {
@@ -154,6 +156,18 @@ export default [
154
156
 
155
157
  For example, this rule can fix `text-align:center` to `text-center`, `font:16px` to `font:md`, `margin:md` to `m:md`, `m:1rem|1.5rem` to `m:md|lg`, `fg:var(--color-red-60)` to `fg:red-60`, `w:md h:md` to `size:md`, `mt:md mb:md` to `my:md`, and `block@dark@sm` to `block@sm@dark`.
156
158
 
159
+ The same canonicalization applies to unquoted `@compose` class lists. In CSS, declaration-like classes are moved to native declarations and suffixes are moved to `@variant`, `@dark`, or `@light` blocks when the rewrite is safe.
160
+
161
+ Standalone CSS files are included by default when using `@master/eslint-config-css`:
162
+
163
+ ```js
164
+ import css from '@master/eslint-config-css'
165
+
166
+ export default [...css]
167
+ ```
168
+
169
+ Direct plugin users can add `css.configs.stylesheet` from `@master/eslint-plugin-css` alongside the recommended config.
170
+
157
171
  You can disable specific canonicalization families:
158
172
 
159
173
  ```js
@@ -167,7 +181,9 @@ export default [
167
181
  preferVariableReferences: true,
168
182
  preferMultiValueTokens: true,
169
183
  preferCompositionUtilities: true,
170
- preferConditionOrder: true
184
+ preferConditionOrder: true,
185
+ preferNativeDeclarationsInCompose: true,
186
+ preferVariantBlocksInCompose: true
171
187
  }]
172
188
  }
173
189
  }
@@ -0,0 +1,6 @@
1
+ import { Linter } from "eslint";
2
+
3
+ //#region src/configs/stylesheet.d.ts
4
+ declare const _default: Linter.Config;
5
+ //#endregion
6
+ export { _default as default };
@@ -0,0 +1,11 @@
1
+ import plugin from "../plugin.js";
2
+ import stylesheet_parser_default from "../stylesheet-parser.js";
3
+ //#region src/configs/stylesheet.ts
4
+ var stylesheet_default = {
5
+ files: ["**/*.{css,scss,less}"],
6
+ plugins: { "@master/css": plugin },
7
+ languageOptions: { parser: stylesheet_parser_default },
8
+ rules: { "@master/css/prefer-canonical-classes": "warn" }
9
+ };
10
+ //#endregion
11
+ export { stylesheet_default as default };
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@ declare const masterCSSPlugin: Omit<TSESLint.Linter.Plugin, 'configs'> & {
8
8
  legacy: unknown;
9
9
  base: Linter.Config;
10
10
  recommended: Linter.Config;
11
+ stylesheet: Linter.Config;
11
12
  };
12
13
  settings: typeof settings;
13
14
  };
package/dist/index.js CHANGED
@@ -3,13 +3,15 @@ import plugin from "./plugin.js";
3
3
  import base_default from "./configs/base.js";
4
4
  import recommended_default from "./configs/recommended.js";
5
5
  import legacy_default from "./configs/legacy.js";
6
+ import stylesheet_default from "./configs/stylesheet.js";
6
7
  //#region src/index.ts
7
8
  const masterCSSPlugin = {
8
9
  ...plugin,
9
10
  configs: {
10
11
  legacy: legacy_default,
11
12
  base: base_default,
12
- recommended: recommended_default
13
+ recommended: recommended_default,
14
+ stylesheet: stylesheet_default
13
15
  },
14
16
  settings
15
17
  };
@@ -1,7 +1,8 @@
1
1
  import create_rule_default from "../create-rule.js";
2
2
  import defineVisitors from "../utils/define-visitors.js";
3
3
  import resolveContext from "../utils/resolve-context.js";
4
- import { findClassConflicts, findPartialClassConflicts, removeClassNamesFromClassList, replaceClassNameInClassList } from "@master/css-lint";
4
+ import reportLintDiagnostics from "../utils/report-lint-diagnostics.js";
5
+ import { createConflictingClassesReport } from "@master/css-lint";
5
6
  //#region src/rules/no-conflicting-classes.ts
6
7
  var no_conflicting_classes_default = create_rule_default({
7
8
  name: "no-conflicting-classes",
@@ -21,54 +22,8 @@ var no_conflicting_classes_default = create_rule_default({
21
22
  return defineVisitors({
22
23
  context,
23
24
  settings
24
- }, (_, { raw, start, end, unescape, classNodes, classValues }) => {
25
- const conflicts = findClassConflicts(classValues, css);
26
- if (conflicts.length) {
27
- const classNamesToRemove = conflicts.map(({ className }) => className);
28
- const keptClassNames = [...new Set(conflicts.flatMap(({ conflicts }) => conflicts))];
29
- const formatClassName = (className) => `"${className}"`;
30
- const formatClassList = (classNames) => {
31
- if (classNames.length <= 2) return classNames.map(formatClassName).join(" and ");
32
- return `${classNames.slice(0, -1).map(formatClassName).join(", ")}, and ${formatClassName(classNames[classNames.length - 1])}`;
33
- };
34
- const fixedRaw = removeClassNamesFromClassList(raw, classNamesToRemove, { unescape });
35
- const firstClassNode = classNodes.find((node) => node.value === classNamesToRemove[0]);
36
- context.report({
37
- loc: firstClassNode.loc,
38
- messageId: "collisionClass",
39
- data: { message: `${formatClassList(classNamesToRemove)} ${classNamesToRemove.length === 1 ? "is" : "are"} overridden by ${formatClassList(keptClassNames)}.` },
40
- fix: function(fixer) {
41
- return fixer.replaceTextRange([start, end], fixedRaw);
42
- }
43
- });
44
- return;
45
- }
46
- const partialConflicts = findPartialClassConflicts(classValues, css);
47
- if (!partialConflicts.length) return;
48
- let fixedRaw = raw;
49
- for (const conflict of partialConflicts) fixedRaw = replaceClassNameInClassList(fixedRaw, conflict.className, conflict.replacement, { unescape });
50
- const classNodeQueues = /* @__PURE__ */ new Map();
51
- for (const classNode of classNodes) {
52
- const queue = classNodeQueues.get(classNode.value);
53
- if (queue) queue.push(classNode);
54
- else classNodeQueues.set(classNode.value, [classNode]);
55
- }
56
- for (const conflict of partialConflicts) {
57
- const classNode = classNodeQueues.get(conflict.className)?.shift();
58
- if (!classNode) continue;
59
- context.report({
60
- loc: classNode.loc,
61
- messageId: "partialCollisionClass",
62
- data: {
63
- actual: conflict.className,
64
- replacement: conflict.replacement,
65
- conflict: conflict.conflict
66
- },
67
- fix(fixer) {
68
- return fixer.replaceTextRange([start, end], fixedRaw);
69
- }
70
- });
71
- }
25
+ }, (node, resolved) => {
26
+ reportLintDiagnostics(context, node, resolved, createConflictingClassesReport(resolved.raw, css, { unescape: resolved.unescape }).diagnostics);
72
27
  });
73
28
  }
74
29
  });
@@ -1,8 +1,9 @@
1
1
  import create_rule_default from "../create-rule.js";
2
2
  import defineVisitors from "../utils/define-visitors.js";
3
3
  import resolveContext from "../utils/resolve-context.js";
4
+ import reportLintDiagnostics from "../utils/report-lint-diagnostics.js";
4
5
  import { noInvalidClassesOptionsSchema } from "../settings-schema.js";
5
- import { getClassValidationIssues } from "@master/css-lint";
6
+ import { createInvalidClassesReport } from "@master/css-lint";
6
7
  //#region src/rules/no-invalid-classes.ts
7
8
  var no_invalid_classes_default = create_rule_default({
8
9
  name: "no-invalid-classes",
@@ -22,18 +23,11 @@ var no_invalid_classes_default = create_rule_default({
22
23
  return defineVisitors({
23
24
  context,
24
25
  settings
25
- }, (_, { classNodes }) => {
26
- for (const node of classNodes) {
27
- const issues = getClassValidationIssues(node.value, css, {
28
- disallowUnknownClass: options.disallowUnknownClass,
29
- displayClassName: node.raw
30
- });
31
- for (const issue of issues) context.report({
32
- loc: node.loc,
33
- messageId: issue.kind === "invalid" ? "invalidClass" : "disallowUnknownClass",
34
- data: { message: issue.message }
35
- });
36
- }
26
+ }, (node, resolved) => {
27
+ reportLintDiagnostics(context, node, resolved, createInvalidClassesReport(resolved.raw, css, {
28
+ unescape: resolved.unescape,
29
+ disallowUnknownClass: options.disallowUnknownClass
30
+ }).diagnostics);
37
31
  });
38
32
  }
39
33
  });
@@ -1,7 +1,8 @@
1
1
  import create_rule_default from "../create-rule.js";
2
2
  import defineVisitors from "../utils/define-visitors.js";
3
3
  import resolveContext from "../utils/resolve-context.js";
4
- import { findUnapprovedRawValueClasses } from "@master/css-lint";
4
+ import reportLintDiagnostics from "../utils/report-lint-diagnostics.js";
5
+ import { createUnapprovedRawValueClassesReport } from "@master/css-lint";
5
6
  //#region src/rules/no-unapproved-raw-values.ts
6
7
  var no_unapproved_raw_values_default = create_rule_default({
7
8
  name: "no-unapproved-raw-values",
@@ -39,21 +40,11 @@ var no_unapproved_raw_values_default = create_rule_default({
39
40
  return defineVisitors({
40
41
  context,
41
42
  settings
42
- }, (node, { classNodes, classValues }) => {
43
- const issues = findUnapprovedRawValueClasses(classValues, css, options);
44
- for (const issue of issues) {
45
- const classNode = classNodes.find((node) => node.value === issue.className);
46
- if (!classNode) continue;
47
- context.report({
48
- node,
49
- loc: classNode.loc,
50
- messageId: "unapprovedRawValue",
51
- data: {
52
- className: issue.className,
53
- value: issue.value
54
- }
55
- });
56
- }
43
+ }, (node, resolved) => {
44
+ reportLintDiagnostics(context, node, resolved, createUnapprovedRawValueClassesReport(resolved.raw, css, {
45
+ ...options,
46
+ unescape: resolved.unescape
47
+ }).diagnostics);
57
48
  });
58
49
  }
59
50
  });
@@ -1,7 +1,9 @@
1
1
  import create_rule_default from "../create-rule.js";
2
2
  import defineVisitors from "../utils/define-visitors.js";
3
3
  import resolveContext from "../utils/resolve-context.js";
4
- import { defaultCanonicalClassNameOptions, replaceClassGroupInClassList, replaceClassNameInClassList, suggestCanonicalClassGroups, suggestCanonicalClassName } from "@master/css-lint";
4
+ import reportLintDiagnostics from "../utils/report-lint-diagnostics.js";
5
+ import resolveComposeDirectiveClassNodes from "../utils/resolve-compose-directive-class-nodes.js";
6
+ import { createCanonicalClassesReport, createCanonicalComposeDirectiveReport, defaultCanonicalClassNameOptions } from "@master/css-lint";
5
7
  //#region src/rules/prefer-canonical-classes.ts
6
8
  var prefer_canonical_classes_default = create_rule_default({
7
9
  name: "prefer-canonical-classes",
@@ -19,7 +21,9 @@ var prefer_canonical_classes_default = create_rule_default({
19
21
  preferVariableReferences: { type: "boolean" },
20
22
  preferMultiValueTokens: { type: "boolean" },
21
23
  preferCompositionUtilities: { type: "boolean" },
22
- preferConditionOrder: { type: "boolean" }
24
+ preferConditionOrder: { type: "boolean" },
25
+ preferNativeDeclarationsInCompose: { type: "boolean" },
26
+ preferVariantBlocksInCompose: { type: "boolean" }
23
27
  },
24
28
  additionalProperties: false
25
29
  }]
@@ -31,49 +35,54 @@ var prefer_canonical_classes_default = create_rule_default({
31
35
  ...defaultCanonicalClassNameOptions,
32
36
  ...context.options[0] || {}
33
37
  };
34
- return defineVisitors({
38
+ const reportCanonicalClassList = (node, { raw, start, end, unescape, classNodes, classValues }) => {
39
+ reportLintDiagnostics(context, node, {
40
+ raw,
41
+ start,
42
+ end,
43
+ unescape,
44
+ classNodes,
45
+ classValues,
46
+ nodes: [],
47
+ value: raw
48
+ }, createCanonicalClassesReport(raw, css, {
49
+ ...options,
50
+ unescape
51
+ }).diagnostics);
52
+ };
53
+ const reportCanonicalComposeDirective = (node, classNode) => {
54
+ reportLintDiagnostics(context, node, classNode, createCanonicalComposeDirectiveReport(classNode.raw, css, options).diagnostics, { getFix(_, fix) {
55
+ if (fix.scope !== "directive") return {
56
+ range: [classNode.start + fix.range.start, classNode.start + fix.range.end],
57
+ text: fix.text
58
+ };
59
+ return {
60
+ range: [classNode.directiveStart, classNode.directiveEnd],
61
+ text: formatComposeDirectiveReplacement(context.sourceCode.getText(), classNode.directiveStart, fix.text)
62
+ };
63
+ } });
64
+ };
65
+ const visitors = defineVisitors({
35
66
  context,
36
67
  settings
37
- }, (node, { raw, start, end, unescape, classNodes, classValues }) => {
38
- const groupSuggestions = suggestCanonicalClassGroups(classValues, css, options);
39
- const coveredClassNames = new Set(groupSuggestions.flatMap((suggestion) => suggestion.classNames));
40
- const reports = [];
41
- let fixedRaw = raw;
42
- for (const suggestion of groupSuggestions) {
43
- const firstClassNode = classNodes.find((node) => node.value === suggestion.classNames[0]);
44
- if (!firstClassNode) continue;
45
- fixedRaw = replaceClassGroupInClassList(fixedRaw, suggestion.classNames, suggestion.recommended, { unescape });
46
- reports.push({
47
- loc: firstClassNode.loc,
48
- actual: suggestion.classNames.join(" "),
49
- recommended: suggestion.recommended
50
- });
68
+ }, reportCanonicalClassList);
69
+ const visitProgram = visitors.Program;
70
+ return {
71
+ ...visitors,
72
+ Program(node) {
73
+ if (typeof visitProgram === "function") visitProgram(node);
74
+ for (const classNode of resolveComposeDirectiveClassNodes(context)) reportCanonicalComposeDirective(node, classNode);
51
75
  }
52
- for (const classNode of classNodes) {
53
- if (coveredClassNames.has(classNode.value)) continue;
54
- const recommended = suggestCanonicalClassName(classNode.value, css, options);
55
- if (!recommended) continue;
56
- fixedRaw = replaceClassNameInClassList(fixedRaw, classNode.value, recommended, { unescape });
57
- reports.push({
58
- loc: classNode.loc,
59
- actual: classNode.value,
60
- recommended
61
- });
62
- }
63
- for (const report of reports) context.report({
64
- node,
65
- loc: report.loc,
66
- messageId: "preferClass",
67
- data: {
68
- actual: report.actual,
69
- recommended: report.recommended
70
- },
71
- fix(fixer) {
72
- return fixer.replaceTextRange([start, end], fixedRaw);
73
- }
74
- });
75
- });
76
+ };
76
77
  }
77
78
  });
79
+ function getLinePrefix(source, index) {
80
+ return source.slice(source.lastIndexOf("\n", index - 1) + 1, index);
81
+ }
82
+ function formatComposeDirectiveReplacement(source, index, replacement) {
83
+ const linePrefix = getLinePrefix(source, index);
84
+ if (/\S/.test(linePrefix)) return replacement.replace(/\n\s*/g, " ");
85
+ return replacement.replace(/\n/g, `\n${linePrefix}`);
86
+ }
78
87
  //#endregion
79
88
  export { prefer_canonical_classes_default as default };
@@ -1,7 +1,8 @@
1
1
  import create_rule_default from "../create-rule.js";
2
2
  import defineVisitors from "../utils/define-visitors.js";
3
3
  import resolveContext from "../utils/resolve-context.js";
4
- import { sortClassList } from "@master/css-lint";
4
+ import reportLintDiagnostics from "../utils/report-lint-diagnostics.js";
5
+ import { createSortClassesReport } from "@master/css-lint";
5
6
  //#region src/rules/sort-classes.ts
6
7
  var sort_classes_default = create_rule_default({
7
8
  name: "sort-classes",
@@ -15,24 +16,21 @@ var sort_classes_default = create_rule_default({
15
16
  defaultOptions: [],
16
17
  create: function(context) {
17
18
  const { settings, css } = resolveContext(context);
18
- const { sourceCode } = context;
19
19
  return defineVisitors({
20
20
  context,
21
21
  settings
22
22
  }, (node, { raw, start, end, nodes, unescape }) => {
23
23
  if (nodes.length <= 1) return;
24
- const orderedRaw = sortClassList(raw, css, { unescape });
25
- if (raw !== orderedRaw) context.report({
26
- node,
27
- loc: {
28
- start: sourceCode.getLocFromIndex(start),
29
- end: sourceCode.getLocFromIndex(end)
30
- },
31
- messageId: "invalidClassOrder",
32
- fix: function(fixer) {
33
- return fixer.replaceTextRange([start, end], orderedRaw);
34
- }
35
- });
24
+ reportLintDiagnostics(context, node, {
25
+ raw,
26
+ start,
27
+ end,
28
+ nodes,
29
+ unescape,
30
+ value: raw,
31
+ classNodes: [],
32
+ classValues: []
33
+ }, createSortClassesReport(raw, css, { unescape }).diagnostics);
36
34
  });
37
35
  }
38
36
  });
@@ -0,0 +1,31 @@
1
+ //#region src/stylesheet-parser.d.ts
2
+ declare const _default: {
3
+ meta: {
4
+ name: string;
5
+ };
6
+ parseForESLint(source: string): {
7
+ ast: {
8
+ type: string;
9
+ body: any[];
10
+ comments: any[];
11
+ tokens: any[];
12
+ sourceType: string;
13
+ range: number[];
14
+ loc: {
15
+ start: {
16
+ line: number;
17
+ column: number;
18
+ };
19
+ end: {
20
+ line: number;
21
+ column: number;
22
+ };
23
+ };
24
+ };
25
+ visitorKeys: {
26
+ Program: any[];
27
+ };
28
+ };
29
+ };
30
+ //#endregion
31
+ export { _default as default };
@@ -0,0 +1,33 @@
1
+ //#region src/stylesheet-parser.ts
2
+ function getLocFromIndex(source, index) {
3
+ const lines = source.slice(0, index).split(/\r\n|[\n\r]/);
4
+ return {
5
+ line: lines.length,
6
+ column: lines[lines.length - 1].length
7
+ };
8
+ }
9
+ var stylesheet_parser_default = {
10
+ meta: { name: "@master/eslint-plugin-css/stylesheet-parser" },
11
+ parseForESLint(source) {
12
+ return {
13
+ ast: {
14
+ type: "Program",
15
+ body: [],
16
+ comments: [],
17
+ tokens: [],
18
+ sourceType: "module",
19
+ range: [0, source.length],
20
+ loc: {
21
+ start: {
22
+ line: 1,
23
+ column: 0
24
+ },
25
+ end: getLocFromIndex(source, source.length)
26
+ }
27
+ },
28
+ visitorKeys: { Program: [] }
29
+ };
30
+ }
31
+ };
32
+ //#endregion
33
+ export { stylesheet_parser_default as default };
@@ -0,0 +1,16 @@
1
+ import { TSESTree } from "@typescript-eslint/utils";
2
+ import { MasterCSSLintDiagnostic, MasterCSSLintFix } from "@master/css-lint";
3
+ import { RuleContext } from "@typescript-eslint/utils/ts-eslint";
4
+ import { ResolvedClassNode } from "./resolve-class-node.js";
5
+
6
+ //#region src/utils/report-lint-diagnostics.d.ts
7
+ type FixRange = [number, number];
8
+ interface ReportLintDiagnosticsOptions {
9
+ getFix?: (diagnostic: MasterCSSLintDiagnostic, fix: MasterCSSLintFix) => {
10
+ range: FixRange;
11
+ text: string;
12
+ } | undefined;
13
+ }
14
+ declare function reportLintDiagnostics(context: RuleContext<any, any[]>, node: TSESTree.Node, resolved: ResolvedClassNode, diagnostics: MasterCSSLintDiagnostic[], options?: ReportLintDiagnosticsOptions): void;
15
+ //#endregion
16
+ export { reportLintDiagnostics as default };
@@ -0,0 +1,46 @@
1
+ //#region src/utils/report-lint-diagnostics.ts
2
+ const messageIdByCode = {
3
+ "invalid-class-order": "invalidClassOrder",
4
+ "invalid-class": "invalidClass",
5
+ "unknown-class": "disallowUnknownClass",
6
+ "conflicting-class": "collisionClass",
7
+ "partially-conflicting-class": "partialCollisionClass",
8
+ "prefer-canonical-class": "preferClass",
9
+ "prefer-native-declaration": "preferClass",
10
+ "prefer-variant-block": "preferClass",
11
+ "unapproved-raw-value": "unapprovedRawValue"
12
+ };
13
+ function stringifyData(data = {}) {
14
+ return Object.fromEntries(Object.entries(data).filter(([, value]) => value !== void 0).map(([key, value]) => [key, Array.isArray(value) ? value.join(", ") : String(value)]));
15
+ }
16
+ function defaultFix(resolved, fix) {
17
+ if (fix.scope === "directive") return;
18
+ return {
19
+ range: [resolved.start + fix.range.start, resolved.start + fix.range.end],
20
+ text: fix.text
21
+ };
22
+ }
23
+ function reportLintDiagnostics(context, node, resolved, diagnostics, options = {}) {
24
+ const { sourceCode } = context;
25
+ for (const diagnostic of diagnostics) {
26
+ const start = resolved.start + diagnostic.range.start;
27
+ const end = resolved.start + diagnostic.range.end;
28
+ const messageId = messageIdByCode[diagnostic.code];
29
+ const fix = diagnostic.fix ? options.getFix?.(diagnostic, diagnostic.fix) || defaultFix(resolved, diagnostic.fix) : void 0;
30
+ const descriptor = {
31
+ node,
32
+ loc: {
33
+ start: sourceCode.getLocFromIndex(start),
34
+ end: sourceCode.getLocFromIndex(end)
35
+ },
36
+ ...messageId ? {
37
+ messageId,
38
+ data: stringifyData(diagnostic.data || { message: diagnostic.message })
39
+ } : { message: diagnostic.message }
40
+ };
41
+ if (fix) descriptor.fix = (fixer) => fixer.replaceTextRange(fix.range, fix.text);
42
+ context.report(descriptor);
43
+ }
44
+ }
45
+ //#endregion
46
+ export { reportLintDiagnostics as default };
@@ -0,0 +1,11 @@
1
+ import { RuleContext } from "@typescript-eslint/utils/ts-eslint";
2
+ import { ResolvedClassNode } from "./resolve-class-node.js";
3
+
4
+ //#region src/utils/resolve-compose-directive-class-nodes.d.ts
5
+ interface ResolvedComposeDirectiveClassNode extends ResolvedClassNode {
6
+ directiveStart: number;
7
+ directiveEnd: number;
8
+ }
9
+ declare function resolveComposeDirectiveClassNodes(context: RuleContext<any, any[]>): ResolvedComposeDirectiveClassNode[];
10
+ //#endregion
11
+ export { ResolvedComposeDirectiveClassNode, resolveComposeDirectiveClassNodes as default };
@@ -0,0 +1,157 @@
1
+ import { collectCSSDirectiveRanges, parseMasterCSSClassList } from "@master/css-lexer";
2
+ //#region src/utils/resolve-compose-directive-class-nodes.ts
3
+ const CSS_LIKE_FILE_RE = /\.(?:css|scss|sass|less|pcss|postcss)$/i;
4
+ const CSS_LINE_COMMENT_FILE_RE = /\.(?:scss|sass|less)$/i;
5
+ const STYLE_CONTAINER_FILE_RE = /\.(?:vue|svelte|astro|html|htm|mdx)$/i;
6
+ const STYLE_LINE_COMMENT_RE = /\blang\s*=\s*(?:"(?:scss|sass|less)"|'(?:scss|sass|less)'|(?:scss|sass|less)\b)/i;
7
+ function getFilename(context) {
8
+ return (context.filename || context.getFilename?.() || "").split(/[?#]/)[0];
9
+ }
10
+ function isWhitespace(char) {
11
+ return char !== void 0 && /\s/.test(char);
12
+ }
13
+ function findHTMLTagEnd(source, start) {
14
+ let quote = "";
15
+ for (let index = start; index < source.length; index++) {
16
+ const char = source[index];
17
+ if (quote) {
18
+ if (char === quote) quote = "";
19
+ continue;
20
+ }
21
+ if (char === "\"" || char === "'") {
22
+ quote = char;
23
+ continue;
24
+ }
25
+ if (char === ">") return index + 1;
26
+ }
27
+ return -1;
28
+ }
29
+ function collectStyleContentRanges(source) {
30
+ const ranges = [];
31
+ const openStylePattern = /<style\b/gi;
32
+ let match;
33
+ while (match = openStylePattern.exec(source)) {
34
+ const openStart = match.index;
35
+ const openEnd = findHTMLTagEnd(source, openStart);
36
+ if (openEnd === -1) break;
37
+ const openTag = source.slice(openStart, openEnd);
38
+ const closeMatch = /<\/style\s*>/i.exec(source.slice(openEnd));
39
+ const closeStart = closeMatch ? openEnd + closeMatch.index : source.length;
40
+ ranges.push({
41
+ start: openEnd,
42
+ end: closeStart,
43
+ lineComments: STYLE_LINE_COMMENT_RE.test(openTag)
44
+ });
45
+ openStylePattern.lastIndex = closeMatch ? closeStart + closeMatch[0].length : source.length;
46
+ }
47
+ return ranges;
48
+ }
49
+ function collectLineCommentRanges(source, start, end) {
50
+ const ranges = [];
51
+ let quote = "";
52
+ let blockComment = false;
53
+ for (let index = start; index < end; index++) {
54
+ const char = source[index];
55
+ const next = source[index + 1];
56
+ if (blockComment) {
57
+ if (char === "*" && next === "/") {
58
+ blockComment = false;
59
+ index++;
60
+ }
61
+ continue;
62
+ }
63
+ if (quote) {
64
+ if (char === "\\") index++;
65
+ else if (char === quote) quote = "";
66
+ continue;
67
+ }
68
+ if (char === "/" && next === "*") {
69
+ blockComment = true;
70
+ index++;
71
+ continue;
72
+ }
73
+ if (char === "\"" || char === "'") {
74
+ quote = char;
75
+ continue;
76
+ }
77
+ if (char === "/" && next === "/") {
78
+ const lineEnd = source.indexOf("\n", index + 2);
79
+ const commentEnd = lineEnd === -1 ? end : Math.min(lineEnd, end);
80
+ ranges.push({
81
+ start: index,
82
+ end: commentEnd
83
+ });
84
+ index = commentEnd;
85
+ }
86
+ }
87
+ return ranges;
88
+ }
89
+ function overlapsRange(start, end, ranges) {
90
+ return ranges.some((range) => start < range.end && end > range.start);
91
+ }
92
+ function collectCandidateRanges(source, filename) {
93
+ if (CSS_LIKE_FILE_RE.test(filename)) return [{
94
+ start: 0,
95
+ end: source.length,
96
+ lineComments: CSS_LINE_COMMENT_FILE_RE.test(filename)
97
+ }];
98
+ if (STYLE_CONTAINER_FILE_RE.test(filename)) return collectStyleContentRanges(source);
99
+ return [];
100
+ }
101
+ function resolveComposeDirectiveClassNode(source, sourceCode, start, end) {
102
+ while (start < end && isWhitespace(source[start])) start++;
103
+ while (end > start && isWhitespace(source[end - 1])) end--;
104
+ if (start >= end) return;
105
+ const raw = source.slice(start, end);
106
+ const nodes = parseMasterCSSClassList(raw, { preserveSpaces: true }).map((item) => {
107
+ const startOffset = start + item.start;
108
+ const endOffset = start + item.end;
109
+ return {
110
+ type: item.type,
111
+ value: item.token,
112
+ raw: item.raw,
113
+ range: [startOffset, endOffset],
114
+ loc: {
115
+ start: sourceCode.getLocFromIndex(startOffset),
116
+ end: sourceCode.getLocFromIndex(endOffset)
117
+ }
118
+ };
119
+ });
120
+ const classNodes = nodes.filter((node) => node.type === "class");
121
+ if (!classNodes.length) return;
122
+ return {
123
+ nodes,
124
+ start,
125
+ end,
126
+ raw,
127
+ value: raw,
128
+ unescape: false,
129
+ classNodes,
130
+ classValues: classNodes.map((node) => node.value)
131
+ };
132
+ }
133
+ function resolveComposeDirectiveClassNodes(context) {
134
+ const filename = getFilename(context);
135
+ const source = context.sourceCode.getText();
136
+ const nodes = [];
137
+ for (const range of collectCandidateRanges(source, filename)) {
138
+ const lineCommentRanges = range.lineComments ? collectLineCommentRanges(source, range.start, range.end) : [];
139
+ const rangeSource = source.slice(range.start, range.end);
140
+ for (const directive of collectCSSDirectiveRanges(rangeSource)) {
141
+ if (directive.name !== "compose") continue;
142
+ if (directive.blockRange || directive.quotedStringRanges.length) continue;
143
+ if (overlapsRange(range.start + directive.keywordRange.start, range.start + directive.keywordRange.end, lineCommentRanges)) continue;
144
+ const classNode = resolveComposeDirectiveClassNode(source, context.sourceCode, range.start + directive.preludeRange.start, range.start + directive.preludeRange.end);
145
+ if (!classNode) continue;
146
+ if (overlapsRange(classNode.start, classNode.end, lineCommentRanges)) continue;
147
+ nodes.push({
148
+ ...classNode,
149
+ directiveStart: range.start + directive.start,
150
+ directiveEnd: range.start + (directive.semicolonRange?.end ?? directive.end)
151
+ });
152
+ }
153
+ }
154
+ return nodes;
155
+ }
156
+ //#endregion
157
+ export { resolveComposeDirectiveClassNodes as default };
package/package.json CHANGED
@@ -47,12 +47,12 @@
47
47
  "dependencies": {
48
48
  "@typescript-eslint/utils": "^8.61.1",
49
49
  "typescript-eslint": "^8.61.1",
50
- "@master/css": "^2.0.0-rc.75",
51
- "@master/css-lexer": "^2.0.0-rc.75",
52
- "@master/css-lint": "^2.0.0-rc.75",
53
- "@master/css-project": "^2.0.0-rc.75",
54
- "@master/css-preset": "^2.0.0-rc.75",
55
- "@master/css-validator": "^2.0.0-rc.75"
50
+ "@master/css": "^2.0.0-rc.76",
51
+ "@master/css-lexer": "^2.0.0-rc.76",
52
+ "@master/css-lint": "^2.0.0-rc.76",
53
+ "@master/css-project": "^2.0.0-rc.76",
54
+ "@master/css-preset": "^2.0.0-rc.76",
55
+ "@master/css-validator": "^2.0.0-rc.76"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@angular-eslint/template-parser": "^21.4.0",
@@ -65,9 +65,9 @@
65
65
  "svelte-eslint-parser": "^1.8.0",
66
66
  "ts-node": "^10.9.2",
67
67
  "vue-eslint-parser": "^10.4.1",
68
- "@master/css-schema": "^2.0.0-rc.75"
68
+ "@master/css-schema": "^2.0.0-rc.76"
69
69
  },
70
- "version": "2.0.0-rc.75",
70
+ "version": "2.0.0-rc.76",
71
71
  "scripts": {
72
72
  "build": "tsdown --config ../../shared/tsdown-package.config.ts",
73
73
  "dev": "tsdown --config ../../shared/tsdown-package.config.ts --watch",