blob: 041e6c11c7933fd37cae5a55b114d63ca5729310 [file] [log] [blame]
"use strict";
const _ = require("lodash");
const beforeBlockString = require("../../utils/beforeBlockString");
const blurComments = require("../../utils/blurComments");
const hasBlock = require("../../utils/hasBlock");
const isCustomProperty = require("../../utils/isCustomProperty");
const keywordSets = require("../../reference/keywordSets");
const optionsMatches = require("../../utils/optionsMatches");
const report = require("../../utils/report");
const ruleMessages = require("../../utils/ruleMessages");
const styleSearch = require("style-search");
const validateOptions = require("../../utils/validateOptions");
const valueParser = require("postcss-value-parser");
const ruleName = "length-zero-no-unit";
const messages = ruleMessages(ruleName, {
rejected: "Unexpected unit"
});
const rule = function(actual, secondary, context) {
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 = hasBlock(atRule)
? beforeBlockString(atRule, { noRawBefore: true })
: atRule.toString();
check(source, atRule);
});
function check(value, node) {
const ignorableIndexes = new Set();
const fixPositions = [];
if (
optionsMatches(secondary, "ignore", "custom-properties") &&
isCustomProperty(value)
) {
return;
}
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 (
[" ", ",", ")", "(", "#", ":", "\n", "\t"].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);
const parsedValue = valueParser.unit(valueWithZero);
if (!parsedValue || (parsedValue && !parsedValue.unit)) {
return;
}
// 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
// and units with lengths
if (
parseFloat(valueWithZero, 10) !== 0 ||
!keywordSets.lengthUnits.has(parsedValue.unit.toLowerCase())
) {
return;
}
if (context.fix) {
fixPositions.unshift({
startIndex: valueWithZeroStart,
length: valueWithZeroEnd - valueWithZeroStart
});
return;
}
report({
message: messages.rejected,
node,
index: valueWithZeroEnd - parsedValue.unit.length,
result,
ruleName
});
});
if (fixPositions.length) {
fixPositions.forEach(function(fixPosition) {
if (node.type === "atrule") {
// Use `-1` for `@` character before each at rule
const realIndex =
fixPosition.startIndex -
node.name.length -
node.raws.afterName.length -
1;
node.params = replaceZero(
node.params,
realIndex,
fixPosition.length
);
} else {
const realIndex =
fixPosition.startIndex -
node.prop.length -
node.raws.between.length;
node.value = replaceZero(node.value, realIndex, fixPosition.length);
}
});
}
}
};
};
function replaceZero(input, startIndex, length) {
const stringStart = input.slice(0, startIndex);
const stringEnd = input.slice(startIndex + length);
return stringStart + "0" + stringEnd;
}
rule.ruleName = ruleName;
rule.messages = messages;
module.exports = rule;