stylelint
Version:
Modern CSS linter
120 lines (96 loc) • 3.43 kB
JavaScript
import valueParser from "postcss-value-parser"
import { vendor } from "postcss"
import {
isStandardSyntaxDeclaration,
isStandardSyntaxProperty,
report,
ruleMessages,
validateOptions,
} from "../../utils"
import shorthandData from "../../reference/shorthandData"
export const ruleName = "shorthand-property-no-redundant-values"
export const messages = ruleMessages(ruleName, {
rejected: (unexpected, expected) => `Unexpected longhand value '${unexpected}' instead of '${expected}'`,
})
const shorthandableProperties = new Set(Object.keys(shorthandData))
const ignoredCharacters = [
"+", "-", "*", "/", "(", ")",
"$", "@", "--", "var(",
]
const ignoredShorthandProperties = new Set([
"background",
"font",
"border",
"border-top",
"border-bottom",
"border-left",
"border-right",
"list-style",
"transition",
])
function isIgnoredCharacters(value) {
return ignoredCharacters.some(char => value.indexOf(char) !== -1)
}
function canCondense(top, right, bottom = null, left = null) {
const lowerTop = top.toLowerCase()
const lowerRight = right.toLowerCase()
const lowerBottom = bottom && bottom.toLowerCase()
const lowerLeft = left && left.toLowerCase()
if (canCondenseToOneValue(lowerTop, lowerRight, lowerBottom, lowerLeft)) {
return [top]
} else if (canCondenseToTwoValues(lowerTop, lowerRight, lowerBottom, lowerLeft)) {
return [ top, right ]
} else if (canCondenseToThreeValues(lowerTop, lowerRight, lowerBottom, lowerLeft)) {
return [ top, right, bottom ]
} else {
return [ top, right, bottom, left ]
}
}
function canCondenseToOneValue(top, right, bottom, left) {
if (top !== right) { return false }
return top === bottom && (bottom === left || !left) || !bottom && !left
}
function canCondenseToTwoValues(top, right, bottom, left) {
return top === bottom && right === left || top === bottom && !left && top !== right
}
function canCondenseToThreeValues(top, right, bottom, left) {
return right === left
}
export default function (actual) {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, { actual })
if (!validOptions) { return }
root.walkDecls(decl => {
if (
!isStandardSyntaxDeclaration(decl)
|| !isStandardSyntaxProperty(decl.prop)
) { return }
const { prop, value } = decl
const normalizedProp = vendor.unprefixed(prop.toLowerCase())
// Ignore not shorthandable properties, and math operations
if (
isIgnoredCharacters(value)
|| !shorthandableProperties.has(normalizedProp)
|| ignoredShorthandProperties.has(normalizedProp)
) { return }
const valuesToShorthand = []
valueParser(value).walk((valueNode) => {
if (valueNode.type !== "word") { return }
valuesToShorthand.push(valueParser.stringify(valueNode))
})
if (valuesToShorthand.length <= 1
|| valuesToShorthand.length > 4
) { return }
const shortestForm = canCondense(...valuesToShorthand)
const shortestFormString = shortestForm.filter((value) => { return value }).join(" ")
const valuesFormString = valuesToShorthand.join(" ")
if (shortestFormString.toLowerCase() === valuesFormString.toLowerCase()) { return }
report({
message: messages.rejected(value, shortestFormString),
node: decl,
result,
ruleName,
})
})
}
}