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
*/
import stylelint from "stylelint";
import valueParser from "postcss-value-parser";
import {
getLocalCustomProperties,
namespace,
createTokenNamesArray,
isWord,
isVariableFunction,
} from "../helpers.mjs";
import {
BACKGROUND_COLOR,
BORDER_COLOR,
BORDER_RADIUS,
BORDER_WIDTH,
FONT_SIZE,
FONT_WEIGHT,
ICON_COLOR,
SIZE,
OPACITY,
SPACE,
TEXT_COLOR,
BOX_SHADOW,
} from "../data.mjs";
const {
utils: { report, ruleMessages, validateOptions },
} = stylelint;
const ruleName = namespace("no-non-semantic-token-usage");
const messages = ruleMessages(ruleName, {
rejected: token =>
`Unexpected usage of \`${token}\`. Design tokens should only be used with properties matching their semantic meaning.`,
});
const meta = {
fixable: false,
};
const backgroundColorTokens = createTokenNamesArray(
BACKGROUND_COLOR.CATEGORIES
);
const borderColorTokens = createTokenNamesArray(BORDER_COLOR.CATEGORIES);
const borderRadiusTokens = createTokenNamesArray(BORDER_RADIUS.CATEGORIES);
const borderWidthTokens = createTokenNamesArray(BORDER_WIDTH.CATEGORIES);
const fontSizeTokens = createTokenNamesArray(FONT_SIZE.CATEGORIES);
const fontWeightTokens = createTokenNamesArray(FONT_WEIGHT.CATEGORIES);
const iconColorTokens = createTokenNamesArray(ICON_COLOR.CATEGORIES);
const sizeTokens = createTokenNamesArray(SIZE.CATEGORIES);
const opacityTokens = createTokenNamesArray(OPACITY.CATEGORIES);
const spaceTokens = createTokenNamesArray(SPACE.CATEGORIES);
const textColorTokens = createTokenNamesArray(TEXT_COLOR.CATEGORIES);
const boxShadowTokens = createTokenNamesArray(BOX_SHADOW.CATEGORIES);
// Get allowed properties by token category
const getAllowedProps = token => {
let tokenProperties = null;
switch (true) {
case backgroundColorTokens.includes(token):
tokenProperties = BACKGROUND_COLOR.PROPERTIES;
break;
case borderColorTokens.includes(token):
tokenProperties = BORDER_COLOR.PROPERTIES;
break;
case borderRadiusTokens.includes(token):
tokenProperties = BORDER_RADIUS.PROPERTIES;
break;
case borderWidthTokens.includes(token):
tokenProperties = BORDER_WIDTH.PROPERTIES;
break;
case fontSizeTokens.includes(token):
tokenProperties = FONT_SIZE.PROPERTIES;
break;
case fontWeightTokens.includes(token):
tokenProperties = FONT_WEIGHT.PROPERTIES;
break;
case iconColorTokens.includes(token):
tokenProperties = ICON_COLOR.PROPERTIES;
break;
case sizeTokens.includes(token):
tokenProperties = SIZE.PROPERTIES;
break;
case opacityTokens.includes(token):
tokenProperties = OPACITY.PROPERTIES;
break;
case spaceTokens.includes(token):
tokenProperties = SPACE.PROPERTIES;
break;
case textColorTokens.includes(token):
tokenProperties = TEXT_COLOR.PROPERTIES;
break;
case boxShadowTokens.includes(token):
tokenProperties = BOX_SHADOW.PROPERTIES;
break;
default:
break;
}
return tokenProperties;
};
// Get all design tokens in CSS declaration value
const getAllTokensInValue = value => {
const parsedValue = valueParser(value).nodes;
const allTokens = parsedValue
.filter(node => isVariableFunction(node))
.map(functionNode => {
const variableNode = functionNode.nodes.find(
node => isWord(node) && node.value.startsWith("--")
);
return variableNode ? variableNode.value : null;
})
.filter(Boolean);
return allTokens;
};
const ruleFunction = primaryOption => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primaryOption,
possible: [true],
});
if (!validOptions) {
return;
}
const cssCustomProperties = getLocalCustomProperties(root);
root.walkDecls(declaration => {
const { prop, value } = declaration;
const tokens = getAllTokensInValue(value);
tokens.forEach(token => {
// If local CSS custom variable declaration, skip
if (prop in cssCustomProperties) {
return;
}
// `var(--token-name)` mirrors shape received from `createTokenNamesArray()`
let varifiedToken = `var(${token})`;
let allowedProps = null;
if (cssCustomProperties[token]) {
// `cssCustomProperties[token]` already in desired shape
varifiedToken = cssCustomProperties[token];
allowedProps = getAllowedProps(cssCustomProperties[token]);
} else {
allowedProps = getAllowedProps(varifiedToken);
}
if (allowedProps && !allowedProps.includes(prop)) {
report({
message: messages.rejected(varifiedToken),
node: declaration,
result,
ruleName,
});
}
});
});
};
};
ruleFunction.ruleName = ruleName;
ruleFunction.messages = messages;
ruleFunction.meta = meta;
export default ruleFunction;