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.
245 lines
7.2 KiB
245 lines
7.2 KiB
1 month ago
|
/**
|
||
|
* @author Yosuke ota
|
||
|
* See LICENSE file in root directory for full license.
|
||
|
*/
|
||
|
'use strict'
|
||
|
|
||
|
const htmlComments = require('../utils/html-comments')
|
||
|
|
||
|
/**
|
||
|
* Normalize options.
|
||
|
* @param {number|"tab"|undefined} type The type of indentation.
|
||
|
* @returns { { indentChar: string, indentSize: number, indentText: string } } Normalized options.
|
||
|
*/
|
||
|
function parseOptions(type) {
|
||
|
const ret = {
|
||
|
indentChar: ' ',
|
||
|
indentSize: 2,
|
||
|
indentText: ''
|
||
|
}
|
||
|
|
||
|
if (Number.isSafeInteger(type)) {
|
||
|
ret.indentSize = Number(type)
|
||
|
} else if (type === 'tab') {
|
||
|
ret.indentChar = '\t'
|
||
|
ret.indentSize = 1
|
||
|
}
|
||
|
ret.indentText = ret.indentChar.repeat(ret.indentSize)
|
||
|
|
||
|
return ret
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {string} s
|
||
|
* @param {string} [unitChar]
|
||
|
*/
|
||
|
function toDisplay(s, unitChar) {
|
||
|
if (s.length === 0 && unitChar) {
|
||
|
return `0 ${toUnit(unitChar)}s`
|
||
|
}
|
||
|
const char = s[0]
|
||
|
if ((char === ' ' || char === '\t') && [...s].every((c) => c === char)) {
|
||
|
return `${s.length} ${toUnit(char)}${s.length === 1 ? '' : 's'}`
|
||
|
}
|
||
|
|
||
|
return JSON.stringify(s)
|
||
|
}
|
||
|
|
||
|
/** @param {string} char */
|
||
|
function toUnit(char) {
|
||
|
if (char === '\t') {
|
||
|
return 'tab'
|
||
|
}
|
||
|
if (char === ' ') {
|
||
|
return 'space'
|
||
|
}
|
||
|
return JSON.stringify(char)
|
||
|
}
|
||
|
|
||
|
module.exports = {
|
||
|
meta: {
|
||
|
type: 'layout',
|
||
|
|
||
|
docs: {
|
||
|
description: 'enforce consistent indentation in HTML comments',
|
||
|
categories: undefined,
|
||
|
url: 'https://eslint.vuejs.org/rules/html-comment-indent.html'
|
||
|
},
|
||
|
fixable: 'whitespace',
|
||
|
schema: [
|
||
|
{
|
||
|
anyOf: [{ type: 'integer', minimum: 0 }, { enum: ['tab'] }]
|
||
|
}
|
||
|
],
|
||
|
messages: {
|
||
|
unexpectedBaseIndentation:
|
||
|
'Expected base point indentation of {{expected}}, but found {{actual}}.',
|
||
|
missingBaseIndentation:
|
||
|
'Expected base point indentation of {{expected}}, but not found.',
|
||
|
unexpectedIndentationCharacter:
|
||
|
'Expected {{expected}} character, but found {{actual}} character.',
|
||
|
unexpectedIndentation:
|
||
|
'Expected indentation of {{expected}} but found {{actual}}.',
|
||
|
unexpectedRelativeIndentation:
|
||
|
'Expected relative indentation of {{expected}} but found {{actual}}.'
|
||
|
}
|
||
|
},
|
||
|
/** @param {RuleContext} context */
|
||
|
create(context) {
|
||
|
const options = parseOptions(context.options[0])
|
||
|
const sourceCode = context.getSourceCode()
|
||
|
return htmlComments.defineVisitor(
|
||
|
context,
|
||
|
null,
|
||
|
(comment) => {
|
||
|
const baseIndentText = getLineIndentText(comment.open.loc.start.line)
|
||
|
let endLine
|
||
|
if (comment.value) {
|
||
|
const startLine = comment.value.loc.start.line
|
||
|
endLine = comment.value.loc.end.line
|
||
|
|
||
|
const checkStartLine =
|
||
|
comment.open.loc.end.line === startLine ? startLine + 1 : startLine
|
||
|
|
||
|
for (let line = checkStartLine; line <= endLine; line++) {
|
||
|
validateIndentForLine(line, baseIndentText, 1)
|
||
|
}
|
||
|
} else {
|
||
|
endLine = comment.open.loc.end.line
|
||
|
}
|
||
|
|
||
|
if (endLine < comment.close.loc.start.line) {
|
||
|
// `-->`
|
||
|
validateIndentForLine(comment.close.loc.start.line, baseIndentText, 0)
|
||
|
}
|
||
|
},
|
||
|
{ includeDirectives: true }
|
||
|
)
|
||
|
|
||
|
/**
|
||
|
* Checks whether the given line is a blank line.
|
||
|
* @param {number} line The number of line. Begins with 1.
|
||
|
* @returns {boolean} `true` if the given line is a blank line
|
||
|
*/
|
||
|
function isEmptyLine(line) {
|
||
|
const lineText = sourceCode.getLines()[line - 1]
|
||
|
return !lineText.trim()
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the actual indentation of the given line.
|
||
|
* @param {number} line The number of line. Begins with 1.
|
||
|
* @returns {string} The actual indentation text
|
||
|
*/
|
||
|
function getLineIndentText(line) {
|
||
|
const lineText = sourceCode.getLines()[line - 1]
|
||
|
const charIndex = lineText.search(/\S/)
|
||
|
// already checked
|
||
|
// if (charIndex < 0) {
|
||
|
// return lineText
|
||
|
// }
|
||
|
return lineText.slice(0, charIndex)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Define the function which fixes the problem.
|
||
|
* @param {number} line The number of line.
|
||
|
* @param {string} actualIndentText The actual indentation text.
|
||
|
* @param {string} expectedIndentText The expected indentation text.
|
||
|
* @returns { (fixer: RuleFixer) => Fix } The defined function.
|
||
|
*/
|
||
|
function defineFix(line, actualIndentText, expectedIndentText) {
|
||
|
return (fixer) => {
|
||
|
const start = sourceCode.getIndexFromLoc({
|
||
|
line,
|
||
|
column: 0
|
||
|
})
|
||
|
return fixer.replaceTextRange(
|
||
|
[start, start + actualIndentText.length],
|
||
|
expectedIndentText
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Validate the indentation of a line.
|
||
|
* @param {number} line The number of line. Begins with 1.
|
||
|
* @param {string} baseIndentText The expected base indentation text.
|
||
|
* @param {number} offset The number of the indentation offset.
|
||
|
*/
|
||
|
function validateIndentForLine(line, baseIndentText, offset) {
|
||
|
if (isEmptyLine(line)) {
|
||
|
return
|
||
|
}
|
||
|
const actualIndentText = getLineIndentText(line)
|
||
|
|
||
|
const expectedOffsetIndentText = options.indentText.repeat(offset)
|
||
|
const expectedIndentText = baseIndentText + expectedOffsetIndentText
|
||
|
|
||
|
// validate base indent
|
||
|
if (
|
||
|
baseIndentText &&
|
||
|
(actualIndentText.length < baseIndentText.length ||
|
||
|
!actualIndentText.startsWith(baseIndentText))
|
||
|
) {
|
||
|
context.report({
|
||
|
loc: {
|
||
|
start: { line, column: 0 },
|
||
|
end: { line, column: actualIndentText.length }
|
||
|
},
|
||
|
messageId: actualIndentText
|
||
|
? 'unexpectedBaseIndentation'
|
||
|
: 'missingBaseIndentation',
|
||
|
data: {
|
||
|
expected: toDisplay(baseIndentText),
|
||
|
actual: toDisplay(actualIndentText.slice(0, baseIndentText.length))
|
||
|
},
|
||
|
fix: defineFix(line, actualIndentText, expectedIndentText)
|
||
|
})
|
||
|
return
|
||
|
}
|
||
|
|
||
|
const actualOffsetIndentText = actualIndentText.slice(
|
||
|
baseIndentText.length
|
||
|
)
|
||
|
|
||
|
// validate indent charctor
|
||
|
for (const [i, char] of [...actualOffsetIndentText].entries()) {
|
||
|
if (char !== options.indentChar) {
|
||
|
context.report({
|
||
|
loc: {
|
||
|
start: { line, column: baseIndentText.length + i },
|
||
|
end: { line, column: baseIndentText.length + i + 1 }
|
||
|
},
|
||
|
messageId: 'unexpectedIndentationCharacter',
|
||
|
data: {
|
||
|
expected: toUnit(options.indentChar),
|
||
|
actual: toUnit(char)
|
||
|
},
|
||
|
fix: defineFix(line, actualIndentText, expectedIndentText)
|
||
|
})
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// validate indent length
|
||
|
if (actualOffsetIndentText.length !== expectedOffsetIndentText.length) {
|
||
|
context.report({
|
||
|
loc: {
|
||
|
start: { line, column: baseIndentText.length },
|
||
|
end: { line, column: actualIndentText.length }
|
||
|
},
|
||
|
messageId: baseIndentText
|
||
|
? 'unexpectedRelativeIndentation'
|
||
|
: 'unexpectedIndentation',
|
||
|
data: {
|
||
|
expected: toDisplay(expectedOffsetIndentText, options.indentChar),
|
||
|
actual: toDisplay(actualOffsetIndentText, options.indentChar)
|
||
|
},
|
||
|
fix: defineFix(line, actualIndentText, expectedIndentText)
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|