diff --git a/docs/rules/indent.md b/docs/rules/indent.md index 7e9bde25..8c35129a 100644 --- a/docs/rules/indent.md +++ b/docs/rules/indent.md @@ -131,3 +131,7 @@ This rule has an object option: - `tagChildrenIndent` (default: `{}`): Specifies the indent increment of the child tags of the specified tag. e.g. For example, `"tagChildrenIndent": { "html": 0 }` will set the `` tag children to 0 indent (2 x 0). - `ignoreComment` (default: `false`): When set to `true`, the indentation of HTML comments (including opening ``, and content) will not be checked. This is useful when you want to allow free-form indentation for comments. + +- `templateIndentBase` (default: `"templateTag"`): Controls the indentation base for HTML in template literals. + - `"templateTag"`: Uses the indentation of the template tag (e.g., `html\`...\``) as the base indentation. + - `"first"`: Uses the first element of the template literal as the base indentation (elements on the same line as the template tag are ignored). diff --git a/packages/eslint-plugin/lib/rules/indent/indent.js b/packages/eslint-plugin/lib/rules/indent/indent.js index 0f8ff3d5..ba76801a 100644 --- a/packages/eslint-plugin/lib/rules/indent/indent.js +++ b/packages/eslint-plugin/lib/rules/indent/indent.js @@ -19,6 +19,7 @@ * @property {number} [Option2.Attribute] * @property {Record} [Option2.tagChildrenIndent] * @property {boolean} [Option2.ignoreComment] + * @property {"first" | "templateTag"} [Option2.templateIndentBase] */ const { parseTemplateLiteral } = require("../utils/template-literal"); @@ -102,6 +103,11 @@ module.exports = { type: "boolean", default: false, }, + templateIndentBase: { + type: "string", + enum: ["first", "templateTag"], + default: "templateTag", + }, }, additionalProperties: false, }, @@ -116,6 +122,7 @@ module.exports = { const indentLevelOptions = (context.options && context.options[1]) || {}; const lines = sourceCode.getLines(); const ignoreComment = indentLevelOptions.ignoreComment === true; + const autoBaseIndent = indentLevelOptions.templateIndentBase === "first"; const { indentType, indentSize, indentChar } = getIndentOptionInfo(context); /** @@ -161,12 +168,35 @@ module.exports = { return 1; } + /** + * @param {TemplateLiteral} node + * @returns {number} + */ + function getAutoBaseSpaces(node) { + if (!autoBaseIndent) { + return 0; + } + const startLineIndex = node.loc.start.line; + const endLineIndex = node.loc.end.line - 1; + const templateLines = lines.slice(startLineIndex, endLineIndex); + for (let i = 0; i < templateLines.length; i++) { + const line = templateLines[i]; + if (line.trim()) { + return countLeftPadding(line); + } + } + return 0; + } + /** * * @param {TemplateLiteral} node * @returns {number} */ function getTemplateLiteralBaseIndentLevel(node) { + if (autoBaseIndent) { + return 0; + } // @ts-ignore const lineIndex = node.loc.start.line - 1; const line = lines[lineIndex]; @@ -181,8 +211,9 @@ module.exports = { /** * @param {number} baseLevel + * @param {number} baseSpaces */ - function createIndentVisitor(baseLevel) { + function createIndentVisitor(baseLevel, baseSpaces) { const indentLevel = new IndentLevel({ getIncreasingLevel, }); @@ -210,7 +241,14 @@ module.exports = { * @returns {string} */ function getExpectedIndent() { - return indentChar.repeat(indentLevel.value()); + let base = ""; + if (indentType === "space") { + base = " ".repeat(baseSpaces); + } else { + base = indentChar.repeat(baseSpaces); + } + + return base + indentChar.repeat(indentLevel.value()); } /** @@ -396,24 +434,26 @@ module.exports = { } return { - ...createIndentVisitor(0), + ...createIndentVisitor(0, 0), TaggedTemplateExpression(node) { if (shouldCheckTaggedTemplateExpression(node, context)) { const base = getTemplateLiteralBaseIndentLevel(node.quasi); + const baseSpaces = getAutoBaseSpaces(node.quasi); parseTemplateLiteral( node.quasi, getSourceCode(context), - createIndentVisitor(base) + createIndentVisitor(base, baseSpaces) ); } }, TemplateLiteral(node) { if (shouldCheckTemplateLiteral(node, context)) { const base = getTemplateLiteralBaseIndentLevel(node); + const baseSpaces = getAutoBaseSpaces(node); parseTemplateLiteral( node, getSourceCode(context), - createIndentVisitor(base) + createIndentVisitor(base, baseSpaces) ); } }, diff --git a/packages/eslint-plugin/tests/rules/indent.test.js b/packages/eslint-plugin/tests/rules/indent.test.js index ce8f6fab..cdfb9abb 100644 --- a/packages/eslint-plugin/tests/rules/indent.test.js +++ b/packages/eslint-plugin/tests/rules/indent.test.js @@ -1470,6 +1470,23 @@ comment ], errors: wrongIndentErrors(2), }, + { + code: ` +
+
+ `, + output: ` +
+
+ `, + options: [ + 2, + { + templateIndentBase: "first", + }, + ], + errors: wrongIndentErrors(1), + }, ], }; } @@ -1566,6 +1583,51 @@ const code = html\` }, ], }, + { + code: `html\` + test + \``, + options: [ + 2, + { + templateIndentBase: "first", + }, + ], + }, + { + code: `html\` +
+ test +
+ \``, + options: [ + 2, + { + templateIndentBase: "first", + }, + ], + }, + { + code: `html\`
+
+
a
+ \``, + options: [ + 2, + { + templateIndentBase: "first", + }, + ], + }, + { + code: `html\`\`;`, + options: [ + 2, + { + templateIndentBase: "first", + }, + ], + }, ], invalid: [ { @@ -1896,5 +1958,55 @@ class Component extends LitElement { options: ["tab", { Attribute: 2, tagChildrenIndent: { span: 2 } }], errors: wrongIndentErrors(1), }, + { + code: ` +const code = html\` +
+ id="\${bar}"> +
\`; + `, + output: ` +const code = html\` +
+ id="\${bar}"> +
\`; + `, + options: [4, { templateIndentBase: "first" }], + errors: wrongIndentErrors(1), + }, + { + code: ` +const code = html\` +
+
\`; + `, + output: ` +const code = html\` +
+
\`; + `, + options: [4, { templateIndentBase: "first" }], + errors: wrongIndentErrors(1), + }, + { + code: ` +const code = html\` +\t\t\t
+\t\t\t
+\`; + `, + output: ` +const code = html\` +\t\t\t
+\t\t\t
+\t\t\t\`; + `, + options: ["tab", { templateIndentBase: "first" }], + errors: wrongIndentErrors(2), + }, ], });