Skip to content

Commit

Permalink
Arbitrary properties & attribute variants (#167)
Browse files Browse the repository at this point in the history
* chore: add failing valid test cases
* fix: prefix parsing when using attribute variants
* fix: #163 add support for contradicting arbitrary properties
* chore: npm audit fix
* chore: README
  • Loading branch information
francoismassart authored Sep 16, 2022
1 parent 2fd2012 commit 63b152b
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 77 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ If you enjoy my work you can:

## Latest changelog

- FIX: [prefix parsing when using attribute variants](https://github.com/francoismassart/eslint-plugin-tailwindcss/issues/164)
- FIX: [add support for contradicting arbitrary properties](https://github.com/francoismassart/eslint-plugin-tailwindcss/issues/163)
- FIX: [conflicting rules with ambiguous arbitrary values](https://github.com/francoismassart/eslint-plugin-tailwindcss/issues/152)(by [acewf](https://github.com/acewf) 🙏)
- FIX: [`parseNodeRecursive`: Correctly recurse into TemplateLiteral expressions](https://github.com/francoismassart/eslint-plugin-tailwindcss/pull/138) (by [mpsijm](https://github.com/mpsijm) 🙏)
- FIX: [Prevent rule crash on class/className attributes with no value](https://github.com/francoismassart/eslint-plugin-tailwindcss/issues/157) (by [threehams](https://github.com/threehams) 🙏)
Expand Down
65 changes: 42 additions & 23 deletions lib/rules/no-contradicting-classname.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,36 +86,55 @@ module.exports = {
}
});

classNames = sorted.filter((slot) => slot.length > 1);
// Only multiple classNames
const sortedGroups = sorted.filter((slot) => slot.length > 1);
const arbitraryPropsGroupIndex = sortedGroups.findIndex((slot) => {
const suffix = groupUtil.getSuffix(slot[0], mergedConfig.separator);
return groupUtil.getArbitraryProperty(suffix, mergedConfig.separator) !== '';
});

// Sorts each groups' classnames
const ambiguousArbitraryValues = String.raw`(\[(.*${mergedConfig.separator}))`
const ambiguousArbitraryValuesOrClasses = String.raw`(\[(.*${mergedConfig.separator}))|(^((?!:).)*$)`
const arbitraryRegex = new RegExp(ambiguousArbitraryValues);
const ambiguousArbitraryValuesOrClasses = String.raw`(\[(.*${mergedConfig.separator}))|(^((?!:).)*$)`;

classNames.forEach((slot) => {
sortedGroups.forEach((group, groupIndex) => {
const variants = [];
slot.forEach((cls) => {
const arbiratySeparator = arbitraryRegex.test(cls);
const start = arbiratySeparator? 0 : cls.lastIndexOf(mergedConfig.separator) + 1;
const prefix = cls.substr(0, start);
const name = cls.substr(start);
const rePrefix = prefix === '' ? ambiguousArbitraryValuesOrClasses : '^' + prefix;
const idx = variants.findIndex((v) => v.prefix === rePrefix);
if (idx === -1) {
variants.push({
prefix: rePrefix,
name: [name],
});
group.forEach((cls) => {
const prefix = groupUtil.getPrefix(cls, mergedConfig.separator);
const name = cls.substr(prefix.length);
if (groupIndex === arbitraryPropsGroupIndex) {
// Arbitrary Props
const arbitraryProp = groupUtil.getArbitraryProperty(name, mergedConfig.separator);
const identifier = prefix + arbitraryProp;
const idx = variants.findIndex((v) => identifier === v.prefix);
if (idx === -1) {
variants.push({
prefix: identifier,
name: [name],
});
} else {
variants[idx].name.push(name);
}
} else {
variants[idx].name.push(name);
// "Regular classNames"
const rePrefix = prefix === '' ? ambiguousArbitraryValuesOrClasses : '^' + prefix;
const idx = variants.findIndex((v) => v.prefix === rePrefix);
if (idx === -1) {
variants.push({
prefix: rePrefix,
name: [name],
});
} else {
variants[idx].name.push(name);
}
}
});
const troubles = variants.filter((v) => v.name.length > 1);
if (troubles.length) {
troubles.forEach((issue) => {
const re = new RegExp(issue.prefix);
const conflicting = slot.filter((c) => re.test(c));

// Several classNames with the same prefix
const potentialTroubles = variants.filter((v) => v.name.length > 1);
if (potentialTroubles.length) {
potentialTroubles.forEach((variantGroup) => {
const re = new RegExp(variantGroup.prefix);
const conflicting = group.filter((c) => re.test(c));
context.report({
node: node,
messageId: 'conflictingClassnames',
Expand Down
64 changes: 40 additions & 24 deletions lib/util/groupMethods.js
Original file line number Diff line number Diff line change
Expand Up @@ -424,38 +424,54 @@ function getGroupIndex(name, arr, separator = ':') {
}

/**
* Get the prefixes of the full classname
* Returns the prefix (variants) of a className including the separator or an empty string if none
*
* @param {String} className The target classname
* @param {String} separator The delimiter to be used between variants
* @returns {String} The variants
* @param {String} name Classname to be parsed
* @param {String} separator The separator character as in config
* @returns {String} The prefix
*/
function getPrefix(name, separator) {
const rootSeparator = String.raw`(?<!\[[a-z0-9\-]*)(${separator})(?![a-z0-9\-]*\])`;
const rootSeparatorRegex = new RegExp(rootSeparator);
let classname = name;
let index = 0;
let results;
while ((results = rootSeparatorRegex.exec(classname)) !== null) {
const newIndex = results.index + separator.length;
index += newIndex;
classname = classname.substring(newIndex);
}

return index ? name.substring(0, index) : '';
}

/**
* Returns the arbitrary property of className without the separator or an empty string if none
* e.g. "[mask-type:luminance]" => "mask-type"
*
* @see https://tailwindcss.com/docs/adding-custom-styles#arbitrary-properties
* @param {String} name Classname suffix (without it variants) to be parsed
* @param {String} separator The separator character as in config
* @returns {String} The arbitrary property
*/
function getPrefixes(className, separator = ':') {
return className.split(separator).slice(0, -1).join(separator);
function getArbitraryProperty(name, separator) {
const arbitraryPropPattern = String.raw`^\[([a-z\-]*)${separator}\.*`;
const arbitraryPropRegExp = new RegExp(arbitraryPropPattern);
const results = arbitraryPropRegExp.exec(name);
return results === null ? '' : results[1];
}

/**
* Get the last part of the full classname
* e.g. "lg:w-[100px]" => "w-[100px]"
*
* @param {String} className The target classname
* @param {String} separator The delimiter to be used between variants
* @returns {String} The classname without its variants
*/
function getSuffix(className, separator = ':') {
const arbitraryPropertiesRe = new RegExp(
`^\\!?([a-z]*${escapeSpecialChars(separator)})*(?<value>\\[(.{1,})\\])$`,
'i'
);
let res = arbitraryPropertiesRe.exec(className);
if (res && res.groups && res.groups.value) {
// arbitraryProperties detected !
return res.groups.value;
}
const arbitraryArr = className.split('-[');
if (arbitraryArr.length === 1) {
return className.split(separator).pop();
}
return arbitraryArr[0].split(separator).pop() + '-[' + arbitraryArr[1];
const prefix = getPrefix(className, separator);
return className.substring(prefix.length);
}

/**
Expand Down Expand Up @@ -524,7 +540,7 @@ function parseClassname(name, arr, config, index = null) {
trailing = trailingRes.groups.trailing || '';
}
core = name.substring(leading.length, name.length - trailing.length);
const classPrefixes = getPrefixes(core, config.separator);
const variants = getPrefix(core, config.separator);
const classSuffix = getSuffix(core, config.separator);
let slot = null;
arr.forEach((group) => {
Expand All @@ -535,7 +551,6 @@ function parseClassname(name, arr, config, index = null) {
}
}
});
const variants = classPrefixes ? classPrefixes + config.separator : '';
const value = slot ? slot.value : '';
const isNegative = value[0] === '-';
const off = isNegative ? 1 : 0;
Expand All @@ -547,7 +562,7 @@ function parseClassname(name, arr, config, index = null) {
parentType: slot ? slot.group : '',
body: body,
value: value,
shorthand: slot ? slot.shorthand : '',
shorthand: slot && slot.shorthand ? slot.shorthand : '',
leading: leading,
trailing: trailing,
important: body.substr(0, 1) === '!',
Expand All @@ -556,9 +571,10 @@ function parseClassname(name, arr, config, index = null) {

module.exports = {
initGroupSlots,
getArbitraryProperty,
getGroups,
getGroupIndex,
getPrefixes,
getPrefix,
getSuffix,
parseClassname,
};
60 changes: 30 additions & 30 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions tests/lib/rules/no-contradicting-classname.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,37 @@ ruleTester.run("no-contradicting-classname", rule, {
{
code: "<div class>No errors while typing</div>",
},
{
code: '<div class="opacity-100 [&[aria-hidden=true]]:opacity-0">No errors while typing</div>',
},
{
code: `
<div
className={clsx(
"[clip-path:circle(100%_at_center)]",
"[mask-image:radial-gradient(circle_at_center,transparent_51%,black_54.8%)]",
"bg-[conic-gradient(transparent,#9e9ab1)]",
"pointer-events-none h-6 w-6 animate-spin rounded-full"
)}
/>`,
},
{
code: `
<div
className={ctl(\`
[clip-path:circle(100%_at_center)]
[mask-image:radial-gradient(circle_at_center,transparent_51%,black_54.8%)]
lg:[mask-image:radial-gradient(circle_at_center,transparent_10%,black_10%)]
bg-[conic-gradient(transparent,#ff00ff)]
lg:bg-[conic-gradient(transparent,#9e9ab1)]
pointer-events-none
h-6
w-6
animate-spin
rounded-full
\`)}
/>`,
},
],

invalid: [
Expand Down Expand Up @@ -543,6 +574,19 @@ ruleTester.run("no-contradicting-classname", rule, {
</div>`,
errors: generateErrors("border-spacing-y-px border-spacing-y-0"),
},
{
code: `
<div
className={ctl(\`
[clip-path:circle(90%_at_center)]
[clip-path:circle(100%_at_center)]
[mask-image:radial-gradient(circle_at_center,transparent_10%,black_10%)]
bg-[conic-gradient(transparent,#ff00ff)]
lg:bg-[conic-gradient(transparent,#9e9ab1)]
\`)}
/>`,
errors: generateErrors("[clip-path:circle(90%_at_center)] [clip-path:circle(100%_at_center)]"),
},
// {
// code: `
// <div class="scale-75 transform-none">
Expand Down
Loading

0 comments on commit 63b152b

Please sign in to comment.