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