blob: 2d305f317083c089b818081f636adcab4ec65dc3 [file] [log] [blame]
"use strict";
const _ = require("lodash");
const atRuleParamIndex = require("../../utils/atRuleParamIndex");
const declarationValueIndex = require("../../utils/declarationValueIndex");
const isStandardSyntaxRule = require("../../utils/isStandardSyntaxRule");
const parseSelector = require("../../utils/parseSelector");
const report = require("../../utils/report");
const ruleMessages = require("../../utils/ruleMessages");
const validateOptions = require("../../utils/validateOptions");
const valueParser = require("postcss-value-parser");
const ruleName = "string-quotes";
const messages = ruleMessages(ruleName, {
expected: q => `Expected ${q} quotes`
});
const singleQuote = `'`;
const doubleQuote = `"`;
const rule = function(expectation, secondary, context) {
const correctQuote = expectation === "single" ? singleQuote : doubleQuote;
const erroneousQuote = expectation === "single" ? doubleQuote : singleQuote;
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: expectation,
possible: ["single", "double"]
},
{
actual: secondary,
possible: {
avoidEscape: _.isBoolean
},
optional: true
}
);
if (!validOptions) {
return;
}
const avoidEscape = _.get(secondary, "avoidEscape", true);
root.walk(node => {
switch (node.type) {
case "atrule":
checkDeclOrAtRule(node, node.params, atRuleParamIndex);
break;
case "decl":
checkDeclOrAtRule(node, node.value, declarationValueIndex);
break;
case "rule":
checkRule(node);
break;
}
});
function checkRule(rule) {
if (!isStandardSyntaxRule(rule)) {
return;
}
if (
rule.selector.indexOf("[") === -1 ||
rule.selector.indexOf("=") === -1
) {
return;
}
const fixPositions = [];
parseSelector(rule.selector, result, rule, selectorTree => {
selectorTree.walkAttributes(attributeNode => {
if (
attributeNode.quoted &&
attributeNode.value.indexOf(erroneousQuote) !== -1
) {
const needsEscape =
attributeNode.value.indexOf(correctQuote) !== -1;
if (avoidEscape && needsEscape) {
// don't consider this an error
return;
}
const openIndex =
// index of the start of our attribute node in our source
attributeNode.sourceIndex +
// length of our attribute
attributeNode.attribute.length +
// length of our operator , ie '='
attributeNode.operator.length +
// and the length of the quote
erroneousQuote.length;
// we currently don't fix escapes
if (context.fix && !needsEscape) {
const closeIndex =
// our initial index
openIndex +
// the length of our value
attributeNode.value.length -
// with the length of our quote subtracted
erroneousQuote.length;
fixPositions.push(openIndex, closeIndex);
} else {
report({
message: messages.expected(expectation),
node: rule,
index: openIndex,
result,
ruleName
});
}
}
});
});
fixPositions.forEach(fixIndex => {
rule.selector = replaceQuote(rule.selector, fixIndex, correctQuote);
});
}
function checkDeclOrAtRule(node, value, getIndex) {
const fixPositions = [];
// Get out quickly if there are no erroneous quotes
if (value.indexOf(erroneousQuote) === -1) {
return;
} else if (node.type === "atrule" && node.name === "charset") {
// allow @charset rules to have double quotes, in spite of the configuration
// TODO: @charset should always use double-quotes, see https://github.com/stylelint/stylelint/issues/2788
return;
}
valueParser(value).walk(valueNode => {
if (valueNode.type === "string" && valueNode.quote === erroneousQuote) {
const needsEscape = valueNode.value.indexOf(correctQuote) !== -1;
if (avoidEscape && needsEscape) {
// don't consider this an error
return;
}
const openIndex = valueNode.sourceIndex;
// we currently don't fix escapes
if (context.fix && !needsEscape) {
const closeIndex =
openIndex + valueNode.value.length + erroneousQuote.length;
fixPositions.push(openIndex, closeIndex);
} else {
report({
message: messages.expected(expectation),
node,
index: getIndex(node) + openIndex,
result,
ruleName
});
}
}
});
fixPositions.forEach(fixIndex => {
if (node.type === "atrule") {
node.params = replaceQuote(node.params, fixIndex, correctQuote);
} else {
node.value = replaceQuote(node.value, fixIndex, correctQuote);
}
});
}
};
};
function replaceQuote(string, index, replace) {
return (
string.substring(0, index) +
replace +
string.substring(index + replace.length)
);
}
rule.ruleName = ruleName;
rule.messages = messages;
module.exports = rule;