|
"use strict"; |
|
Object.defineProperty(exports, "__esModule", { value: true }); |
|
const utils_1 = require("../utils"); |
|
const ast_utils_1 = require("../utils/ast-utils"); |
|
const ast_utils_2 = require("../utils/ast-utils"); |
|
const compat_1 = require("../utils/compat"); |
|
const QUOTE_CHARS = { |
|
double: '"', |
|
single: "'" |
|
}; |
|
const QUOTE_NAMES = { |
|
double: 'double quotes', |
|
single: 'single quotes', |
|
unquoted: 'unquoted' |
|
}; |
|
exports.default = (0, utils_1.createRule)('html-quotes', { |
|
meta: { |
|
docs: { |
|
description: 'enforce quotes style of HTML attributes', |
|
category: 'Stylistic Issues', |
|
recommended: false, |
|
conflictWithPrettier: true |
|
}, |
|
fixable: 'code', |
|
schema: [ |
|
{ |
|
type: 'object', |
|
properties: { |
|
prefer: { enum: ['double', 'single'] }, |
|
dynamic: { |
|
type: 'object', |
|
properties: { |
|
quoted: { type: 'boolean' }, |
|
avoidInvalidUnquotedInHTML: { type: 'boolean' } |
|
}, |
|
additionalProperties: false |
|
} |
|
}, |
|
additionalProperties: false |
|
} |
|
], |
|
messages: { |
|
expectedEnclosed: 'Expected to be enclosed by quotes.', |
|
expectedEnclosedBy: 'Expected to be enclosed by {{kind}}.', |
|
unexpectedEnclosed: 'Unexpected to be enclosed by any quotes.' |
|
}, |
|
type: 'layout' |
|
}, |
|
create(context) { |
|
const sourceCode = (0, compat_1.getSourceCode)(context); |
|
const preferQuote = context.options[0]?.prefer ?? 'double'; |
|
const dynamicQuote = context.options[0]?.dynamic?.quoted ? preferQuote : 'unquoted'; |
|
const avoidInvalidUnquotedInHTML = Boolean(context.options[0]?.dynamic?.avoidInvalidUnquotedInHTML); |
|
|
|
function canBeUnquotedInHTML(text) { |
|
return !/[\s"'<=>`]/u.test(text); |
|
} |
|
|
|
function verifyQuote(prefer, quoteAndRange) { |
|
if (!quoteAndRange) { |
|
|
|
return; |
|
} |
|
if (quoteAndRange.quote === prefer) { |
|
|
|
return; |
|
} |
|
let messageId; |
|
let expectedQuote = prefer; |
|
if (quoteAndRange.quote !== 'unquoted') { |
|
if (expectedQuote === 'unquoted') { |
|
messageId = 'unexpectedEnclosed'; |
|
} |
|
else { |
|
const contentText = sourceCode.text.slice(quoteAndRange.range[0] + 1, quoteAndRange.range[1] - 1); |
|
const needEscape = contentText.includes(QUOTE_CHARS[expectedQuote]); |
|
if (needEscape) { |
|
|
|
return; |
|
} |
|
messageId = 'expectedEnclosedBy'; |
|
} |
|
} |
|
else { |
|
const contentText = sourceCode.text.slice(...quoteAndRange.range); |
|
const needEscapeDoubleQuote = contentText.includes('"'); |
|
const needEscapeSingleQuote = contentText.includes("'"); |
|
if (needEscapeDoubleQuote && needEscapeSingleQuote) { |
|
|
|
return; |
|
} |
|
if (needEscapeDoubleQuote && expectedQuote === 'double') { |
|
expectedQuote = 'single'; |
|
messageId = 'expectedEnclosed'; |
|
} |
|
else if (needEscapeSingleQuote && expectedQuote === 'single') { |
|
expectedQuote = 'double'; |
|
messageId = 'expectedEnclosed'; |
|
} |
|
else { |
|
messageId = 'expectedEnclosedBy'; |
|
} |
|
} |
|
context.report({ |
|
loc: { |
|
start: sourceCode.getLocFromIndex(quoteAndRange.range[0]), |
|
end: sourceCode.getLocFromIndex(quoteAndRange.range[1]) |
|
}, |
|
messageId, |
|
data: { kind: QUOTE_NAMES[expectedQuote] }, |
|
*fix(fixer) { |
|
if (expectedQuote !== 'unquoted') { |
|
yield fixer.insertTextBeforeRange([quoteAndRange.range[0], quoteAndRange.range[0]], QUOTE_CHARS[expectedQuote]); |
|
} |
|
if (quoteAndRange.quote !== 'unquoted') { |
|
yield fixer.removeRange([quoteAndRange.range[0], quoteAndRange.range[0] + 1]); |
|
yield fixer.removeRange([quoteAndRange.range[1] - 1, quoteAndRange.range[1]]); |
|
} |
|
if (expectedQuote !== 'unquoted') { |
|
yield fixer.insertTextAfterRange([quoteAndRange.range[1], quoteAndRange.range[1]], QUOTE_CHARS[expectedQuote]); |
|
} |
|
} |
|
}); |
|
} |
|
|
|
function verifyForValues(attr) { |
|
const quoteAndRange = (0, ast_utils_2.getAttributeValueQuoteAndRange)(attr, sourceCode); |
|
verifyQuote(preferQuote, quoteAndRange); |
|
} |
|
|
|
function verifyForDynamicMustacheTag(attr, valueNode) { |
|
const quoteAndRange = (0, ast_utils_2.getAttributeValueQuoteAndRange)(attr, sourceCode); |
|
const text = sourceCode.text.slice(...valueNode.range); |
|
verifyQuote(avoidInvalidUnquotedInHTML && !canBeUnquotedInHTML(text) ? preferQuote : dynamicQuote, quoteAndRange); |
|
} |
|
|
|
function verifyForDirective(attr) { |
|
const mustacheTokens = (0, ast_utils_1.getMustacheTokens)(attr, sourceCode); |
|
if (!mustacheTokens) { |
|
return; |
|
} |
|
const quoteAndRange = (0, ast_utils_2.getAttributeValueQuoteAndRange)(attr, sourceCode); |
|
const text = sourceCode.text.slice(mustacheTokens.openToken.range[0], mustacheTokens.closeToken.range[1]); |
|
verifyQuote(avoidInvalidUnquotedInHTML && !canBeUnquotedInHTML(text) ? preferQuote : dynamicQuote, quoteAndRange); |
|
} |
|
return { |
|
'SvelteAttribute, SvelteStyleDirective'(node) { |
|
if (node.value.length === 1 && node.value[0].type === 'SvelteMustacheTag') { |
|
verifyForDynamicMustacheTag(node, node.value[0]); |
|
} |
|
else if (node.value.length >= 1) { |
|
verifyForValues(node); |
|
} |
|
}, |
|
'SvelteDirective, SvelteSpecialDirective'(node) { |
|
if (node.expression == null) { |
|
return; |
|
} |
|
if (node.key.range[0] <= node.expression.range[0] && |
|
node.expression.range[1] <= node.key.range[1]) { |
|
|
|
return; |
|
} |
|
verifyForDirective(node); |
|
} |
|
}; |
|
} |
|
}); |
|
|