You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
221 lines
5.6 KiB
221 lines
5.6 KiB
// @ts-nocheck |
|
|
|
'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(ruleNode) { |
|
if (!isStandardSyntaxRule(ruleNode)) { |
|
return; |
|
} |
|
|
|
if (!ruleNode.selector.includes('[') || !ruleNode.selector.includes('=')) { |
|
return; |
|
} |
|
|
|
const fixPositions = []; |
|
|
|
parseSelector(ruleNode.selector, result, ruleNode, (selectorTree) => { |
|
let selectorFixed = false; |
|
|
|
selectorTree.walkAttributes((attributeNode) => { |
|
if (!attributeNode.quoted) { |
|
return; |
|
} |
|
|
|
if (attributeNode.quoteMark === correctQuote) { |
|
if (avoidEscape) { |
|
const needsCorrectEscape = attributeNode.value.includes(correctQuote); |
|
const needsOtherEscape = attributeNode.value.includes(erroneousQuote); |
|
|
|
if (needsOtherEscape) { |
|
return; |
|
} |
|
|
|
if (needsCorrectEscape) { |
|
if (context.fix) { |
|
selectorFixed = true; |
|
attributeNode.quoteMark = erroneousQuote; |
|
} else { |
|
report({ |
|
message: messages.expected(expectation === 'single' ? 'double' : expectation), |
|
node: ruleNode, |
|
index: attributeNode.sourceIndex + attributeNode.offsetOf('value'), |
|
result, |
|
ruleName, |
|
}); |
|
} |
|
} |
|
} |
|
} |
|
|
|
if (attributeNode.quoteMark === erroneousQuote) { |
|
if (avoidEscape) { |
|
const needsCorrectEscape = attributeNode.value.includes(correctQuote); |
|
const needsOtherEscape = attributeNode.value.includes(erroneousQuote); |
|
|
|
if (needsOtherEscape) { |
|
if (context.fix) { |
|
selectorFixed = true; |
|
attributeNode.quoteMark = correctQuote; |
|
} else { |
|
report({ |
|
message: messages.expected(expectation), |
|
node: ruleNode, |
|
index: attributeNode.sourceIndex + attributeNode.offsetOf('value'), |
|
result, |
|
ruleName, |
|
}); |
|
} |
|
|
|
return; |
|
} |
|
|
|
if (needsCorrectEscape) { |
|
return; |
|
} |
|
} |
|
|
|
if (context.fix) { |
|
selectorFixed = true; |
|
attributeNode.quoteMark = correctQuote; |
|
} else { |
|
report({ |
|
message: messages.expected(expectation), |
|
node: ruleNode, |
|
index: attributeNode.sourceIndex + attributeNode.offsetOf('value'), |
|
result, |
|
ruleName, |
|
}); |
|
} |
|
} |
|
}); |
|
|
|
if (selectorFixed) { |
|
ruleNode.selector = selectorTree.toString(); |
|
} |
|
}); |
|
|
|
fixPositions.forEach((fixIndex) => { |
|
ruleNode.selector = replaceQuote(ruleNode.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;
|
|
|