Repository: prettier/plugin-pug
Pull request #580 (2025-04-19 05:40:04)

Stop unnecessary quote escaping on vue v-bind strings
This PR solves issue #531. Previously, using the Vue v-bind directive on an attribute whose value involved a string could result in the string's delimiting quotes being incorrectly escaped. Now escaping behaves as expected, so this:
```
div(:value=attributes['data-value'])
div(:value=attributes['"data-value"'])
div(:value=attributes['"\'data-value\'"'])
div(:value='"\'data-value\'"')
``` 
Becomes this when formatted:
```
div(:value=attributes["data-value"])
div(:value=attributes['"data-value"'])
div(:value=attributes["\"'data-value'\""])
div(:value="\"\'data-value\'\"")
```
Some quotes are transformed because of option `singleQuote` or `pugSingleQuote` being set to `false`.

github-actions[bot]: <h2>Coverage Report</h2>
<table> <thead> <tr> <th align="center">Status</th> <th align="left">Category</th> <th align="right">Percentage</th> <th align="right">Covered / Total</th> </tr> </thead> <tbody> <tr> <td align="center">🟢</td> <td align="left">Lines</td> <td align="right">94.59% (🎯 90%)<br/>⬆️ <em>+0.01%</em></td> <td align="right">2641 / 2792</td> </tr> <tr> <td align="center">🟢</td> <td align="left">Statements</td> <td align="right">94.59% (🎯 90%)<br/>⬆️ <em>+0.01%</em></td> <td align="right">2641 / 2792</td> </tr> <tr> <td align="center">🟢</td> <td align="left">Functions</td> <td align="right">96.29% (🎯 90%)<br/>🟰 <em>±0%</em></td> <td align="right">104 / 108</td> </tr> <tr> <td align="center">🟢</td> <td align="left">Branches</td> <td align="right">92.97% (🎯 90%)<br/>⬇️ <em>-0.01%</em></td> <td align="right">635 / 683</td> </tr> </tbody> </table><details><summary>File Coverage</summary><table> <thead> <tr> <th align="left">File</th> <th align="right">Stmts</th> <th align="right">Branches</th> <th align="right">Functions</th> <th align="right">Lines</th> <th align="left">Uncovered Lines</th> </tr> </thead> <tbody> <tr> <td align="left" colspan="6"><b>Changed Files</b></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/printer.ts">src/printer.ts</a></td> <td align="right">94.07%<br/>⬆️ <em>+0.01%</em></td> <td align="right">94.17%<br/>⬇️ <em>-0.02%</em></td> <td align="right">100%<br/>🟰 <em>±0%</em></td> <td align="right">94.07%<br/>⬆️ <em>+0.01%</em></td> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/printer.ts#L334-L335" class="text-red">334-335</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/printer.ts#L371-L372" class="text-red">371-372</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/printer.ts#L521-L523" class="text-red">521-523</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/printer.ts#L535-L544" class="text-red">535-544</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/printer.ts#L549-L562" class="text-red">549-562</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/printer.ts#L610-L646" class="text-red">610-646</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/printer.ts#L662-L663" class="text-red">662-663</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/printer.ts#L701-L702" class="text-red">701-702</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/printer.ts#L769-L773" class="text-red">769-773</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/printer.ts#L1122-L1123" class="text-red">1122-1123</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/printer.ts#L1216-L1217" class="text-red">1216-1217</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/printer.ts#L1244" class="text-red">1244</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/printer.ts#L1256-L1257" class="text-red">1256-1257</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/printer.ts#L1557-L1558" class="text-red">1557-1558</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/printer.ts#L1604-L1605" class="text-red">1604-1605</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/printer.ts#L1930-L1936" class="text-red">1930-1936</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/printer.ts#L1974-L1979" class="text-red">1974-1979</a></td> </tr> <tr> <td align="left" colspan="6"><b>Unchanged Files</b></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/doctype-shortcut-registry.ts">src/doctype-shortcut-registry.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/index.ts">src/index.ts</a></td> <td align="right">93.18%</td> <td align="right">100%</td> <td align="right">71.42%</td> <td align="right">93.18%</td> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/index.ts#L79-L81" class="text-red">79-81</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/index.ts#L84-L86" class="text-red">84-86</a></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/logger.ts">src/logger.ts</a></td> <td align="right">77.46%</td> <td align="right">90.9%</td> <td align="right">81.81%</td> <td align="right">77.46%</td> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/logger.ts#L37-L46" class="text-red">37-46</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/logger.ts#L93-L94" class="text-red">93-94</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/logger.ts#L153-L157" class="text-red">153-157</a></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/common.ts">src/options/common.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/constants.ts">src/options/constants.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/converge.ts">src/options/converge.ts</a></td> <td align="right">100%</td> <td align="right">60%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/index.ts">src/options/index.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/pug-attribute-separator.ts">src/options/pug-attribute-separator.ts</a></td> <td align="right">89.74%</td> <td align="right">75%</td> <td align="right">100%</td> <td align="right">89.74%</td> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/pug-attribute-separator.ts#L52-L55" class="text-red">52-55</a></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/pug-class-location.ts">src/options/pug-class-location.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/pug-class-notation.ts">src/options/pug-class-notation.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/pug-closing-bracket-indent-depth.ts">src/options/pug-closing-bracket-indent-depth.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/pug-comment-preserve-spaces.ts">src/options/pug-comment-preserve-spaces.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/pug-explicit-div.ts">src/options/pug-explicit-div.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/pug-framework.ts">src/options/pug-framework.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/pug-id-notation.ts">src/options/pug-id-notation.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/pug-preserve-attribute-brackets.ts">src/options/pug-preserve-attribute-brackets.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/pug-preserve-whitespace.ts">src/options/pug-preserve-whitespace.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/pug-single-file-component-indentation.ts">src/options/pug-single-file-component-indentation.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/pug-wrap-attributes.ts">src/options/pug-wrap-attributes.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/types.ts">src/options/types.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/attribute-sorting/index.ts">src/options/attribute-sorting/index.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/attribute-sorting/utils.ts">src/options/attribute-sorting/utils.ts</a></td> <td align="right">95.69%</td> <td align="right">94.44%</td> <td align="right">100%</td> <td align="right">95.69%</td> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/attribute-sorting/utils.ts#L82-L83" class="text-red">82-83</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/attribute-sorting/utils.ts#L94-L95" class="text-red">94-95</a></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/empty-attributes/index.ts">src/options/empty-attributes/index.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/empty-attributes/types.ts">src/options/empty-attributes/types.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/options/empty-attributes/utils.ts">src/options/empty-attributes/utils.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/utils/angular.ts">src/utils/angular.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/utils/common.ts">src/utils/common.ts</a></td> <td align="right">89.67%</td> <td align="right">86.11%</td> <td align="right">100%</td> <td align="right">89.67%</td> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/utils/common.ts#L19-L20" class="text-red">19-20</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/utils/common.ts#L27-L28" class="text-red">27-28</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/utils/common.ts#L56-L57" class="text-red">56-57</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/utils/common.ts#L258-L259" class="text-red">258-259</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/utils/common.ts#L262-L263" class="text-red">262-263</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/utils/common.ts#L269-L272" class="text-red">269-272</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/utils/common.ts#L331-L332" class="text-red">331-332</a></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/utils/script-mime-types.ts">src/utils/script-mime-types.ts</a></td> <td align="right">87.5%</td> <td align="right">83.33%</td> <td align="right">100%</td> <td align="right">87.5%</td> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/utils/script-mime-types.ts#L42-L43" class="text-red">42-43</a>, <a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/utils/script-mime-types.ts#L49-L50" class="text-red">49-50</a></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/utils/svelte.ts">src/utils/svelte.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/prettier/plugin-pug/blob/eff552aa6a78b00df86e1b181c9d2456fd7b5f96/src/utils/vue.ts">src/utils/vue.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> </tbody> </table></details>
<em>Generated in workflow <a href=https://github.com/prettier/plugin-pug/actions/runs/14714122560>#550</a> for commit <a href="https://github.com/prettier/plugin-pug/commit/eff552aa6a78b00df86e1b181c9d2456fd7b5f96">eff552a</a> by the <a href="https://github.com/davelosert/vitest-coverage-report-action">Vitest Coverage Report Action</a></em>

<!-- vitest-coverage-report-marker-root -->

Patch:
diff --git a/src/printer.ts b/src/printer.ts
index 9390781..293ef69 100644
--- a/src/printer.ts
+++ b/src/printer.ts
@@ -680,21 +680,19 @@ export class PugPrinter {
   ): Promise<string> {
     const { trimTrailingSemicolon = false, trimLeadingSemicolon = true } =
       formatOptions;
-    val = val.trim();
     const options: Options = { ...this.codeInterpolationOptions };
+
+    val = val.trim();
     const wasQuoted: boolean = isQuoted(val);
+
     if (wasQuoted) {
       options.singleQuote = !this.options.pugSingleQuote;
       val = val.slice(1, -1); // Remove quotes
     }
 
     val = await format(val, { parser, ...options });
-    val =
-      this.quotes === '"'
-        ? // Escape double quotes, but only if they are not already escaped
-          val.replaceAll(/(?<!\\)((?:\\\\)*)"/g, '$1\\"')
-        : val.replaceAll("'", "\\'");
     val = unwrapLineFeeds(val);
+
     if (trimTrailingSemicolon && val.at(-1) === ';') {
       val = val.slice(0, -1);
     }
@@ -703,7 +701,17 @@ export class PugPrinter {
       val = val.slice(1);
     }
 
-    return wasQuoted ? this.quoteString(val) : val;
+    if (wasQuoted) {
+      val =
+        this.quotes === '"'
+          ? // Escape double quotes, but only if they are not already escaped
+            (val = val.replaceAll(/(?<!\\)((?:\\\\)*)"/g, '$1\\"'))
+          : (val = val.replaceAll("'", "\\'"));
+
+      val = this.quoteString(val);
+    }
+
+    return val;
   }
 
   private async formatStyleAttribute(val: string): Promise<string> {


Test patch:
diff --git a/tests/frameworks/vue/v-bind/formatted.pug b/tests/frameworks/vue/v-bind/formatted.pug
index 039af42..6911f70 100644
--- a/tests/frameworks/vue/v-bind/formatted.pug
+++ b/tests/frameworks/vue/v-bind/formatted.pug
@@ -2,3 +2,8 @@ a(v-bind="{ ...$attrs }") ...
 a(v-bind:href="url") ...
 a(:href="url") ...
 a(:[key]="url") ...
+
+div(:value=attributes["data-value"])
+div(:value=attributes['"data-value"'])
+div(:value=attributes["\"'data-value'\""])
+div(:value="\"\'data-value\'\"")
diff --git a/tests/frameworks/vue/v-bind/unformatted.pug b/tests/frameworks/vue/v-bind/unformatted.pug
index a3e1a63..743e0d4 100644
--- a/tests/frameworks/vue/v-bind/unformatted.pug
+++ b/tests/frameworks/vue/v-bind/unformatted.pug
@@ -2,3 +2,8 @@ a(v-bind="{  ...$attrs  }") ...
 a(v-bind:href="  url ") ...
 a(:href="  url ") ...
 a(:[key]="  url ") ...
+
+div(:value=attributes['data-value'])
+div(:value=attributes['"data-value"'])
+div(:value=attributes['"\'data-value\'"'])
+div(:value='"\'data-value\'"')
\ No newline at end of file