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.
258 lines
7.8 KiB
258 lines
7.8 KiB
/** |
|
* @author Yosuke ota |
|
* See LICENSE file in root directory for full license. |
|
*/ |
|
'use strict' |
|
|
|
// ----------------------------------------------------------------------------- |
|
// Requirements |
|
// ----------------------------------------------------------------------------- |
|
|
|
const htmlComments = require('../utils/html-comments') |
|
|
|
// ------------------------------------------------------------------------------ |
|
// Helpers |
|
// ------------------------------------------------------------------------------ |
|
|
|
/** |
|
* 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') { |
|
if (s.split('').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) |
|
} |
|
|
|
// ------------------------------------------------------------------------------ |
|
// Rule Definition |
|
// ------------------------------------------------------------------------------ |
|
|
|
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 (let i = 0; i < actualOffsetIndentText.length; ++i) { |
|
if (actualOffsetIndentText[i] !== 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(actualOffsetIndentText[i]) |
|
}, |
|
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) |
|
}) |
|
} |
|
} |
|
} |
|
}
|
|
|