plugins/spacing.js (105 lines of code) (raw):
const stylelint = require('stylelint')
const declarationValueIndex = require('stylelint/lib/utils/declarationValueIndex')
const valueParser = require('postcss-value-parser')
const spacerValues = {
'$spacer-1': '4px',
'$spacer-2': '8px',
'$spacer-3': '16px',
'$spacer-4': '24px',
'$spacer-5': '32px',
'$spacer-6': '40px',
'$spacer-7': '48px',
'$spacer-8': '64px',
'$spacer-9': '80px',
'$spacer-10': '96px',
'$spacer-11': '112px',
'$spacer-12': '128px',
'$em-spacer-1': '0.0625em',
'$em-spacer-2': '0.125em',
'$em-spacer-3': '0.25em',
'$em-spacer-4': '0.375em',
'$em-spacer-5': '0.5em',
'$em-spacer-6': '0.75em'
}
const ruleName = 'primer/spacing'
const messages = stylelint.utils.ruleMessages(ruleName, {
rejected: (value, replacement) => {
if (replacement === null) {
return `Please use a primer spacer variable instead of '${value}'. Consult the primer docs for a suitable replacement. https://primer.style/css/support/spacing`
}
return `Please replace ${value} with spacing variable '${replacement}'.`
}
})
// eslint-disable-next-line no-unused-vars
module.exports = stylelint.createPlugin(ruleName, (enabled, options = {}, context) => {
if (!enabled) {
return noop
}
const lintResult = (root, result) => {
root.walk(decl => {
if (decl.type !== 'decl' || !decl.prop.match(/^(padding|margin)/)) {
return noop
}
const problems = []
let containsMath = false
let conatinsVariable = false
const parsedValue = valueParser(decl.value).walk(declValue => {
// Only check word types. https://github.com/TrySound/postcss-value-parser#word
if (!['word', 'function'].includes(declValue.type)) {
return false
}
// Ignore values that are not numbers.
if (['0', 'auto', 'inherit', 'initial'].includes(declValue.value)) {
return noop
}
// Remove leading negative sign, if any.
const cleanDeclValue = declValue.value.replace(/^-/g, '')
// If the a variable is found in the value, skip it.
if (
Object.keys(spacerValues).some(variable =>
new RegExp(`${variable.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(cleanDeclValue)
)
) {
conatinsVariable = true
return noop
}
// For now we're going to ignore math.
if (['*', '+', '-', '/'].includes(declValue.value)) {
containsMath = true
return noop
}
let valueMatch = null
if ((valueMatch = Object.keys(spacerValues).find(spacer => spacerValues[spacer] === cleanDeclValue))) {
if (context.fix) {
declValue.value = declValue.value.replace(spacerValues[valueMatch], valueMatch)
} else {
problems.push({
index: declarationValueIndex(decl) + declValue.sourceIndex,
message: messages.rejected(spacerValues[valueMatch], valueMatch)
})
}
} else if (declValue.value !== '' && declValue.type !== 'function' && !containsMath) {
problems.push({
index: declarationValueIndex(decl) + declValue.sourceIndex,
message: messages.rejected(declValue.value, null)
})
}
})
if (context.fix) {
decl.value = parsedValue.toString()
}
if (containsMath && conatinsVariable) {
return noop
}
if (problems.length) {
for (const err of problems) {
stylelint.utils.report({
index: err.index,
message: err.message,
node: decl,
result,
ruleName
})
}
}
})
}
return lintResult
})
function noop() {}
module.exports.ruleName = ruleName
module.exports.messages = messages