'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 = `"`; function rule(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.includes('[') || !rule.selector.includes('=')) { return; } const fixPositions = []; parseSelector(rule.selector, result, rule, (selectorTree) => { selectorTree.walkAttributes((attributeNode) => { if (attributeNode.quoted && attributeNode.value.includes(erroneousQuote)) { const needsEscape = attributeNode.value.includes(correctQuote); 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.includes(erroneousQuote)) { return; } 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.includes(correctQuote); 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;