Repository: sveltejs/prettier-plugin-svelte
Pull request #199 (2021-02-03 14:08:58)

(fix) keep whitespace around script/style tags
#197


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bd3b19a..2f8b01f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # prettier-plugin-svelte changelog
 
+## 2.1.2 (Unreleased)
+
+* Keep whitespace around script/style tags ([#197](https://github.com/sveltejs/prettier-plugin-svelte/issues/197))
+
 ## 2.1.1
 
 * Fix `svelteBracketNewLine: true` sometimes not having `>` on a separate line ([#194](https://github.com/sveltejs/prettier-plugin-svelte/issues/194))
diff --git a/src/embed.ts b/src/embed.ts
index 8e71454..09608f8 100644
--- a/src/embed.ts
+++ b/src/embed.ts
@@ -4,7 +4,7 @@ import { snippedTagContentAttribute } from './lib/snipTagContent';
 import { PrintFn } from './print';
 import {
     getAttributeTextValue,
-    getPreviousNode,
+    getLeadingComment,
     isIgnoreDirective,
     isNodeSupportedLanguage,
 } from './print/node-helpers';
@@ -176,9 +176,7 @@ function embedTag(
 ) {
     const node: Node = path.getNode();
     const content = getSnippedContent(node);
-
-    const previousNode = getPreviousNode(path);
-    const previousComment = previousNode && previousNode.type === 'Comment' ? previousNode : null;
+    const previousComment = getLeadingComment(path);
 
     const body: Doc =
         isNodeSupportedLanguage(node) && !isIgnoreDirective(previousComment)
diff --git a/src/lib/snipTagContent.ts b/src/lib/snipTagContent.ts
index dfa7a55..1f69955 100644
--- a/src/lib/snipTagContent.ts
+++ b/src/lib/snipTagContent.ts
@@ -1,7 +1,7 @@
-export const snippedTagContentAttribute = '✂prettier:content✂'
+export const snippedTagContentAttribute = '✂prettier:content✂';
 
 export function snipTagContent(tagName: string, source: string, placeholder = ''): string {
-    const regex = new RegExp(`[\\s\n]*<${tagName}([^]*?)>([^]*?)<\/${tagName}>[\\s\n]*`, 'gi');
+    const regex = new RegExp(`<${tagName}([^]*?)>([^]*?)<\/${tagName}>`, 'gi');
     return source.replace(regex, (_, attributes, content) => {
         const encodedContent = Buffer.from(content).toString('base64');
         return `<${tagName}${attributes} ${snippedTagContentAttribute}="${encodedContent}">${placeholder}</${tagName}>`;
diff --git a/src/print/index.ts b/src/print/index.ts
index 1460078..255641a 100644
--- a/src/print/index.ts
+++ b/src/print/index.ts
@@ -9,7 +9,7 @@ import { flatten, isASTNode, isPreTagContent } from './helpers';
 import {
     checkWhitespaceAtEndOfSvelteBlock,
     checkWhitespaceAtStartOfSvelteBlock,
-    doesEmbedStartAt,
+    doesEmbedStartAfterNode,
     endsWithLinebreak,
     getUnencodedText,
     isBlockElement,
@@ -32,6 +32,7 @@ import {
     trimTextNodeLeft,
     trimTextNodeRight,
     canOmitSoftlineBeforeClosingTag,
+    getNextNode,
 } from './node-helpers';
 import {
     ASTNode,
@@ -568,6 +569,8 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
         case 'Ref':
             return concat([line, 'ref:', node.name]);
         case 'Comment': {
+            const nodeAfterComment = getNextNode(path);
+
             /**
              * If there is no sibling node that starts right after us but the parent indicates
              * that there used to be, that means that node was actually an embedded `<style>`
@@ -575,7 +578,11 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
              * If so, the comment does not refer to the next line we will see.
              * The `embed` function handles printing the comment in the right place.
              */
-            if (doesEmbedStartAt(node.end, path)) {
+            if (
+                doesEmbedStartAfterNode(node, path) ||
+                (isEmptyTextNode(nodeAfterComment) &&
+                    doesEmbedStartAfterNode(nodeAfterComment, path))
+            ) {
                 return '';
             } else if (isIgnoreDirective(node)) {
                 ignoreNext = true;
@@ -884,6 +891,10 @@ function prepareChildren(children: Node[], path: FastPath, print: PrintFn): Node
             continue;
         }
 
+        if (isEmptyTextNode(currentChild) && doesEmbedStartAfterNode(currentChild, path)) {
+            continue;
+        }
+
         if (isCommentFollowedByOptions(currentChild, idx)) {
             svelteOptionsComment = printComment(currentChild);
             const nextChild = children[idx + 1];
diff --git a/src/print/node-helpers.ts b/src/print/node-helpers.ts
index 2e4afd2..8f0776b 100644
--- a/src/print/node-helpers.ts
+++ b/src/print/node-helpers.ts
@@ -18,6 +18,7 @@ import {
     KeyBlockNode,
     PendingBlockNode,
     ThenBlockNode,
+    CommentNode,
 } from './nodes';
 import { blockElements, TagName } from '../lib/elements';
 import { FastPath, ParserOptions } from 'prettier';
@@ -73,32 +74,64 @@ export function getChildren(node: Node): Node[] {
 }
 
 /**
- * Returns the previous sibling node.
+ * Returns siblings, that is, the children of the parent.
  */
-export function getPreviousNode(path: FastPath): Node | undefined {
-    const node: Node = path.getNode();
+export function getSiblings(path: FastPath): Node[] {
     let parent: Node = path.getParentNode();
 
     if (isASTNode(parent)) {
         parent = parent.html;
     }
 
-    return getChildren(parent).find((child) => child.end === node.start);
+    return getChildren(parent);
+}
+
+/**
+ * Returns the previous sibling node.
+ */
+export function getPreviousNode(path: FastPath): Node | undefined {
+    const node: Node = path.getNode();
+    return getSiblings(path).find((child) => child.end === node.start);
+}
+
+/**
+ * Returns the next sibling node.
+ */
+export function getNextNode(path: FastPath, node: Node = path.getNode()): Node | undefined {
+    return getSiblings(path).find((child) => child.start === node.end);
+}
+
+/**
+ * Returns the comment that is above the current node.
+ */
+export function getLeadingComment(path: FastPath): CommentNode | undefined {
+    const siblings = getSiblings(path);
+
+    let node: Node = path.getNode();
+    let prev: Node | undefined = siblings.find((child) => child.end === node.start);
+    while (prev && (prev.type !== 'Comment' || isEmptyTextNode(prev))) {
+        node = prev;
+        prev = siblings.find((child) => child.end === node.start);
+    }
+
+    return prev && prev.type === 'Comment' ? prev : undefined;
 }
 
 /**
  * Did there use to be any embedded object (that has been snipped out of the AST to be moved)
  * at the specified position?
  */
-export function doesEmbedStartAt(position: number, path: FastPath) {
+export function doesEmbedStartAfterNode(node: Node, path: FastPath, siblings = getSiblings(path)) {
+    const position = node.end;
     const root = path.stack[0];
     const embeds = [root.css, root.html, root.instance, root.js, root.module] as Node[];
 
-    return embeds.find((n) => n && n.start === position) != null;
+    const nextNode = siblings[siblings.indexOf(node) + 1];
+    return embeds.find((n) => n && n.start >= position && (!nextNode || n.end <= nextNode.start));
 }
 
-export function isEmptyTextNode(node: Node): node is TextNode {
-    return node.type === 'Text' && getUnencodedText(node).trim() === '';
+export function isEmptyTextNode(node: Node | undefined): node is TextNode {
+    return !!node && node.type === 'Text' && getUnencodedText(node).trim() === '';
 }
 
 export function isIgnoreDirective(node: Node | undefined | null): boolean {
@@ -254,17 +287,17 @@ export function trimTextNodeLeft(node: TextNode): void {
  */
 export function trimChildren(children: Node[], path: FastPath): void {
     let firstNonEmptyNode = children.findIndex(
-        (n) => !isEmptyTextNode(n) && !doesEmbedStartAt(n.end, path),
+        (n) => !isEmptyTextNode(n) && !doesEmbedStartAfterNode(n, path),
     );
     firstNonEmptyNode = firstNonEmptyNode === -1 ? children.length - 1 : firstNonEmptyNode;
 
     let lastNonEmptyNode = findLastIndex((n, idx) => {
-        // Last node is ok to end and the start of an embeded region,
+        // Last node is ok to end at the start of an embedded region,
         // if it's not a comment (which should stick to the region)
         return (
             !isEmptyTextNode(n) &&
             ((idx === children.length - 1 && n.type !== 'Comment') ||
-                !doesEmbedStartAt(n.end, path))
+                !doesEmbedStartAfterNode(n, path))
         );
     }, children);
     lastNonEmptyNode = lastNonEmptyNode === -1 ? 0 : lastNonEmptyNode;


Test patch:
diff --git a/test/formatting/samples/script-style-tags-multiple-nested/input.html b/test/formatting/samples/script-style-tags-multiple-nested/input.html
new file mode 100644
index 0000000..5f32df0
--- /dev/null
+++ b/test/formatting/samples/script-style-tags-multiple-nested/input.html
@@ -0,0 +1,30 @@
+<div>above</div>
+<script>
+    const hi = '';
+</script>
+<div>below</div>
+
+<div>
+    <div>above</div>
+    <script>
+        const hi = '';
+    </script>
+    <div>below</div>
+</div>
+
+<div>
+    <div>above</div>
+    <script>
+        const hi = '';
+    </script>
+    <style></style>
+</div>
+
+<!-- i stay above style -->
+
+
+<style>
+    .a {
+        color: blue;
+    }
+</style>
diff --git a/test/formatting/samples/script-style-tags-multiple-nested/output.html b/test/formatting/samples/script-style-tags-multiple-nested/output.html
new file mode 100644
index 0000000..cc4e12a
--- /dev/null
+++ b/test/formatting/samples/script-style-tags-multiple-nested/output.html
@@ -0,0 +1,29 @@
+<script>
+    const hi = "";
+</script>
+
+<div>above</div>
+<div>below</div>
+
+<div>
+    <div>above</div>
+    <script>
+        const hi = "";
+    </script>
+    <div>below</div>
+</div>
+
+<div>
+    <div>above</div>
+    <script>
+        const hi = "";
+    </script>
+    <style></style>
+</div>
+
+<!-- i stay above style -->
+<style>
+    .a {
+        color: blue;
+    }
+</style>
diff --git a/test/printer/samples/svelte-head-script-content.html b/test/printer/samples/svelte-head-script-content.html
new file mode 100644
index 0000000..5af04fb
--- /dev/null
+++ b/test/printer/samples/svelte-head-script-content.html
@@ -0,0 +1,7 @@
+<svelte:head>
+    <script async src="helloooooooooooooo{id}"></script>
+    <script>
+        let foo = "bar";
+        window.foo = foo;
+    </script>
+</svelte:head>