diff --git a/packages/eslint-plugin/lib/rules/indent/indent.js b/packages/eslint-plugin/lib/rules/indent/indent.js index ee1f83a5..542d14d6 100644 --- a/packages/eslint-plugin/lib/rules/indent/indent.js +++ b/packages/eslint-plugin/lib/rules/indent/indent.js @@ -5,6 +5,7 @@ * @typedef { import("../../types").BaseNode } BaseNode * @typedef { import("../../types").TagNode } TagNode * @typedef { import("../../types").RuleListener } RuleListener + * @typedef { import("../../types").Context } Context * @typedef { import("eslint").AST.Token } Token * @typedef { import("eslint").SourceCode } SourceCode * @typedef { import("estree").TemplateLiteral } TemplateLiteral @@ -13,11 +14,15 @@ * @property {"space"} SPACE * @typedef {Object} MessageId * @property {"wrongIndent"} WRONG_INDENT + * @typedef {Object} IndentOptionInfo + * @property {IndentType["TAB"] | IndentType["SPACE"]} indentType + * @property {number} indentSize + * @property {string} indentChar */ const { parse } = require("@html-eslint/template-parser"); const { RULE_CATEGORY } = require("../../constants"); -const { splitToLineNodes } = require("../utils/node"); +const { splitToLineNodes, isLine, isTag } = require("../utils/node"); const { shouldCheckTaggedTemplateExpression, shouldCheckTemplateLiteral, @@ -72,6 +77,17 @@ module.exports = { minimum: 1, default: 1, }, + tagChildrenIndent: { + default: {}, + type: "object", + patternProperties: { + "^[a-z]+$": { + type: "integer", + minimum: 0, + }, + }, + additionalProperties: false, + }, }, }, ], @@ -84,39 +100,44 @@ module.exports = { const sourceCode = getSourceCode(context); const indentLevelOptions = (context.options && context.options[1]) || {}; const lines = sourceCode.getLines(); - const { indentType, indentSize, indentChar } = (function () { - const options = context.options; - /** - * @type {IndentType['SPACE'] | IndentType['TAB']} - */ - let indentType = INDENT_TYPES.SPACE; - let indentSize = 4; - if (options.length) { - if (options[0] === INDENT_TYPES.TAB) { - indentType = INDENT_TYPES.TAB; - } else { - indentSize = options[0]; - } - } - const indentChar = - indentType === INDENT_TYPES.SPACE ? " ".repeat(indentSize) : "\t"; - return { indentType, indentSize, indentChar }; - })(); + const { indentType, indentSize, indentChar } = getIndentOptionInfo(context); /** - * @param {string} str - * @returns {number} + * @param {TagNode} node + * @return {number} */ - function countLeftPadding(str) { - return str.length - str.replace(/^[\s\t]+/, "").length; + function getTagIncreasingLevel(node) { + if ( + node.parent && + isTag(node.parent) && + indentLevelOptions && + typeof indentLevelOptions.tagChildrenIndent === "object" && + indentLevelOptions.tagChildrenIndent + ) { + const option = + indentLevelOptions.tagChildrenIndent[node.parent.name.toLowerCase()]; + if (typeof option === "number") { + return option; + } + } + + return 1; } /** * @param {AnyNode} node - * @returns {node is LineNode} + * @return {number} */ - function isLineNode(node) { - return node.type === "Line"; + function getIncreasingLevel(node) { + if (isLine(node)) { + return 1; + } + if (isTag(node)) { + return getTagIncreasingLevel(node); + } + return typeof indentLevelOptions[node.type] === "number" + ? indentLevelOptions[node.type] + : 1; } /** @@ -142,18 +163,14 @@ module.exports = { */ function createIndentVisitor(baseLevel) { const indentLevel = new IndentLevel({ - getIncreasingLevel(node) { - return typeof indentLevelOptions[node.type] === "number" - ? indentLevelOptions[node.type] - : 1; - }, + getIncreasingLevel, }); indentLevel.setBase(baseLevel); let parentIgnoringChildCount = 0; /** - * @param {AnyNode} node + * @param {AnyNode | LineNode} node * @returns {string} */ function getActualIndent(node) { @@ -161,7 +178,7 @@ module.exports = { const line = lines[node.loc.start.line - 1]; let column = node.loc.start.column; - if (isLineNode(node)) { + if (isLine(node)) { column += countLeftPadding(node.value); } @@ -175,33 +192,6 @@ module.exports = { return indentChar.repeat(indentLevel.value()); } - /** - * @param {AnyNode} node - * @param {string} actualIndent - * @return {BaseNode} - */ - function getIndentNodeToReport(node, actualIndent) { - let rangeStart = node.range[0]; - - if (node.type !== "Line") { - rangeStart -= actualIndent.length; - } - - return { - range: [rangeStart, rangeStart + actualIndent.length], - loc: { - start: { - column: 0, - line: node.loc.start.line, - }, - end: { - column: actualIndent.length, - line: node.loc.start.line, - }, - }, - }; - } - /** * @param {string} actualIndent * @param {number} expectedIndentSize @@ -235,7 +225,7 @@ module.exports = { } /** - * @param {AnyNode} node + * @param {AnyNode | LineNode} node */ function checkIndent(node) { if (parentIgnoringChildCount > 0) { @@ -287,7 +277,9 @@ module.exports = { OpenStyleTagStart: checkIndent, OpenStyleTagEnd: checkIndent, OpenTagStart: checkIndent, - OpenTagEnd: checkIndent, + OpenTagEnd(node) { + checkIndent(node); + }, CloseTag: checkIndent, "Tag:exit"(node) { if (IGNORING_NODES.includes(node.name)) { @@ -296,7 +288,6 @@ module.exports = { indentLevel.dedent(node); }, - // Attribute Attribute(node) { indentLevel.indent(node); }, @@ -305,8 +296,6 @@ module.exports = { "Attribute:exit"(node) { indentLevel.dedent(node); }, - - // Text Text(node) { indentLevel.indent(node); const lineNodes = splitToLineNodes(node); @@ -367,3 +356,61 @@ module.exports = { }; }, }; + +/** + * @param {AnyNode | LineNode} node + * @param {string} actualIndent + * @return {BaseNode} + */ +function getIndentNodeToReport(node, actualIndent) { + let rangeStart = node.range[0]; + + if (!isLine(node)) { + rangeStart -= actualIndent.length; + } + + return { + range: [rangeStart, rangeStart + actualIndent.length], + loc: { + start: { + column: 0, + line: node.loc.start.line, + }, + end: { + column: actualIndent.length, + line: node.loc.start.line, + }, + }, + }; +} + +/** + * @param {string} str + * @returns {number} + */ +function countLeftPadding(str) { + return str.length - str.replace(/^[\s\t]+/, "").length; +} + +/** + * @param {Context} context + * @return {IndentOptionInfo} + */ +function getIndentOptionInfo(context) { + const options = context.options; + /** + * @type {IndentType['SPACE'] | IndentType['TAB']} + */ + let indentType = INDENT_TYPES.SPACE; + let indentSize = 4; + if (options.length) { + if (options[0] === INDENT_TYPES.TAB) { + indentType = INDENT_TYPES.TAB; + } else { + indentSize = options[0]; + } + } + const indentChar = + indentType === INDENT_TYPES.SPACE ? " ".repeat(indentSize) : "\t"; + return { indentType, indentSize, indentChar }; +} diff --git a/packages/eslint-plugin/lib/rules/utils/node.js b/packages/eslint-plugin/lib/rules/utils/node.js index 1d178a9d..b3aa6088 100644 --- a/packages/eslint-plugin/lib/rules/utils/node.js +++ b/packages/eslint-plugin/lib/rules/utils/node.js @@ -176,6 +176,14 @@ function isText(node) { return node.type === NODE_TYPES.Text; } +/** + * @param {AnyNode | LineNode} node + * @returns {node is LineNode} + */ +function isLine(node) { + return node.type === "Line"; +} + const lineBreakPattern = /\r\n|[\r\n\u2028\u2029]/u; const lineEndingPattern = new RegExp(lineBreakPattern.source, "gu"); /** @@ -214,6 +222,7 @@ module.exports = { isTag, isComment, isText, + isLine, isOverlapWithTemplates, codeToLines, isRangesOverlap, diff --git a/packages/eslint-plugin/lib/types.d.ts b/packages/eslint-plugin/lib/types.d.ts index 27265cac..7ff7fe5a 100644 --- a/packages/eslint-plugin/lib/types.d.ts +++ b/packages/eslint-plugin/lib/types.d.ts @@ -5,7 +5,11 @@ import * as ESHtml from "es-html-parser"; type Fix = ESLint.Rule.Fix; type Token = ESLint.AST.Token; -type AnyNode = ESHtml.AnyNode | LineNode; +type WithParent = N & { + parent: null | WithParent; +}; + +export type AnyNode = WithParent; export type Range = ESLint.AST.Range; @@ -27,16 +31,7 @@ interface TextNode extends ESHtml.TextNode { parent: TagNode; } -export interface TagNode extends ESHtml.TagNode { - attributes: AttributeNode[]; - parent: TagNode | HTMLNode; - openStart: OpenTagStartNode; - openEnd: OpenTagEndNode; - close: CloseTagNode; - children: Array< - TextNode | TagNode | ScriptTagNode | StyleTagNode | CommentNode - >; -} +export type TagNode = WithParent; interface OpenTagStartNode extends ESHtml.OpenTagStartNode { parent: TagNode; diff --git a/packages/eslint-plugin/tests/rules/indent.test.js b/packages/eslint-plugin/tests/rules/indent.test.js index 264add81..93741b10 100644 --- a/packages/eslint-plugin/tests/rules/indent.test.js +++ b/packages/eslint-plugin/tests/rules/indent.test.js @@ -337,6 +337,43 @@ function createTests() { }, ], }, + { + code: ` + + + + + + + `, + options: [ + 2, + { + tagChildrenIndent: { + html: 0, + }, + }, + ], + }, + { + code: ` + + +
+ text + + + `, + options: [ + 2, + { + tagChildrenIndent: { + html: 0, + }, + }, + ], + }, ], invalid: [ { @@ -993,6 +1030,29 @@ id="bar" `, }, + { + code: ` + + + + + `, + output: ` + + + + + `, + errors: wrongIndentErrors(1), + options: [ + 2, + { + tagChildrenIndent: { + html: 0, + }, + }, + ], + }, ], }; } @@ -1043,6 +1103,27 @@ return "
" } `, }, + { + code: ` +const code = html\` + + + + + + + +\`; + `, + options: [ + 2, + { + tagChildrenIndent: { + html: 0, + }, + }, + ], + }, ], invalid: [ { diff --git a/packages/template-parser/lib/template-parser.js b/packages/template-parser/lib/template-parser.js index 39568482..77c60afd 100644 --- a/packages/template-parser/lib/template-parser.js +++ b/packages/template-parser/lib/template-parser.js @@ -63,7 +63,7 @@ function parse(node, sourceCode, visitors) { }, }, }); - traverse(ast, visitors); + traverse(ast, visitors, null); return { ast, html, tokens }; } diff --git a/packages/template-parser/lib/traverser.js b/packages/template-parser/lib/traverser.js index c4243a1b..01398b23 100644 --- a/packages/template-parser/lib/traverser.js +++ b/packages/template-parser/lib/traverser.js @@ -56,18 +56,20 @@ const visitorKeys = { * * @param {AnyNode} node * @param {TemplateHTMLVisitor} visitors + * @param {any} parent */ -function traverse(node, visitors) { +function traverse(node, visitors, parent) { const enterVisitor = visitors[node.type]; + node.parent = parent; enterVisitor && enterVisitor(node); const nextKeys = visitorKeys[node.type]; nextKeys.forEach((key) => { const next = node[key]; if (Array.isArray(next)) { - next.forEach((n) => traverse(n, visitors)); + next.forEach((n) => traverse(n, visitors, node)); } else if (next) { - traverse(next, visitors); + traverse(next, visitors, node); } }); const exitVisitor = visitors[`${node.type}:exit`];