204 lines
5.4 KiB
JavaScript
204 lines
5.4 KiB
JavaScript
'use strict';
|
|
|
|
const stylelint = require('stylelint');
|
|
const valueParser = require('postcss-value-parser');
|
|
|
|
/** plugin name - prefixes all rules as per stylelint requirements */
|
|
const PLUGIN_NAME = 'rem-over-px';
|
|
|
|
/** rule name for rem-over-px */
|
|
const ruleName = `${PLUGIN_NAME}/rem-over-px`;
|
|
|
|
/** rule messages */
|
|
const messages = stylelint.utils.ruleMessages(ruleName, {
|
|
/** Report message for prefer rem over px */
|
|
remOverPx(val = '') {
|
|
return `Expected px unit in "${val}" to be rem.`;
|
|
},
|
|
});
|
|
|
|
/** default secondary options */
|
|
const defaultSecondaryOptions = {
|
|
/** properties to ignore */
|
|
ignore: ['1px'],
|
|
/** functions to ignore */
|
|
ignoreFunctions: ['url'],
|
|
/** @ rules to ignore */
|
|
ignoreAtRules: ['media'],
|
|
/** Base font size - used by autofix to convert px to rem */
|
|
fontSize: 16,
|
|
};
|
|
|
|
/** Regex to match pixels declarations in a string */
|
|
// eslint-disable-next-line
|
|
const regexPX = new RegExp(/(\d+\.?\d*)px/, 'g');
|
|
|
|
/** Converts a string with px units to rem */
|
|
const _pxToRem = (CSSString = '', fontSize = defaultSecondaryOptions.fontSize || 16) =>
|
|
CSSString.replace(regexPX, (match, n) => `${n / fontSize}rem`);
|
|
|
|
/** checks if prop is in ignore list */
|
|
const _propInIgnoreList = (prop, list) =>
|
|
prop && list.some((item) => prop.indexOf(item) > -1);
|
|
|
|
/** checks if prop is in ignore list with px */
|
|
const _propAddXpxInIgnoreList = (prop, list, px) => {
|
|
const reg = new RegExp(`\\s${px}`);
|
|
|
|
return (
|
|
prop &&
|
|
list.some(
|
|
(item) => reg.test(item) && prop.indexOf(item.replace(reg, '')) > -1,
|
|
)
|
|
);
|
|
};
|
|
|
|
/**
|
|
* check if a value has forbidden `px`
|
|
*/
|
|
const _hasForbiddenPX = (node, options) => {
|
|
const { type } = node;
|
|
/** value to check */
|
|
const value = type === 'decl' ? node.value : node.params;
|
|
/** prop to check */
|
|
const prop = type === 'decl' ? node.prop : null;
|
|
|
|
/** parsed value */
|
|
const parsed = valueParser(value);
|
|
/* parse secondaryOptions */
|
|
const {
|
|
ignore = defaultSecondaryOptions.ignore,
|
|
ignoreFunctions = defaultSecondaryOptions.ignoreFunctions,
|
|
ignoreAtRules = defaultSecondaryOptions.ignoreAtRules,
|
|
} = options;
|
|
|
|
/** Whether we matched px declarations */
|
|
let hasPX = false;
|
|
|
|
/** early exit and ignore */
|
|
if (
|
|
/* ignore atRules */
|
|
(type === 'atrule' && ignoreAtRules.indexOf(node.name) !== -1) ||
|
|
/* ignore declarations that are children of atrules - eg: keyframes */
|
|
(type === 'decl' && node?.parent?.parent?.type === 'atrule' && ignoreAtRules.indexOf(node?.parent?.parent?.name) !== -1) ||
|
|
/* ignore declarations ignored by props */
|
|
(type === 'decl' && _propInIgnoreList(node.prop, ignore))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
/** Walk through the parsed tree and match - return boolean, true if we matched an issue */
|
|
parsed.walk((currNode) => {
|
|
// if currNode is `url(xxx)`, prevent the traversal
|
|
if (
|
|
currNode.type === 'function' &&
|
|
ignoreFunctions.indexOf(currNode.value) !== -1
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
/** whether a px value was matched */
|
|
let matched;
|
|
|
|
if (
|
|
currNode.type === 'word' &&
|
|
(matched = currNode.value.match(/^([-,+]?\d+(\.\d+)?px)$/))
|
|
) {
|
|
/** matched px value. eg: '10px' */
|
|
const px = matched[1];
|
|
|
|
/** handled 0px edge case */
|
|
if (px === '0px') {
|
|
return;
|
|
}
|
|
|
|
/** check if prop is in ignore list, else -> set hasPX since an issue was matched */
|
|
if (
|
|
!_propAddXpxInIgnoreList(prop, ignore, px) &&
|
|
ignore.indexOf(px) === -1
|
|
) {
|
|
hasPX = true;
|
|
}
|
|
} else if (
|
|
/* handle string case, eg: mixins etc... */
|
|
currNode.type === 'string' &&
|
|
/(@\{[\w-]+\})px\b/.test(currNode.value)
|
|
) {
|
|
// eg. ~'@{width}px'
|
|
hasPX = true;
|
|
}
|
|
});
|
|
|
|
return hasPX;
|
|
};
|
|
|
|
/** rem-over-px plugin handler */
|
|
const pluginHandler =
|
|
(
|
|
primaryOption,
|
|
secondaryOptionObject = defaultSecondaryOptions,
|
|
context = {},
|
|
) =>
|
|
(root, result) => {
|
|
/** no primary option was provided or null, rule is disabled */
|
|
if (!primaryOption) {
|
|
return;
|
|
}
|
|
|
|
/* check for declarations */
|
|
root.walkDecls((declaration) => {
|
|
if (_hasForbiddenPX(declaration, secondaryOptionObject)) {
|
|
/* handle fixing */
|
|
if (context.fix) {
|
|
// Apply fixes using PostCSS API
|
|
declaration.value = _pxToRem(
|
|
declaration.value,
|
|
secondaryOptionObject.fontSize,
|
|
);
|
|
|
|
// Return and don't report a problem
|
|
return;
|
|
}
|
|
|
|
/* handle reporting */
|
|
stylelint.utils.report({
|
|
ruleName,
|
|
result,
|
|
node: declaration,
|
|
message: messages.remOverPx(declaration),
|
|
});
|
|
}
|
|
});
|
|
|
|
// check for rules
|
|
root.walkAtRules((atRule) => {
|
|
if (_hasForbiddenPX(atRule, secondaryOptionObject)) {
|
|
/* handle fixing */
|
|
if (context.fix) {
|
|
// Apply fixes using PostCSS API
|
|
atRule.value = _pxToRem(atRule.value, secondaryOptionObject.fontSize);
|
|
|
|
// Return and don't report a problem
|
|
return;
|
|
}
|
|
|
|
/* handle reporting */
|
|
stylelint.utils.report({
|
|
ruleName,
|
|
result,
|
|
node: atRule,
|
|
message: messages.remOverPx(atRule),
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Stylelint plugin rem-over-px
|
|
*
|
|
* Enforces the usage of rem units over px units.
|
|
*/
|
|
module.exports = stylelint.createPlugin(ruleName, pluginHandler);
|
|
|
|
module.exports.ruleName = ruleName;
|
|
module.exports.messages = messages;
|