Repository: ony3000/prettier-plugin-classnames
Pull request #60 (2024-05-14 14:04:31)

Fix bug related to delimiter conversion


Patch:
diff --git a/src/packages/core-parts/finder.ts b/src/packages/core-parts/finder.ts
index d8d581b..6578cba 100644
--- a/src/packages/core-parts/finder.ts
+++ b/src/packages/core-parts/finder.ts
@@ -8,7 +8,7 @@ import type {
   ClassNameNode,
   NarrowedParserOptions,
 } from './shared';
-import { EOL, SINGLE_QUOTE, DOUBLE_QUOTE } from './shared';
+import { EOL, SINGLE_QUOTE, DOUBLE_QUOTE, BACKTICK } from './shared';
 
 type ASTNode = {
   type: string;
@@ -50,6 +50,9 @@ function createExpressionNode(
     isItAnObjectProperty: false,
     isItAnOperandOfTernaryOperator: false,
     isItFunctionArgument: false,
+    hasSingleQuote: false,
+    hasDoubleQuote: false,
+    hasBacktick: false,
     shouldKeepDelimiter: false,
     ...arg,
   };
@@ -124,7 +127,7 @@ export function findTargetClassNameNodes(
    */
   const keywordStartingNodes: ASTNode[] = [];
   /**
-   * Class names enclosed in some kind of quotes
+   * Class names enclosed in delimiters
    */
   const classNameNodes: ClassNameNode[] = [];
 
@@ -319,6 +322,11 @@ export function findTargetClassNameNodes(
                   // eslint-disable-next-line no-param-reassign
                   array[index] = createExpressionNode({
                     delimiterType: classNameNode.delimiterType,
+                    hasSingleQuote: classNameNode.hasSingleQuote,
+                    hasDoubleQuote: classNameNode.hasDoubleQuote,
+                    hasBacktick: classNameNode.hasBacktick,
+                    shouldKeepDelimiter:
+                      classNameNode.hasSingleQuote && classNameNode.hasDoubleQuote,
                     range: classNameNode.range,
                     startLineIndex: classNameNode.startLineIndex,
                   });
@@ -416,6 +424,9 @@ export function findTargetClassNameNodes(
                 : node.raw[0] === DOUBLE_QUOTE
                 ? 'double-quote'
                 : 'backtick',
+            hasSingleQuote: node.value.indexOf(SINGLE_QUOTE) !== -1,
+            hasDoubleQuote: node.value.indexOf(DOUBLE_QUOTE) !== -1,
+            hasBacktick: node.value.indexOf(BACKTICK) !== -1,
             range: [currentNodeRangeStart, currentNodeRangeEnd],
             startLineIndex: node.loc.start.line - 1,
           });
@@ -437,6 +448,7 @@ export function findTargetClassNameNodes(
               extra: z.object({
                 raw: z.string(),
               }),
+              value: z.string(),
             }),
           )
         ) {
@@ -449,6 +461,9 @@ export function findTargetClassNameNodes(
                 : node.extra.raw[0] === DOUBLE_QUOTE
                 ? 'double-quote'
                 : 'backtick',
+            hasSingleQuote: node.value.indexOf(SINGLE_QUOTE) !== -1,
+            hasDoubleQuote: node.value.indexOf(DOUBLE_QUOTE) !== -1,
+            hasBacktick: node.value.indexOf(BACKTICK) !== -1,
             range: [currentNodeRangeStart, currentNodeRangeEnd],
             startLineIndex: node.loc.start.line - 1,
           });
@@ -482,8 +497,8 @@ export function findTargetClassNameNodes(
           const { cooked } = node.quasis[0].value;
           const conditionForPreservation =
             !node.quasis[0].tail ||
-            (options.singleQuote && cooked.indexOf("'") !== -1) ||
-            (!options.singleQuote && cooked.indexOf('"') !== -1);
+            (options.singleQuote && cooked.indexOf(SINGLE_QUOTE) !== -1) ||
+            (!options.singleQuote && cooked.indexOf(DOUBLE_QUOTE) !== -1);
 
           if (conditionForPreservation) {
             classNameNodes.push(
@@ -653,7 +668,7 @@ export function findTargetClassNameNodesForVue(
    */
   const keywordStartingNodes: ASTNode[] = [];
   /**
-   * Class names enclosed in some kind of quotes
+   * Class names enclosed in delimiters
    */
   const classNameNodes: ClassNameNode[] = [];
 
@@ -930,7 +945,7 @@ export function findTargetClassNameNodesForAstro(
    */
   const keywordStartingNodes: ASTNode[] = [];
   /**
-   * Class names enclosed in some kind of quotes
+   * Class names enclosed in delimiters
    */
   const classNameNodes: ClassNameNode[] = [];
 
diff --git a/src/packages/core-parts/index.ts b/src/packages/core-parts/index.ts
index 43b95fc..c7165b4 100644
--- a/src/packages/core-parts/index.ts
+++ b/src/packages/core-parts/index.ts
@@ -6,7 +6,7 @@ import {
   findTargetClassNameNodesForAstro,
 } from './finder';
 import type { Dict, ClassNameNode, NarrowedParserOptions } from './shared';
-import { EOL, PH, SPACE, TAB } from './shared';
+import { EOL, PH, SPACE, TAB, SINGLE_QUOTE, DOUBLE_QUOTE, BACKTICK } from './shared';
 
 type LineNode = {
   indentLevel: number;
@@ -40,18 +40,39 @@ function getExtraIndentLevel(node: ClassNameNode) {
   return 0;
 }
 
-function getSomeKindOfQuotes(
+function getDelimiters(
   node: ClassNameNode,
   isMultiLineClassName: boolean,
   parser: string,
+  singleQuote: boolean,
 ): [string, string] {
-  const baseQuote =
-    // eslint-disable-next-line no-nested-ternary
-    node.type === 'expression' && node.delimiterType === 'backtick' && node.shouldKeepDelimiter
-      ? '`'
-      : parser === 'vue' && node.type === 'expression'
-      ? "'"
-      : '"';
+  let baseDelimiter = DOUBLE_QUOTE;
+
+  if (node.type === 'expression') {
+    if (node.delimiterType === 'backtick' && node.shouldKeepDelimiter) {
+      baseDelimiter = BACKTICK;
+    } else if (parser === 'vue') {
+      baseDelimiter = SINGLE_QUOTE;
+    } else if (singleQuote && node.hasSingleQuote) {
+      if (node.shouldKeepDelimiter) {
+        if (node.delimiterType === 'backtick') {
+          baseDelimiter = BACKTICK;
+        } else if (node.delimiterType === 'single-quote') {
+          baseDelimiter = SINGLE_QUOTE;
+        }
+      }
+    } else if (!singleQuote && node.hasDoubleQuote) {
+      if (node.shouldKeepDelimiter) {
+        if (node.delimiterType === 'backtick') {
+          baseDelimiter = BACKTICK;
+        } else if (node.delimiterType === 'single-quote') {
+          baseDelimiter = SINGLE_QUOTE;
+        }
+      } else {
+        baseDelimiter = SINGLE_QUOTE;
+      }
+    }
+  }
 
   const opener = `${
     isMultiLineClassName &&
@@ -60,8 +81,8 @@ function getSomeKindOfQuotes(
     node.isItAnObjectProperty
       ? '['
       : ''
-  }${!isMultiLineClassName || node.type === 'attribute' ? baseQuote : '`'}`;
-  const closer = `${!isMultiLineClassName || node.type === 'attribute' ? baseQuote : '`'}${
+  }${!isMultiLineClassName || node.type === 'attribute' ? baseDelimiter : BACKTICK}`;
+  const closer = `${!isMultiLineClassName || node.type === 'attribute' ? baseDelimiter : BACKTICK}${
     isMultiLineClassName &&
     node.type === 'expression' &&
     node.delimiterType !== 'backtick' &&
@@ -254,10 +275,11 @@ function replaceClassName({
       )}${trailingSpace}`;
 
       const isMultiLineClassName = classNameWithOriginalSpaces.split(EOL).length > 1;
-      const [quoteStart, quoteEnd] = getSomeKindOfQuotes(
+      const [delimiterStart, delimiterEnd] = getDelimiters(
         classNameNode,
         isMultiLineClassName,
         options.parser,
+        options.singleQuote,
       );
 
       const elementOpener = '<';
@@ -286,9 +308,13 @@ function replaceClassName({
       const frozenIndent = freezeNonClassName(rawIndent);
 
       const isAttributeType = classNameNode.type === 'attribute';
-      const substitute = `${isAttributeType ? '' : quoteStart}${classNameWithOriginalSpaces}${
-        isAttributeType ? '' : quoteEnd
-      }`
+      const substitute = `${isAttributeType ? '' : delimiterStart}${
+        classNameNode.type === 'expression' &&
+        classNameNode.delimiterType !== 'backtick' &&
+        isMultiLineClassName
+          ? classNameWithOriginalSpaces.replace(/`/g, '\\`')
+          : classNameWithOriginalSpaces
+      }${isAttributeType ? '' : delimiterEnd}`
         .split(EOL)
         .map((raw) => {
           const frozen = freezeClassName(raw);
@@ -556,10 +582,11 @@ async function replaceClassNameAsync({
       )}${trailingSpace}`;
 
       const isMultiLineClassName = classNameWithOriginalSpaces.split(EOL).length > 1;
-      const [quoteStart, quoteEnd] = getSomeKindOfQuotes(
+      const [delimiterStart, delimiterEnd] = getDelimiters(
         classNameNode,
         isMultiLineClassName,
         options.parser,
+        options.singleQuote,
       );
 
       const elementOpener = '<';
@@ -588,9 +615,13 @@ async function replaceClassNameAsync({
       const frozenIndent = freezeNonClassName(rawIndent);
 
       const isAttributeType = classNameNode.type === 'attribute';
-      const substitute = `${isAttributeType ? '' : quoteStart}${classNameWithOriginalSpaces}${
-        isAttributeType ? '' : quoteEnd
-      }`
+      const substitute = `${isAttributeType ? '' : delimiterStart}${
+        classNameNode.type === 'expression' &&
+        classNameNode.delimiterType !== 'backtick' &&
+        isMultiLineClassName
+          ? classNameWithOriginalSpaces.replace(/`/g, '\\`')
+          : classNameWithOriginalSpaces
+      }${isAttributeType ? '' : delimiterEnd}`
         .split(EOL)
         .map((raw) => {
           const frozen = freezeClassName(raw);
diff --git a/src/packages/core-parts/shared.ts b/src/packages/core-parts/shared.ts
index 3be95fc..7ad91ce 100644
--- a/src/packages/core-parts/shared.ts
+++ b/src/packages/core-parts/shared.ts
@@ -28,6 +28,11 @@ export const SINGLE_QUOTE = "'";
  */
 export const DOUBLE_QUOTE = '"';
 
+/**
+ * backtick character
+ */
+export const BACKTICK = '`';
+
 export type Dict<T = unknown> = Record<string, T | undefined>;
 
 export type NodeRange = [number, number];
@@ -40,6 +45,9 @@ type ClassNameNodeBase = {
 type UnknownNode = ClassNameNodeBase & {
   type: 'unknown';
   delimiterType: 'single-quote' | 'double-quote' | 'backtick';
+  hasSingleQuote?: boolean;
+  hasDoubleQuote?: boolean;
+  hasBacktick?: boolean;
 };
 
 type AttributeNode = ClassNameNodeBase & {
@@ -55,6 +63,9 @@ export type ExpressionNode = ClassNameNodeBase & {
   isItAnObjectProperty: boolean;
   isItAnOperandOfTernaryOperator: boolean;
   isItFunctionArgument: boolean;
+  hasSingleQuote: boolean;
+  hasDoubleQuote: boolean;
+  hasBacktick: boolean;
   shouldKeepDelimiter: boolean;
 };
 


Test patch:
diff --git a/tests/v2-test/vue/delimiter-conversion/ideal.test.ts b/tests/v2-test/vue/delimiter-conversion/ideal.test.ts
index 0007f31..ee035a5 100644
--- a/tests/v2-test/vue/delimiter-conversion/ideal.test.ts
+++ b/tests/v2-test/vue/delimiter-conversion/ideal.test.ts
@@ -71,12 +71,12 @@ const fixtures: Fixture[] = [
 `,
     output: `<template>
   <div>
-    <Box
+    <div
       v-bind:class="\`lorem ipsum do\\\`or sit amet consectetur
         adipiscing elit proin ex massa hendrerit eu posuere\`"
     >
       <slot></slot>
-    </Box>
+    </div>
   </div>
 </template>
 `,
@@ -94,12 +94,12 @@ const fixtures: Fixture[] = [
 `,
     output: `<template>
   <div>
-    <Box
+    <div
       v-bind:class="\`lorem ipsum do\\\`or sit amet consectetur
         adipiscing elit proin ex massa hendrerit eu posuere\`"
     >
       <slot></slot>
-    </Box>
+    </div>
   </div>
 </template>
 `,
diff --git a/tests/v3-test/vue/delimiter-conversion/ideal.test.ts b/tests/v3-test/vue/delimiter-conversion/ideal.test.ts
index 0007f31..ee035a5 100644
--- a/tests/v3-test/vue/delimiter-conversion/ideal.test.ts
+++ b/tests/v3-test/vue/delimiter-conversion/ideal.test.ts
@@ -71,12 +71,12 @@ const fixtures: Fixture[] = [
 `,
     output: `<template>
   <div>
-    <Box
+    <div
       v-bind:class="\`lorem ipsum do\\\`or sit amet consectetur
         adipiscing elit proin ex massa hendrerit eu posuere\`"
     >
       <slot></slot>
-    </Box>
+    </div>
   </div>
 </template>
 `,
@@ -94,12 +94,12 @@ const fixtures: Fixture[] = [
 `,
     output: `<template>
   <div>
-    <Box
+    <div
       v-bind:class="\`lorem ipsum do\\\`or sit amet consectetur
         adipiscing elit proin ex massa hendrerit eu posuere\`"
     >
       <slot></slot>
-    </Box>
+    </div>
   </div>
 </template>
 `,