stylelint
Version:
Modern CSS linter
119 lines (98 loc) • 4.04 kB
JavaScript
import {
findIndex,
findLastIndex,
range,
} from "lodash"
import {
blurComments,
cssStatementHasBlock,
cssStatementStringBeforeBlock,
report,
ruleMessages,
styleSearch,
validateOptions,
} from "../../utils"
export const ruleName = "number-zero-length-no-unit"
export const messages = ruleMessages(ruleName, {
rejected: "Unexpected unit on zero length number",
})
// Only length units can be left off
// cf. http://www.w3.org/TR/css3-values/#length-value
// cf. https://github.com/brigade/scss-lint/issues/154
const lengthUnits = new Set([
"em", "ex", "ch", "vw", "vh", "cm", "mm", "in", "pt", "pc", "px",
"rem",
"vmin", "vmax",
])
export default function (actual) {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, { actual })
if (!validOptions) { return }
root.walkDecls(decl => {
check(blurComments(decl.toString()), decl)
})
root.walkAtRules(atRule => {
const source = (cssStatementHasBlock(atRule))
? cssStatementStringBeforeBlock(atRule, { noBefore: true })
: atRule.toString()
check(source, atRule)
})
function check(value, node) {
const ignorableIndexes = new Set()
styleSearch({ source: value, target: "0" }, match => {
const index = match.startIndex
// Given a 0 somewhere in the full property value (not in a string, thanks
// to styleSearch) we need to isolate the value that contains the zero.
// To do so, we'll find the last index before the 0 of a character that would
// divide one value in a list from another, and the next index of such a
// character; then we build a substring from those indexes, which we can
// assess.
// If a single value includes multiple 0's (e.g. 100.01px), we don't want
// each 0 to be treated as a separate value, possibly resulting in multiple
// warnings for the same value (e.g. 0.00px).
//
// This check prevents that from happening: we build and check against a
// Set containing all the indexes that are part of a value already validated.
if (ignorableIndexes.has(index)) { return }
const prevValueBreakIndex = findLastIndex(value.substr(0, index), char => {
return [ " ", ",", ")", "(", "#" ].indexOf(char) !== -1
})
// Ignore hex colors
if (value[prevValueBreakIndex] === "#") { return }
// If no prev break was found, this value starts at 0
const valueWithZeroStart = (prevValueBreakIndex === -1)
? 0
: prevValueBreakIndex + 1
const nextValueBreakIndex = findIndex(value.substr(valueWithZeroStart), char => {
return [ " ", ",", ")" ].indexOf(char) !== -1
})
// If no next break was found, this value ends at the end of the string
const valueWithZeroEnd = (nextValueBreakIndex === -1)
? value.length
: nextValueBreakIndex + valueWithZeroStart
const valueWithZero = value.slice(valueWithZeroStart, valueWithZeroEnd)
// Add the indexes to ignorableIndexes so the same value will not
// be checked multiple times.
range(valueWithZeroStart, valueWithZeroEnd).forEach(i => ignorableIndexes.add(i))
// Only pay attention if the value parses to 0
if (parseFloat(valueWithZero, 10) !== 0) { return }
// If there is not a length unit at the end of this value, ignore.
// (Length units are 2, 3, or 4 characters)
const unitLength = (function () {
if (lengthUnits.has(valueWithZero.slice(-4))) { return 4 }
if (lengthUnits.has(valueWithZero.slice(-3))) { return 3 }
if (lengthUnits.has(valueWithZero.slice(-2))) { return 2 }
return 0
}())
if (!unitLength) { return }
report({
message: messages.rejected,
node,
index: valueWithZeroEnd - unitLength,
result,
ruleName,
})
})
}
}
}