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),
+ },
],
});