| "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; |