Repository: ony3000/prettier-plugin-classnames (ts)

Issue:
Proposal: minAttributeWidth setting
It looks like the plugin uses `printWidth` to wrap, regardless of the attribute column start. `endingPosition` is used to align the wrapped portion of the text: indented under the attribute col start in the case of `relative` and a col 0 in the case of absolute.

In the case of `relative`, this leads to long lines as the break point occurs at attribute start + print width. In the case of `absolute` this leads to non-indented blocks of text.

IMO both are problematic. Either you have longer-than-expected lines or you have non-indented blocks.

If you introduce a config setting like `minAttributeWidth` the user could specify a min width used to calculate break points. The `relative` setting (default) would take the max value of "print width minus attribute start" and the new "min attribute width" setting, which could default to a value of `printWidth` to preserve the current behavior.

If I have this wrong, please lmk. My goal for our code base is to respect `printWidth` (absolute behavior) and maintain indents on wrapping (relative behavior).

Great plugin btw! It's indispensable for Tailwind's brave new world :) 

Pull request:
Adds `absolute-with-indent`, which can be selected as an option to `endingPosition`
This PR closes #32.

See #26 for an explanation of `endingPosition`.

## How is it different from existing options?

### `absolute-with-indent`

Basically the same as the `absolute` option, but indentation respects the `relative` option.

Example output:

```
--------------------------------------------------| printWidth=50
export function Callout({ children }) {
  return (
    <div
      className="bg-gray-100/50 border
        border-zinc-400/30 dark:bg-neutral-900/50
        dark:border-neutral-500/30 px-4 py-4
        rounded-xl"
    >
      {children}
    </div>
  );
}
```

```
--------------------------------------------------| printWidth=50
export function Callout({ children }) {
  return (
    <div
      className={classNames(
        `bg-gray-100/50 border border-zinc-400/30
        dark:bg-neutral-900/50
        dark:border-neutral-500/30 px-4 py-4
        rounded-xl`,
      )}
    >
      {children}
    </div>
  );
}
```

Patch:
diff --git a/cspell.json b/cspell.json
index 04731ab..32d5cc4 100644
--- a/cspell.json
+++ b/cspell.json
@@ -1,5 +1,5 @@
 {
   "version": "0.2",
   "language": "en",
-  "words": ["slsl", "slto", "tlop", "tlto", "tlpq", "quasis", "astro"]
+  "words": ["slsl", "slto", "tlsl", "tlop", "tlto", "tlpq", "quasis", "astro"]
 }
diff --git a/src/packages/core-parts/finder.ts b/src/packages/core-parts/finder.ts
index 9d85193..9d87db1 100644
--- a/src/packages/core-parts/finder.ts
+++ b/src/packages/core-parts/finder.ts
@@ -209,22 +209,40 @@ export function findTargetClassNameNodes(
       case 'JSXExpressionContainer': {
         nonCommentNodes.push(currentASTNode);
 
-        classNameNodes.forEach((classNameNode) => {
-          const [classNameRangeStart, classNameRangeEnd] = classNameNode.range;
+        if (
+          isTypeof(
+            node,
+            z.object({
+              loc: z.object({
+                start: z.object({
+                  line: z.number(),
+                }),
+              }),
+            }),
+          )
+        ) {
+          const currentNodeStartLineIndex = node.loc.start.line - 1;
 
-          if (
-            currentNodeRangeStart <= classNameRangeStart &&
-            classNameRangeEnd <= currentNodeRangeEnd
-          ) {
-            if (classNameNode.type === ClassNameType.USL) {
-              // eslint-disable-next-line no-param-reassign
-              classNameNode.type = ClassNameType.CSL;
-            } else if (classNameNode.type === ClassNameType.UTL) {
-              // eslint-disable-next-line no-param-reassign
-              classNameNode.type = ClassNameType.CTL;
+          classNameNodes.forEach((classNameNode) => {
+            const [classNameRangeStart, classNameRangeEnd] = classNameNode.range;
+
+            if (
+              currentNodeRangeStart <= classNameRangeStart &&
+              classNameRangeEnd <= currentNodeRangeEnd
+            ) {
+              if (classNameNode.type === ClassNameType.USL) {
+                // eslint-disable-next-line no-param-reassign
+                classNameNode.type = ClassNameType.CSL;
+              } else if (classNameNode.type === ClassNameType.UTL) {
+                // eslint-disable-next-line no-param-reassign
+                classNameNode.type =
+                  classNameNode.startLineIndex === currentNodeStartLineIndex
+                    ? ClassNameType.TLSL
+                    : ClassNameType.CTL;
+              }
             }
-          }
-        });
+          });
+        }
         break;
       }
       case 'ObjectProperty':
diff --git a/src/packages/core-parts/index.ts b/src/packages/core-parts/index.ts
index 5ecbf74..bb3b20d 100644
--- a/src/packages/core-parts/index.ts
+++ b/src/packages/core-parts/index.ts
@@ -40,7 +40,7 @@ function getExtraIndentLevel(type: ClassNameType) {
       ClassNameType.AOL,
       ClassNameType.SLSL,
       ClassNameType.SLTO,
-      ClassNameType.CTL,
+      ClassNameType.TLSL,
       ClassNameType.TLTO,
     ].includes(type)
   ) {
@@ -66,6 +66,7 @@ function getSomeKindOfQuotes(
           ClassNameType.FA,
           ClassNameType.SLSL,
           ClassNameType.CTL,
+          ClassNameType.TLSL,
           ClassNameType.TLOP,
           ClassNameType.TLTO,
         ].includes(type)
@@ -123,9 +124,19 @@ function replaceClassName({
         return formattedPrevText;
       }
 
+      const isStartingPositionRelative = options.endingPosition !== 'absolute';
+      const isEndingPositionAbsolute = options.endingPosition !== 'relative';
+      const isOutputIdeal = isStartingPositionRelative && isEndingPositionAbsolute;
+
+      const { indentLevel: baseIndentLevel } = lineNodes[startLineIndex];
+      const extraIndentLevel = getExtraIndentLevel(type);
+      const multiLineIndentLevel = isStartingPositionRelative
+        ? baseIndentLevel + extraIndentLevel
+        : 0;
+
       const classNameBase = formattedPrevText.slice(rangeStart + 1, rangeEnd - 1);
 
-      // preprocess (1)
+      // preprocess (first)
       const [leadingSpace, classNameWithoutSpacesAtBothEnds, trailingSpace] =
         replaceSpacesAtBothEnds(classNameBase);
 
@@ -133,38 +144,64 @@ function replaceClassName({
         .split(EOL)
         .slice(0, startLineIndex)
         .reduce((textLength, line) => textLength + line.length + EOL.length, 0);
-      const padLength = rangeStart + 1 - totalTextLengthUptoPrevLine;
-
-      // preprocess (2)
-      const classNameWithPadding = `${PH.repeat(
-        options.endingPosition === 'absolute' ? padLength : 0,
+      const firstLinePadLength =
+        rangeStart +
+        1 -
+        totalTextLengthUptoPrevLine +
+        (indentUnit === '\t' ? 3 : 0) * baseIndentLevel;
+
+      // preprocess (first+1)
+      const classNameWithFirstLinePadding = `${PH.repeat(
+        isEndingPositionAbsolute ? firstLinePadLength : 0,
       )}${classNameWithoutSpacesAtBothEnds}`;
 
-      const formattedClassName = format(classNameWithPadding, {
-        ...options,
-        parser: 'html',
-        plugins: [],
-        rangeStart: 0,
-        rangeEnd: Infinity,
-        endOfLine: 'lf',
-      }).trimEnd();
+      const formattedClassName = ((cn: string) => {
+        const formatted = format(cn, {
+          ...options,
+          parser: 'html',
+          plugins: [],
+          rangeStart: 0,
+          rangeEnd: Infinity,
+          endOfLine: 'lf',
+        }).trimEnd();
+
+        if (!isOutputIdeal) {
+          return formatted;
+        }
+
+        const [firstLine, ...rest] = formatted.split(EOL);
+
+        if (rest.length === 0) {
+          return firstLine;
+        }
+
+        const multiLinePadLength =
+          (indentUnit === '\t' ? 4 : indentUnit.length) * multiLineIndentLevel;
+
+        const formattedRest = format(rest.join(EOL), {
+          ...options,
+          parser: 'html',
+          plugins: [],
+          rangeStart: 0,
+          rangeEnd: Infinity,
+          endOfLine: 'lf',
+          printWidth: options.printWidth - multiLinePadLength,
+        }).trimEnd();
+
+        return `${firstLine}${EOL}${formattedRest}`;
+      })(classNameWithFirstLinePadding);
 
-      // postprocess (1)
+      // postprocess (last-1)
       const classNameWithoutPadding = formattedClassName.slice(
-        options.endingPosition === 'absolute' ? padLength : 0,
+        isEndingPositionAbsolute ? firstLinePadLength : 0,
       );
 
-      // postprocess (2)
+      // postprocess (last)
       const classNameWithOriginalSpaces = `${leadingSpace}${classNameWithoutPadding.slice(
         leadingSpace.length,
         -trailingSpace.length || undefined,
       )}${trailingSpace}`;
 
-      const { indentLevel: baseIndentLevel } = lineNodes[startLineIndex];
-      const extraIndentLevel = getExtraIndentLevel(type);
-      const multiLineIndentLevel =
-        options.endingPosition === 'absolute' ? 0 : baseIndentLevel + extraIndentLevel;
-
       const isMultiLineClassName = classNameWithOriginalSpaces.split(EOL).length > 1;
       const [quoteStart, quoteEnd] = getSomeKindOfQuotes(
         type,
@@ -261,9 +298,20 @@ async function replaceClassNameAsync({
       }
 
       const formattedPrevText = await formattedPrevTextPromise;
+
+      const isStartingPositionRelative = options.endingPosition !== 'absolute';
+      const isEndingPositionAbsolute = options.endingPosition !== 'relative';
+      const isOutputIdeal = isStartingPositionRelative && isEndingPositionAbsolute;
+
+      const { indentLevel: baseIndentLevel } = lineNodes[startLineIndex];
+      const extraIndentLevel = getExtraIndentLevel(type);
+      const multiLineIndentLevel = isStartingPositionRelative
+        ? baseIndentLevel + extraIndentLevel
+        : 0;
+
       const classNameBase = formattedPrevText.slice(rangeStart + 1, rangeEnd - 1);
 
-      // preprocess (1)
+      // preprocess (first)
       const [leadingSpace, classNameWithoutSpacesAtBothEnds, trailingSpace] =
         replaceSpacesAtBothEnds(classNameBase);
 
@@ -271,40 +319,68 @@ async function replaceClassNameAsync({
         .split(EOL)
         .slice(0, startLineIndex)
         .reduce((textLength, line) => textLength + line.length + EOL.length, 0);
-      const padLength = rangeStart + 1 - totalTextLengthUptoPrevLine;
-
-      // preprocess (2)
-      const classNameWithPadding = `${PH.repeat(
-        options.endingPosition === 'absolute' ? padLength : 0,
+      const firstLinePadLength =
+        rangeStart +
+        1 -
+        totalTextLengthUptoPrevLine +
+        (indentUnit === '\t' ? 3 : 0) * baseIndentLevel;
+
+      // preprocess (first+1)
+      const classNameWithFirstLinePadding = `${PH.repeat(
+        isEndingPositionAbsolute ? firstLinePadLength : 0,
       )}${classNameWithoutSpacesAtBothEnds}`;
 
-      const formattedClassName = (
-        await format(classNameWithPadding, {
-          ...options,
-          parser: 'html',
-          plugins: [],
-          rangeStart: 0,
-          rangeEnd: Infinity,
-          endOfLine: 'lf',
-        })
-      ).trimEnd();
-
-      // postprocess (1)
+      const formattedClassName = await (async (cn: string) => {
+        const formatted = (
+          await format(cn, {
+            ...options,
+            parser: 'html',
+            plugins: [],
+            rangeStart: 0,
+            rangeEnd: Infinity,
+            endOfLine: 'lf',
+          })
+        ).trimEnd();
+
+        if (!isOutputIdeal) {
+          return formatted;
+        }
+
+        const [firstLine, ...rest] = formatted.split(EOL);
+
+        if (rest.length === 0) {
+          return firstLine;
+        }
+
+        const multiLinePadLength =
+          (indentUnit === '\t' ? 4 : indentUnit.length) * multiLineIndentLevel;
+
+        const formattedRest = (
+          await format(rest.join(EOL), {
+            ...options,
+            parser: 'html',
+            plugins: [],
+            rangeStart: 0,
+            rangeEnd: Infinity,
+            endOfLine: 'lf',
+            printWidth: options.printWidth - multiLinePadLength,
+          })
+        ).trimEnd();
+
+        return `${firstLine}${EOL}${formattedRest}`;
+      })(classNameWithFirstLinePadding);
+
+      // postprocess (last-1)
       const classNameWithoutPadding = formattedClassName.slice(
-        options.endingPosition === 'absolute' ? padLength : 0,
+        isEndingPositionAbsolute ? firstLinePadLength : 0,
       );
 
-      // postprocess (2)
+      // postprocess (last)
       const classNameWithOriginalSpaces = `${leadingSpace}${classNameWithoutPadding.slice(
         leadingSpace.length,
         -trailingSpace.length || undefined,
       )}${trailingSpace}`;
 
-      const { indentLevel: baseIndentLevel } = lineNodes[startLineIndex];
-      const extraIndentLevel = getExtraIndentLevel(type);
-      const multiLineIndentLevel =
-        options.endingPosition === 'absolute' ? 0 : baseIndentLevel + extraIndentLevel;
-
       const isMultiLineClassName = classNameWithOriginalSpaces.split(EOL).length > 1;
       const [quoteStart, quoteEnd] = getSomeKindOfQuotes(
         type,
diff --git a/src/packages/core-parts/shared.ts b/src/packages/core-parts/shared.ts
index a9ae358..e122945 100644
--- a/src/packages/core-parts/shared.ts
+++ b/src/packages/core-parts/shared.ts
@@ -31,6 +31,10 @@ export enum ClassNameType {
    * Common template literal
    */
   CTL,
+  /**
+   * Template literal starting on the same line as the attribute
+   */
+  TLSL,
   /**
    * Template literal as object property
    */
@@ -64,11 +68,12 @@ export type ClassNameNode = {
 };
 
 export type NarrowedParserOptions = {
+  printWidth: number;
   tabWidth: number;
   useTabs: boolean;
   singleQuote: boolean;
   parser: string;
   customAttributes: string[];
   customFunctions: string[];
-  endingPosition: 'relative' | 'absolute';
+  endingPosition: 'relative' | 'absolute' | 'absolute-with-indent';
 };
diff --git a/src/packages/v2-plugin/options.ts b/src/packages/v2-plugin/options.ts
index 3acc683..a38be65 100644
--- a/src/packages/v2-plugin/options.ts
+++ b/src/packages/v2-plugin/options.ts
@@ -35,6 +35,12 @@ export const options: SupportOptions = {
         value: 'absolute',
         description: 'Each line ends at a `printWidth` distance from the start of the line.',
       },
+      {
+        since: '0.6.0',
+        value: 'absolute-with-indent',
+        description:
+          'Basically the same as the `absolute` option, but indentation respects the `relative` option.',
+      },
     ],
   },
 };
diff --git a/src/packages/v2-plugin/parsers.ts b/src/packages/v2-plugin/parsers.ts
index de46cfc..02fa237 100644
--- a/src/packages/v2-plugin/parsers.ts
+++ b/src/packages/v2-plugin/parsers.ts
@@ -75,31 +75,27 @@ function transformParser(
         );
       }
 
-      if (parserName === 'vue' || parserName === 'astro') {
-        const secondAst = defaultParser.parse(
-          secondFormattedText,
-          { [parserName]: defaultParser },
-          options,
-        );
-        const classNameSecondWrappedText = parseLineByLineAndReplace({
-          formattedText: secondFormattedText,
-          ast: secondAst,
-          // @ts-ignore
-          options,
-          format,
-          addon,
-          targetClassNameTypes: [ClassNameType.ASL, ClassNameType.AOL],
-        });
-
-        return {
-          type: 'FormattedText',
-          body: classNameSecondWrappedText,
-        };
-      }
+      const secondAst = defaultParser.parse(
+        secondFormattedText,
+        { [parserName]: defaultParser },
+        options,
+      );
+      const classNameSecondWrappedText = parseLineByLineAndReplace({
+        formattedText: secondFormattedText,
+        ast: secondAst,
+        // @ts-ignore
+        options,
+        format,
+        addon,
+        targetClassNameTypes:
+          parserName === 'vue' || parserName === 'astro'
+            ? [ClassNameType.ASL, ClassNameType.AOL]
+            : [ClassNameType.CTL, ClassNameType.TLSL],
+      });
 
       return {
         type: 'FormattedText',
-        body: secondFormattedText,
+        body: classNameSecondWrappedText,
       };
     },
     astFormat: 'classnames-ast',
diff --git a/src/packages/v3-plugin/options.ts b/src/packages/v3-plugin/options.ts
index 244caba..f715d26 100644
--- a/src/packages/v3-plugin/options.ts
+++ b/src/packages/v3-plugin/options.ts
@@ -38,6 +38,12 @@ export const options: SupportOptions = {
         value: 'absolute',
         description: 'Each line ends at a `printWidth` distance from the start of the line.',
       },
+      {
+        since: '0.6.0',
+        value: 'absolute-with-indent',
+        description:
+          'Basically the same as the `absolute` option, but indentation respects the `relative` option.',
+      },
     ],
   },
 };
diff --git a/src/packages/v3-plugin/parsers.ts b/src/packages/v3-plugin/parsers.ts
index f3a6e18..a175f6a 100644
--- a/src/packages/v3-plugin/parsers.ts
+++ b/src/packages/v3-plugin/parsers.ts
@@ -74,27 +74,23 @@ function transformParser(
         );
       }
 
-      if (parserName === 'vue' || parserName === 'astro') {
-        const secondAst = await defaultParser.parse(secondFormattedText, options);
-        const classNameSecondWrappedText = await parseLineByLineAndReplaceAsync({
-          formattedText: secondFormattedText,
-          ast: secondAst,
-          // @ts-ignore
-          options,
-          format,
-          addon,
-          targetClassNameTypes: [ClassNameType.ASL, ClassNameType.AOL],
-        });
-
-        return {
-          type: 'FormattedText',
-          body: classNameSecondWrappedText,
-        };
-      }
+      const secondAst = await defaultParser.parse(secondFormattedText, options);
+      const classNameSecondWrappedText = await parseLineByLineAndReplaceAsync({
+        formattedText: secondFormattedText,
+        ast: secondAst,
+        // @ts-ignore
+        options,
+        format,
+        addon,
+        targetClassNameTypes:
+          parserName === 'vue' || parserName === 'astro'
+            ? [ClassNameType.ASL, ClassNameType.AOL]
+            : [ClassNameType.CTL, ClassNameType.TLSL],
+      });
 
       return {
         type: 'FormattedText',
-        body: secondFormattedText,
+        body: classNameSecondWrappedText,
       };
     },
     astFormat: 'classnames-ast',


Test patch:
diff --git a/tests/v2-test/astro/attribute-without-expression.test.ts b/tests/v2-test/astro/attribute-without-expression.test.ts
index 36bae34..38fd833 100644
--- a/tests/v2-test/astro/attribute-without-expression.test.ts
+++ b/tests/v2-test/astro/attribute-without-expression.test.ts
@@ -120,7 +120,7 @@ const fixtures: Fixture[] = [
 `,
   },
   {
-    name: 'ending position',
+    name: 'ending position (1)',
     input: `
 <div class="rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50">
   <slot />
@@ -137,6 +137,26 @@ dark:border-neutral-500/30 dark:bg-neutral-900/50"
       endingPosition: 'absolute',
     },
   },
+  {
+    name: 'ending position (2)',
+    input: `
+<div class="rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50">
+  <slot />
+</div>
+`,
+    output: `<div
+  class="rounded-xl border border-zinc-400/30 bg-gray-100/50
+    px-4 py-4 dark:border-neutral-500/30
+    dark:bg-neutral-900/50"
+>
+  <slot />
+</div>
+`,
+    options: {
+      printWidth: 60,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
 ];
 
 describe('astro/attribute-without-expression', () => {
diff --git a/tests/v2-test/astro/expression.test.ts b/tests/v2-test/astro/expression.test.ts
index fbd5c54..45b22f6 100644
--- a/tests/v2-test/astro/expression.test.ts
+++ b/tests/v2-test/astro/expression.test.ts
@@ -368,6 +368,46 @@ dark:border-neutral-500/30 dark:bg-neutral-900/50\`]: true,
       endingPosition: 'absolute',
     },
   },
+  {
+    name: 'ending position (4)',
+    input: `
+<div class={'rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50'}>
+  <slot />
+</div>
+`,
+    output: `<div
+  class={\`rounded-xl border border-zinc-400/30
+    bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30
+    dark:bg-neutral-900/50\`}
+>
+  <slot />
+</div>
+`,
+    options: {
+      printWidth: 60,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
+  {
+    name: 'ending position (5) - useTabs: true',
+    input: `
+<div class={'rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50'}>
+  <slot />
+</div>
+`,
+    output: `<div
+\tclass={\`rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4
+\t\tpy-4 dark:border-neutral-500/30 dark:bg-neutral-900/50\`}
+>
+\t<slot />
+</div>
+`,
+    options: {
+      printWidth: 70,
+      useTabs: true,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
 ];
 
 describe('astro/expression', () => {
diff --git a/tests/v2-test/babel/attribute-without-expression.test.ts b/tests/v2-test/babel/attribute-without-expression.test.ts
index 67c3ff6..f658a31 100644
--- a/tests/v2-test/babel/attribute-without-expression.test.ts
+++ b/tests/v2-test/babel/attribute-without-expression.test.ts
@@ -172,7 +172,7 @@ export function Callout({ children }) {
 `,
   },
   {
-    name: 'ending position',
+    name: 'ending position (1)',
     input: `
 export function Callout({ children }) {
   return (
@@ -197,6 +197,34 @@ py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50"
       endingPosition: 'absolute',
     },
   },
+  {
+    name: 'ending position (2)',
+    input: `
+export function Callout({ children }) {
+  return (
+    <div className="rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50">
+      {children}
+    </div>
+  );
+}
+`,
+    output: `export function Callout({ children }) {
+  return (
+    <div
+      className="rounded-xl border border-zinc-400/30
+        bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30
+        dark:bg-neutral-900/50"
+    >
+      {children}
+    </div>
+  );
+}
+`,
+    options: {
+      printWidth: 60,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
 ];
 
 describe('babel/attribute-without-expression', () => {
diff --git a/tests/v2-test/babel/expression.test.ts b/tests/v2-test/babel/expression.test.ts
index 220663b..5b9ff36 100644
--- a/tests/v2-test/babel/expression.test.ts
+++ b/tests/v2-test/babel/expression.test.ts
@@ -632,6 +632,61 @@ dark:border-neutral-500/30 dark:bg-neutral-900/50\`]: true,
       endingPosition: 'absolute',
     },
   },
+  {
+    name: 'ending position (4)',
+    input: `
+export function Callout({ children }) {
+  return (
+    <div className={'rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50'}>
+      {children}
+    </div>
+  );
+}
+`,
+    output: `export function Callout({ children }) {
+  return (
+    <div
+      className={\`rounded-xl border border-zinc-400/30
+        bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30
+        dark:bg-neutral-900/50\`}
+    >
+      {children}
+    </div>
+  );
+}
+`,
+    options: {
+      printWidth: 60,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
+  {
+    name: 'ending position (5) - useTabs: true',
+    input: `
+export function Callout({ children }) {
+  return (
+    <div className={'rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50'}>
+      {children}
+    </div>
+  );
+}
+`,
+    output: `export function Callout({ children }) {
+\treturn (
+\t\t<div
+\t\t\tclassName={\`rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4
+\t\t\t\tpy-4 dark:border-neutral-500/30 dark:bg-neutral-900/50\`}
+\t\t>
+\t\t\t{children}
+\t\t</div>
+\t);
+}
+`,
+    options: {
+      useTabs: true,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
   {
     name: 'issue #27 (1) - template literal ending with a space',
     input: `
diff --git a/tests/v2-test/typescript/attribute-without-expression.test.ts b/tests/v2-test/typescript/attribute-without-expression.test.ts
index 646c8e0..1aaa810 100644
--- a/tests/v2-test/typescript/attribute-without-expression.test.ts
+++ b/tests/v2-test/typescript/attribute-without-expression.test.ts
@@ -172,7 +172,7 @@ export function Callout({ children }) {
 `,
   },
   {
-    name: 'ending position',
+    name: 'ending position (1)',
     input: `
 export function Callout({ children }) {
   return (
@@ -197,6 +197,34 @@ py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50"
       endingPosition: 'absolute',
     },
   },
+  {
+    name: 'ending position (2)',
+    input: `
+export function Callout({ children }) {
+  return (
+    <div className="rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50">
+      {children}
+    </div>
+  );
+}
+`,
+    output: `export function Callout({ children }) {
+  return (
+    <div
+      className="rounded-xl border border-zinc-400/30
+        bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30
+        dark:bg-neutral-900/50"
+    >
+      {children}
+    </div>
+  );
+}
+`,
+    options: {
+      printWidth: 60,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
 ];
 
 describe('typescript/attribute-without-expression', () => {
diff --git a/tests/v2-test/typescript/expression.test.ts b/tests/v2-test/typescript/expression.test.ts
index 2ff3534..35a16a6 100644
--- a/tests/v2-test/typescript/expression.test.ts
+++ b/tests/v2-test/typescript/expression.test.ts
@@ -632,6 +632,61 @@ dark:border-neutral-500/30 dark:bg-neutral-900/50\`]: true,
       endingPosition: 'absolute',
     },
   },
+  {
+    name: 'ending position (4)',
+    input: `
+export function Callout({ children }) {
+  return (
+    <div className={'rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50'}>
+      {children}
+    </div>
+  );
+}
+`,
+    output: `export function Callout({ children }) {
+  return (
+    <div
+      className={\`rounded-xl border border-zinc-400/30
+        bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30
+        dark:bg-neutral-900/50\`}
+    >
+      {children}
+    </div>
+  );
+}
+`,
+    options: {
+      printWidth: 60,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
+  {
+    name: 'ending position (5) - useTabs: true',
+    input: `
+export function Callout({ children }) {
+  return (
+    <div className={'rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50'}>
+      {children}
+    </div>
+  );
+}
+`,
+    output: `export function Callout({ children }) {
+\treturn (
+\t\t<div
+\t\t\tclassName={\`rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4
+\t\t\t\tpy-4 dark:border-neutral-500/30 dark:bg-neutral-900/50\`}
+\t\t>
+\t\t\t{children}
+\t\t</div>
+\t);
+}
+`,
+    options: {
+      useTabs: true,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
   {
     name: 'issue #27 (1) - template literal ending with a space',
     input: `
diff --git a/tests/v2-test/vue/attribute-without-expression.test.ts b/tests/v2-test/vue/attribute-without-expression.test.ts
index b5da164..2661430 100644
--- a/tests/v2-test/vue/attribute-without-expression.test.ts
+++ b/tests/v2-test/vue/attribute-without-expression.test.ts
@@ -152,7 +152,7 @@ const fixtures: Fixture[] = [
 `,
   },
   {
-    name: 'ending position',
+    name: 'ending position (1)',
     input: `
 <template>
   <div class="rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50">
@@ -173,6 +173,30 @@ dark:border-neutral-500/30 dark:bg-neutral-900/50"
       endingPosition: 'absolute',
     },
   },
+  {
+    name: 'ending position (2)',
+    input: `
+<template>
+  <div class="rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50">
+    <slot></slot>
+  </div>
+</template>
+`,
+    output: `<template>
+  <div
+    class="rounded-xl border border-zinc-400/30
+      bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30
+      dark:bg-neutral-900/50"
+  >
+    <slot></slot>
+  </div>
+</template>
+`,
+    options: {
+      printWidth: 60,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
 ];
 
 describe('vue/attribute-without-expression', () => {
diff --git a/tests/v2-test/vue/expression.test.ts b/tests/v2-test/vue/expression.test.ts
index 3e76671..45b8388 100644
--- a/tests/v2-test/vue/expression.test.ts
+++ b/tests/v2-test/vue/expression.test.ts
@@ -456,6 +456,55 @@ dark:border-neutral-500/30 dark:bg-neutral-900/50\`]: true,
       endingPosition: 'absolute',
     },
   },
+  {
+    name: 'ending position (4)',
+    input: `
+<template>
+  <div :class="'rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50'">
+    <slot></slot>
+  </div>
+</template>
+`,
+    output: `<template>
+  <div
+    :class="\`rounded-xl border border-zinc-400/30
+      bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30
+      dark:bg-neutral-900/50\`"
+  >
+    <slot></slot>
+  </div>
+</template>
+`,
+    options: {
+      printWidth: 60,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
+  {
+    name: 'ending position (5) - useTabs: true',
+    input: `
+<template>
+  <div :class="'rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50'">
+    <slot></slot>
+  </div>
+</template>
+`,
+    output: `<template>
+\t<div
+\t\t:class="\`rounded-xl border border-zinc-400/30 bg-gray-100/50
+\t\t\tpx-4 py-4 dark:border-neutral-500/30
+\t\t\tdark:bg-neutral-900/50\`"
+\t>
+\t\t<slot></slot>
+\t</div>
+</template>
+`,
+    options: {
+      printWidth: 70,
+      useTabs: true,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
 ];
 
 describe('vue/expression', () => {
diff --git a/tests/v3-test/astro/attribute-without-expression.test.ts b/tests/v3-test/astro/attribute-without-expression.test.ts
index 25967eb..0966376 100644
--- a/tests/v3-test/astro/attribute-without-expression.test.ts
+++ b/tests/v3-test/astro/attribute-without-expression.test.ts
@@ -120,7 +120,7 @@ const fixtures: Fixture[] = [
 `,
   },
   {
-    name: 'ending position',
+    name: 'ending position (1)',
     input: `
 <div class="rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50">
   <slot />
@@ -137,6 +137,26 @@ dark:border-neutral-500/30 dark:bg-neutral-900/50"
       endingPosition: 'absolute',
     },
   },
+  {
+    name: 'ending position (2)',
+    input: `
+<div class="rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50">
+  <slot />
+</div>
+`,
+    output: `<div
+  class="rounded-xl border border-zinc-400/30 bg-gray-100/50
+    px-4 py-4 dark:border-neutral-500/30
+    dark:bg-neutral-900/50"
+>
+  <slot />
+</div>
+`,
+    options: {
+      printWidth: 60,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
 ];
 
 describe('astro/attribute-without-expression', () => {
diff --git a/tests/v3-test/astro/expression.test.ts b/tests/v3-test/astro/expression.test.ts
index 1330d36..a4d3951 100644
--- a/tests/v3-test/astro/expression.test.ts
+++ b/tests/v3-test/astro/expression.test.ts
@@ -368,6 +368,46 @@ dark:border-neutral-500/30 dark:bg-neutral-900/50\`]: true,
       endingPosition: 'absolute',
     },
   },
+  {
+    name: 'ending position (4)',
+    input: `
+<div class={'rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50'}>
+  <slot />
+</div>
+`,
+    output: `<div
+  class={\`rounded-xl border border-zinc-400/30
+    bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30
+    dark:bg-neutral-900/50\`}
+>
+  <slot />
+</div>
+`,
+    options: {
+      printWidth: 60,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
+  {
+    name: 'ending position (5) - useTabs: true',
+    input: `
+<div class={'rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50'}>
+  <slot />
+</div>
+`,
+    output: `<div
+\tclass={\`rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4
+\t\tpy-4 dark:border-neutral-500/30 dark:bg-neutral-900/50\`}
+>
+\t<slot />
+</div>
+`,
+    options: {
+      printWidth: 70,
+      useTabs: true,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
 ];
 
 describe('astro/expression', () => {
diff --git a/tests/v3-test/babel/attribute-without-expression.test.ts b/tests/v3-test/babel/attribute-without-expression.test.ts
index 8c1aed9..b67180d 100644
--- a/tests/v3-test/babel/attribute-without-expression.test.ts
+++ b/tests/v3-test/babel/attribute-without-expression.test.ts
@@ -172,7 +172,7 @@ export function Callout({ children }) {
 `,
   },
   {
-    name: 'ending position',
+    name: 'ending position (1)',
     input: `
 export function Callout({ children }) {
   return (
@@ -197,6 +197,34 @@ py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50"
       endingPosition: 'absolute',
     },
   },
+  {
+    name: 'ending position (2)',
+    input: `
+export function Callout({ children }) {
+  return (
+    <div className="rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50">
+      {children}
+    </div>
+  );
+}
+`,
+    output: `export function Callout({ children }) {
+  return (
+    <div
+      className="rounded-xl border border-zinc-400/30
+        bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30
+        dark:bg-neutral-900/50"
+    >
+      {children}
+    </div>
+  );
+}
+`,
+    options: {
+      printWidth: 60,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
 ];
 
 describe('babel/attribute-without-expression', () => {
diff --git a/tests/v3-test/babel/expression.test.ts b/tests/v3-test/babel/expression.test.ts
index f6fa0ee..484d286 100644
--- a/tests/v3-test/babel/expression.test.ts
+++ b/tests/v3-test/babel/expression.test.ts
@@ -632,6 +632,61 @@ dark:border-neutral-500/30 dark:bg-neutral-900/50\`]: true,
       endingPosition: 'absolute',
     },
   },
+  {
+    name: 'ending position (4)',
+    input: `
+export function Callout({ children }) {
+  return (
+    <div className={'rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50'}>
+      {children}
+    </div>
+  );
+}
+`,
+    output: `export function Callout({ children }) {
+  return (
+    <div
+      className={\`rounded-xl border border-zinc-400/30
+        bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30
+        dark:bg-neutral-900/50\`}
+    >
+      {children}
+    </div>
+  );
+}
+`,
+    options: {
+      printWidth: 60,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
+  {
+    name: 'ending position (5) - useTabs: true',
+    input: `
+export function Callout({ children }) {
+  return (
+    <div className={'rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50'}>
+      {children}
+    </div>
+  );
+}
+`,
+    output: `export function Callout({ children }) {
+\treturn (
+\t\t<div
+\t\t\tclassName={\`rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4
+\t\t\t\tpy-4 dark:border-neutral-500/30 dark:bg-neutral-900/50\`}
+\t\t>
+\t\t\t{children}
+\t\t</div>
+\t);
+}
+`,
+    options: {
+      useTabs: true,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
   {
     name: 'issue #27 (1) - template literal ending with a space',
     input: `
diff --git a/tests/v3-test/typescript/attribute-without-expression.test.ts b/tests/v3-test/typescript/attribute-without-expression.test.ts
index d25d0a4..3035d70 100644
--- a/tests/v3-test/typescript/attribute-without-expression.test.ts
+++ b/tests/v3-test/typescript/attribute-without-expression.test.ts
@@ -172,7 +172,7 @@ export function Callout({ children }) {
 `,
   },
   {
-    name: 'ending position',
+    name: 'ending position (1)',
     input: `
 export function Callout({ children }) {
   return (
@@ -197,6 +197,34 @@ py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50"
       endingPosition: 'absolute',
     },
   },
+  {
+    name: 'ending position (2)',
+    input: `
+export function Callout({ children }) {
+  return (
+    <div className="rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50">
+      {children}
+    </div>
+  );
+}
+`,
+    output: `export function Callout({ children }) {
+  return (
+    <div
+      className="rounded-xl border border-zinc-400/30
+        bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30
+        dark:bg-neutral-900/50"
+    >
+      {children}
+    </div>
+  );
+}
+`,
+    options: {
+      printWidth: 60,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
 ];
 
 describe('typescript/attribute-without-expression', () => {
diff --git a/tests/v3-test/typescript/expression.test.ts b/tests/v3-test/typescript/expression.test.ts
index 540ca25..5452c40 100644
--- a/tests/v3-test/typescript/expression.test.ts
+++ b/tests/v3-test/typescript/expression.test.ts
@@ -632,6 +632,61 @@ dark:border-neutral-500/30 dark:bg-neutral-900/50\`]: true,
       endingPosition: 'absolute',
     },
   },
+  {
+    name: 'ending position (4)',
+    input: `
+export function Callout({ children }) {
+  return (
+    <div className={'rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50'}>
+      {children}
+    </div>
+  );
+}
+`,
+    output: `export function Callout({ children }) {
+  return (
+    <div
+      className={\`rounded-xl border border-zinc-400/30
+        bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30
+        dark:bg-neutral-900/50\`}
+    >
+      {children}
+    </div>
+  );
+}
+`,
+    options: {
+      printWidth: 60,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
+  {
+    name: 'ending position (5) - useTabs: true',
+    input: `
+export function Callout({ children }) {
+  return (
+    <div className={'rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50'}>
+      {children}
+    </div>
+  );
+}
+`,
+    output: `export function Callout({ children }) {
+\treturn (
+\t\t<div
+\t\t\tclassName={\`rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4
+\t\t\t\tpy-4 dark:border-neutral-500/30 dark:bg-neutral-900/50\`}
+\t\t>
+\t\t\t{children}
+\t\t</div>
+\t);
+}
+`,
+    options: {
+      useTabs: true,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
   {
     name: 'issue #27 (1) - template literal ending with a space',
     input: `
diff --git a/tests/v3-test/vue/attribute-without-expression.test.ts b/tests/v3-test/vue/attribute-without-expression.test.ts
index e82e92c..4be9731 100644
--- a/tests/v3-test/vue/attribute-without-expression.test.ts
+++ b/tests/v3-test/vue/attribute-without-expression.test.ts
@@ -152,7 +152,7 @@ const fixtures: Fixture[] = [
 `,
   },
   {
-    name: 'ending position',
+    name: 'ending position (1)',
     input: `
 <template>
   <div class="rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50">
@@ -173,6 +173,30 @@ dark:border-neutral-500/30 dark:bg-neutral-900/50"
       endingPosition: 'absolute',
     },
   },
+  {
+    name: 'ending position (2)',
+    input: `
+<template>
+  <div class="rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50">
+    <slot></slot>
+  </div>
+</template>
+`,
+    output: `<template>
+  <div
+    class="rounded-xl border border-zinc-400/30
+      bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30
+      dark:bg-neutral-900/50"
+  >
+    <slot></slot>
+  </div>
+</template>
+`,
+    options: {
+      printWidth: 60,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
 ];
 
 describe('vue/attribute-without-expression', () => {
diff --git a/tests/v3-test/vue/expression.test.ts b/tests/v3-test/vue/expression.test.ts
index 08be4d2..989aa69 100644
--- a/tests/v3-test/vue/expression.test.ts
+++ b/tests/v3-test/vue/expression.test.ts
@@ -456,6 +456,55 @@ dark:border-neutral-500/30 dark:bg-neutral-900/50\`]: true,
       endingPosition: 'absolute',
     },
   },
+  {
+    name: 'ending position (4)',
+    input: `
+<template>
+  <div :class="'rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50'">
+    <slot></slot>
+  </div>
+</template>
+`,
+    output: `<template>
+  <div
+    :class="\`rounded-xl border border-zinc-400/30
+      bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30
+      dark:bg-neutral-900/50\`"
+  >
+    <slot></slot>
+  </div>
+</template>
+`,
+    options: {
+      printWidth: 60,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
+  {
+    name: 'ending position (5) - useTabs: true',
+    input: `
+<template>
+  <div :class="'rounded-xl border border-zinc-400/30 bg-gray-100/50 px-4 py-4 dark:border-neutral-500/30 dark:bg-neutral-900/50'">
+    <slot></slot>
+  </div>
+</template>
+`,
+    output: `<template>
+\t<div
+\t\t:class="\`rounded-xl border border-zinc-400/30 bg-gray-100/50
+\t\t\tpx-4 py-4 dark:border-neutral-500/30
+\t\t\tdark:bg-neutral-900/50\`"
+\t>
+\t\t<slot></slot>
+\t</div>
+</template>
+`,
+    options: {
+      printWidth: 70,
+      useTabs: true,
+      endingPosition: 'absolute-with-indent',
+    },
+  },
 ];
 
 describe('vue/expression', () => {