diff --git a/lib/rules/no-custom-classname.js b/lib/rules/no-custom-classname.js
index 97216be9..151c905f 100644
--- a/lib/rules/no-custom-classname.js
+++ b/lib/rules/no-custom-classname.js
@@ -177,6 +177,43 @@ module.exports = {
});
};
+ // @html-eslint/parser node (with proper Alpinejs handling)
+ const htmlElsintParserVisitor = function (node) {
+ const attrName = node.key?.value;
+ const rawValue = node.value?.value;
+
+ const ALPINE_MARKERS = new Set(['x-transition', 'x-cloak']);
+
+ // alpinejs attributes, to be ignored
+ if (!attrName || attrName.startsWith('x-')) return;
+ if (typeof rawValue !== 'string') return;
+
+ if (attrName === 'class') {
+ const classNames = rawValue
+ .split(/\s+/)
+ .filter((name) => name && !ALPINE_MARKERS.has(name));
+ parseForCustomClassNames(classNames, node);
+ }
+
+ if (attrName === ':class') {
+ // Only extract keys in object-style bindings: { 'foo bar': condition }
+ const matches = rawValue.match(/'([^']+?)'\s*:/g) || [];
+ const classNames = [];
+
+ for (const match of matches) {
+ const rawKey = match.replace(/['":]/g, '').trim();
+ rawKey.split(/\s+/).forEach((name) => {
+ if (name && !ALPINE_MARKERS.has(name)) {
+ classNames.push(name);
+ }
+ });
+ }
+
+ parseForCustomClassNames(classNames, node);
+ }
+ };
+
+
const scriptVisitor = {
JSXAttribute: attributeVisitor,
TextAttribute: attributeVisitor,
@@ -187,6 +224,8 @@ module.exports = {
}
astUtil.parseNodeRecursive(node, node.quasi, parseForCustomClassNames, false, false, ignoredKeys);
},
+ Attribute: htmlElsintParserVisitor,
+
};
const templateVisitor = {
diff --git a/package-lock.json b/package-lock.json
index 08c57dd4..d3522018 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
},
"devDependencies": {
"@angular-eslint/template-parser": "^15.2.0",
+ "@html-eslint/parser": "^0.37.0",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/line-clamp": "^0.4.2",
@@ -132,6 +133,22 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@html-eslint/parser": {
+ "version": "0.37.0",
+ "resolved": "https://registry.npmjs.org/@html-eslint/parser/-/parser-0.37.0.tgz",
+ "integrity": "sha512-shrBuyEsa8iilhRrUlArULkleVlsNZVPXVulejmq7DkxNj1bQyYVFRYHIkhg5GkbPZT6wrUeA2SLOJ2xanpYIA==",
+ "dev": true,
+ "dependencies": {
+ "@html-eslint/template-syntax-parser": "^0.37.0",
+ "es-html-parser": "0.1.1"
+ }
+ },
+ "node_modules/@html-eslint/template-syntax-parser": {
+ "version": "0.37.0",
+ "resolved": "https://registry.npmjs.org/@html-eslint/template-syntax-parser/-/template-syntax-parser-0.37.0.tgz",
+ "integrity": "sha512-xL/OVGJOJQ8G3akZFlQeADPMX9OTVrUwMaAGrwIewf4k7lxnpz8sDaFgpkA5X7AYzmhIeENrhaB/pIbC0KaSyQ==",
+ "dev": true
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -944,6 +961,12 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
+ "node_modules/es-html-parser": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/es-html-parser/-/es-html-parser-0.1.1.tgz",
+ "integrity": "sha512-SNHdEpKkN4nWZ3sFq9AxPlaUzPKJewGh59JrVS2355vELTOFygyf/lbfDDIONuGvYrhvAHoaUd+sK9UGaGrKUg==",
+ "dev": true
+ },
"node_modules/escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
@@ -2924,6 +2947,22 @@
"integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
"dev": true
},
+ "@html-eslint/parser": {
+ "version": "0.37.0",
+ "resolved": "https://registry.npmjs.org/@html-eslint/parser/-/parser-0.37.0.tgz",
+ "integrity": "sha512-shrBuyEsa8iilhRrUlArULkleVlsNZVPXVulejmq7DkxNj1bQyYVFRYHIkhg5GkbPZT6wrUeA2SLOJ2xanpYIA==",
+ "dev": true,
+ "requires": {
+ "@html-eslint/template-syntax-parser": "^0.37.0",
+ "es-html-parser": "0.1.1"
+ }
+ },
+ "@html-eslint/template-syntax-parser": {
+ "version": "0.37.0",
+ "resolved": "https://registry.npmjs.org/@html-eslint/template-syntax-parser/-/template-syntax-parser-0.37.0.tgz",
+ "integrity": "sha512-xL/OVGJOJQ8G3akZFlQeADPMX9OTVrUwMaAGrwIewf4k7lxnpz8sDaFgpkA5X7AYzmhIeENrhaB/pIbC0KaSyQ==",
+ "dev": true
+ },
"@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -3510,6 +3549,12 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
+ "es-html-parser": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/es-html-parser/-/es-html-parser-0.1.1.tgz",
+ "integrity": "sha512-SNHdEpKkN4nWZ3sFq9AxPlaUzPKJewGh59JrVS2355vELTOFygyf/lbfDDIONuGvYrhvAHoaUd+sK9UGaGrKUg==",
+ "dev": true
+ },
"escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
diff --git a/package.json b/package.json
index 85edb84e..b134da35 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,7 @@
},
"devDependencies": {
"@angular-eslint/template-parser": "^15.2.0",
+ "@html-eslint/parser": "^0.37.0",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/line-clamp": "^0.4.2",
diff --git a/tests/lib/rules/no-custom-classname.js b/tests/lib/rules/no-custom-classname.js
index 4259aad6..33b8f692 100644
--- a/tests/lib/rules/no-custom-classname.js
+++ b/tests/lib/rules/no-custom-classname.js
@@ -1123,7 +1123,28 @@ ruleTester.run("no-custom-classname", rule, {
code: `