Repository: sveltejs/prettier-plugin-svelte
Pull request #423 (2024-01-28 16:13:08)

Remove Buffer usage for Browser Environments
Closes https://github.com/sveltejs/prettier-plugin-svelte/issues/418


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2750f54..ef0f13d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
 # prettier-plugin-svelte changelog
 
+## 3.2.0 (Unreleased)
+
+-   (feat) format JSON script tags
+-   (fix) don't duplicate comments of nested script/style tags
+
 ## 3.1.2
 
 -   (fix) handle `>` tags in attributes
diff --git a/src/base64-string.ts b/src/base64-string.ts
new file mode 100644
index 0000000..887084f
--- /dev/null
+++ b/src/base64-string.ts
@@ -0,0 +1,20 @@
+// Base64 string encoding and decoding module.
+// Uses Buffer for Node.js and btoa/atob for browser environments.
+// We use TextEncoder/TextDecoder for browser environments because
+// they can handle non-ASCII characters, unlike btoa/atob.
+
+export const stringToBase64 =
+    typeof Buffer !== 'undefined'
+        ? (str: string) => Buffer.from(str).toString('base64')
+        : (str: string) =>
+              btoa(
+                  new TextEncoder()
+                      .encode(str)
+                      .reduce((acc, byte) => acc + String.fromCharCode(byte), ''),
+              );
+
+export const base64ToString =
+    typeof Buffer !== 'undefined'
+        ? (str: string) => Buffer.from(str, 'base64').toString()
+        : (str: string) =>
+              new TextDecoder().decode(Uint8Array.from(atob(str), (c) => c.charCodeAt(0)));
diff --git a/src/embed.ts b/src/embed.ts
index 4c433b5..c04a953 100644
--- a/src/embed.ts
+++ b/src/embed.ts
@@ -11,6 +11,7 @@ import {
     getLeadingComment,
     isIgnoreDirective,
     isInsideQuotedAttribute,
+    isJSON,
     isLess,
     isNodeSupportedLanguage,
     isPugTemplate,
@@ -20,6 +21,7 @@ import {
 } from './print/node-helpers';
 import { CommentNode, ElementNode, Node, ScriptNode, StyleNode } from './print/nodes';
 import { extractAttributes } from './lib/extractAttributes';
+import { base64ToString } from './base64-string';
 
 const {
     builders: { group, hardline, softline, indent, dedent, literalline },
@@ -177,7 +179,7 @@ export function embed(path: FastPath, _options: Options) {
 
     const embedType = (
         tag: 'script' | 'style' | 'template',
-        parser: 'typescript' | 'babel-ts' | 'css' | 'scss' | 'less' | 'pug',
+        parser: 'typescript' | 'babel-ts' | 'css' | 'scss' | 'less' | 'pug' | 'json',
         isTopLevel: boolean,
     ) => {
         return async (
@@ -203,7 +205,7 @@ export function embed(path: FastPath, _options: Options) {
             // the user could have set the default language. babel-ts will format things a little
             // bit different though, especially preserving parentheses around dot notation which
             // fixes https://github.com/sveltejs/prettier-plugin-svelte/issues/218
-            isTypeScript(node) ? 'typescript' : 'babel-ts',
+            isTypeScript(node) ? 'typescript' : isJSON(node) ? 'json' : 'babel-ts',
             isTopLevel,
         );
     const embedStyle = (isTopLevel: boolean) =>
@@ -252,7 +254,7 @@ function getSnippedContent(node: Node) {
     const encodedContent = getAttributeTextValue(snippedTagContentAttribute, node);
 
     if (encodedContent) {
-        return Buffer.from(encodedContent, 'base64').toString('utf-8');
+        return base64ToString(encodedContent);
     } else {
         return '';
     }
@@ -260,7 +262,7 @@ function getSnippedContent(node: Node) {
 
 async function formatBodyContent(
     content: string,
-    parser: 'typescript' | 'babel-ts' | 'css' | 'scss' | 'less' | 'pug',
+    parser: 'typescript' | 'babel-ts' | 'css' | 'scss' | 'less' | 'pug' | 'json',
     textToDoc: (text: string, options: object) => Promise<Doc>,
     options: ParserOptions & { pugTabWidth?: number },
 ) {
@@ -365,7 +367,8 @@ async function embedTag(
         // the new position.
         return [...comments, result, hardline];
     } else {
-        return comments.length ? [...comments, result] : result;
+        // Only comments at the top level get the special "move comment" treatment.
+        return isTopLevel && comments.length ? [...comments, result] : result;
     }
 }
 
diff --git a/src/lib/snipTagContent.ts b/src/lib/snipTagContent.ts
index 8933726..cb02d8d 100644
--- a/src/lib/snipTagContent.ts
+++ b/src/lib/snipTagContent.ts
@@ -1,3 +1,5 @@
+import { base64ToString, stringToBase64 } from '../base64-string';
+
 export const snippedTagContentAttribute = '✂prettier:content✂';
 
 const scriptRegex =
@@ -42,7 +44,7 @@ export function snipScriptAndStyleTagContent(source: string): string {
             if (match.startsWith('<!--') || withinOtherSpan(index)) {
                 return match;
             }
-            const encodedContent = Buffer.from(content).toString('base64');
+            const encodedContent = stringToBase64(content);
             const newContent = `<${tagName}${attributes} ${snippedTagContentAttribute}="${encodedContent}">${placeholder}</${tagName}>`;
 
             // Adjust the spans because the source now has a different content length
@@ -97,7 +99,7 @@ const regex = /(<\w+.*?)\s*✂prettier:content✂="(.*?)">.*?(?=<\/)/gi;
 
 export function unsnipContent(text: string): string {
     return text.replace(regex, (_, start, encodedContent) => {
-        const content = Buffer.from(encodedContent, 'base64').toString('utf8');
+        const content = base64ToString(encodedContent);
         return `${start}>${content}`;
     });
 }
diff --git a/src/print/index.ts b/src/print/index.ts
index 394acc5..f525b39 100644
--- a/src/print/index.ts
+++ b/src/print/index.ts
@@ -46,7 +46,6 @@ import {
 import {
     ASTNode,
     AttributeNode,
-    CommentInfo,
     CommentNode,
     IfBlockNode,
     Node,
diff --git a/src/print/node-helpers.ts b/src/print/node-helpers.ts
index 7ee1b63..842b713 100644
--- a/src/print/node-helpers.ts
+++ b/src/print/node-helpers.ts
@@ -268,6 +268,12 @@ export function isTypeScript(node: Node) {
     return ['typescript', 'ts'].includes(lang);
 }
 
+export function isJSON(node: Node) {
+    const lang = getLangAttribute(node) || '';
+    // https://github.com/prettier/prettier/pull/6293
+    return lang.endsWith('json') || lang.endsWith('importmap');
+}
+
 export function isLess(node: Node) {
     const lang = getLangAttribute(node) || '';
     return ['less'].includes(lang);


Test patch:
diff --git a/test/printer/samples/no-tag-snippings.html b/test/printer/samples/no-tag-snippings.html
index d60d00a..9222c2f 100644
--- a/test/printer/samples/no-tag-snippings.html
+++ b/test/printer/samples/no-tag-snippings.html
@@ -1,8 +1,8 @@
 <svelte:head>
     <script type="application/ld+json">
         {
-          "@context": "https://schema.org",
-          "@type": "etc..."
+            "@context": "https://schema.org",
+            "@type": "etc..."
         }
     </script>
     {@html `<style>${getCssText()}</style>`}
diff --git a/test/printer/samples/script-style-inside-element-comment.html b/test/printer/samples/script-style-inside-element-comment.html
new file mode 100644
index 0000000..2fd3e3c
--- /dev/null
+++ b/test/printer/samples/script-style-inside-element-comment.html
@@ -0,0 +1,10 @@
+<div>
+    <!-- hello -->
+    <style></style>
+</div>
+<svelte:head>
+    <!-- world -->
+    <script>
+        let x;
+    </script>
+</svelte:head>