Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import stylelint from "stylelint";
import valueParser from "postcss-value-parser";
import {
namespace,
createTokenNamesArray,
isValidTokenUsage,
getLocalCustomProperties,
usesRawFallbackValues,
usesRawShorthandValues,
createAllowList,
} from "../helpers.mjs";
const {
utils: { report, ruleMessages, validateOptions },
} = stylelint;
const ruleName = namespace("use-space-tokens");
const messages = ruleMessages(ruleName, {
rejected: (value, suggestedValue) => {
if (suggestedValue != null) {
return `${value} should be using a space design token. Suggested value: ${suggestedValue}. This may be fixable by running the same command again with --fix.`;
}
return `${value} should be using a space design token.`;
},
});
const meta = {
fixable: true,
};
const INCLUDE_CATEGORIES = ["space"];
const tokenCSS = createTokenNamesArray(INCLUDE_CATEGORIES);
// Allowed values in CSS
const ALLOW_LIST = createAllowList(["0", "auto"]);
const CSS_PROPERTIES = [
"margin",
"margin-block",
"margin-block-end",
"margin-block-start",
"margin-inline",
"margin-inline-end",
"margin-inline-start",
"margin-top",
"margin-right",
"margin-bottom",
"margin-left",
"padding",
"padding-block",
"padding-block-end",
"padding-block-start",
"padding-inline",
"padding-inline-end",
"padding-inline-start",
"padding-top",
"padding-right",
"padding-bottom",
"padding-left",
"gap",
"column-gap",
"row-gap",
"inset",
"inset-block",
"inset-block-end",
"inset-block-start",
"inset-inline",
"inset-inline-end",
"inset-inline-start",
"top",
"right",
"bottom",
"left",
];
// the token tree has values that don't make sense to auto-fix, like changing 0 to var(--button-padding-icon),
// so we'll ignore those and stick to auto-fixable values that are likely to be used
const RAW_VALUE_TO_TOKEN_VALUE = {
"2px": "var(--space-xxsmall)",
"4px": "var(--space-xsmall)",
"8px": "var(--space-small)",
"12px": "var(--space-medium)",
"16px": "var(--space-large)",
"24px": "var(--space-xlarge)",
"32px": "var(--space-xxlarge)",
};
const getFixedValue = currentValue => {
const val = valueParser(currentValue);
let hasFixes = false;
val.walk(node => {
if (node.type == "word") {
const token = RAW_VALUE_TO_TOKEN_VALUE[node.value.trim()];
if (token) {
hasFixes = true;
node.value = token;
}
}
});
if (hasFixes) {
return val.toString();
}
return null;
};
const ruleFunction = primaryOption => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primaryOption,
possible: [true],
});
if (!validOptions) {
return;
}
// Walk declarations once to generate a lookup table of variables.
const cssCustomProperties = getLocalCustomProperties(root);
// Walk declarations again to detect non-token values.
root.walkDecls(declarations => {
// If the property is not in our list to check, skip it.
if (!CSS_PROPERTIES.includes(declarations.prop)) {
return;
}
// Otherwise, see if we are using the tokens correctly
if (
isValidTokenUsage(
declarations.value,
tokenCSS,
cssCustomProperties,
ALLOW_LIST
) &&
!usesRawFallbackValues(declarations.value, RAW_VALUE_TO_TOKEN_VALUE) &&
!usesRawShorthandValues(
declarations.value,
tokenCSS,
cssCustomProperties,
ALLOW_LIST
)
) {
return;
}
const fixedValue = getFixedValue(declarations.value);
report({
message: messages.rejected(declarations.value, fixedValue),
node: declarations,
result,
ruleName,
fix: () => {
if (fixedValue != null) {
declarations.value = fixedValue;
}
},
});
});
};
};
ruleFunction.ruleName = ruleName;
ruleFunction.messages = messages;
ruleFunction.meta = meta;
export default ruleFunction;