Repository: sveltejs/prettier-plugin-svelte (ts)

Issue:
Handling of multiple HTML comments
Scenario: I try to write to comments on the top of the svelte file.

Expected behavior: They are preserved.
Actual behavior: `prettier-plugin-svelte` seems to push one off and places it before the template part:

```
diff --git a/ts/editor/image-overlay/WithImageConstrained.svelte b/ts/editor/image-overlay/WithImageConstrained.svelte
index 50abe8bdf..13e8e2a40 100644
--- a/ts/editor/image-overlay/WithImageConstrained.svelte
+++ b/ts/editor/image-overlay/WithImageConstrained.svelte
@@ -1,8 +1,4 @@
 <!--
-Copyright: ...
-License: ...
--->
-<!--
 @component
 My comp
 -->
@@ -202,6 +198,12 @@ My comp
     }
 </script>
 
+<!--
+Copyright: ...
+License: ...
+-->
+
+
 {#if activeImage}
     <slot {toggleActualSize} {active} />
 {/if}
```

Pull request description:
feat: allow multiple comments atop of script/style tags
closes #291

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index acca81e..cba15df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 -   (feat) support `requirePragma` and `insertPragma` options ([#350](https://github.com/sveltejs/prettier-plugin-svelte/issues/350))
 -   (feat) support `<svelte:document>`
 -   (feat) trim whitespace in `class` attributes ([#339](https://github.com/sveltejs/prettier-plugin-svelte/issues/339))
+-   (feat) allow multiple comments atop of script/style tags ([#291](https://github.com/sveltejs/prettier-plugin-svelte/issues/291))
 -   (fix) handle script/style attributes without quotes ([#344](https://github.com/sveltejs/prettier-plugin-svelte/issues/344))
 
 ## 2.9.0
diff --git a/src/embed.ts b/src/embed.ts
index a03f60d..bf96f7c 100644
--- a/src/embed.ts
+++ b/src/embed.ts
@@ -14,7 +14,7 @@ import {
     isTypeScript,
     printRaw,
 } from './print/node-helpers';
-import { ElementNode, Node, ScriptNode, StyleNode } from './print/nodes';
+import { CommentNode, ElementNode, Node, ScriptNode, StyleNode } from './print/nodes';
 
 const {
     builders: { concat, hardline, softline, indent, dedent, literalline },
@@ -193,11 +193,16 @@ function embedTag(
     const node: ScriptNode | StyleNode | ElementNode = path.getNode();
     const content =
         tag === 'template' ? printRaw(node as ElementNode, text) : getSnippedContent(node);
-    const previousComment = getLeadingComment(path);
+    const previousComments =
+        node.type === 'Script' || node.type === 'Style'
+            ? node.comments
+            : [getLeadingComment(path)]
+                  .filter(Boolean)
+                  .map((comment) => ({ comment: comment as CommentNode, emptyLineAfter: false }));
 
     const canFormat =
         isNodeSupportedLanguage(node) &&
-        !isIgnoreDirective(previousComment) &&
+        !isIgnoreDirective(previousComments[previousComments.length - 1]?.comment) &&
         (tag !== 'template' ||
             options.plugins.some(
                 (plugin) => typeof plugin !== 'string' && plugin.parsers && plugin.parsers.pug,
@@ -223,16 +228,21 @@ function embedTag(
     ]);
     let result = groupConcat([openingTag, body, '</', tag, '>']);
 
+    const comments = [];
+    for (const comment of previousComments) {
+        comments.push('<!--', comment.comment.data, '-->');
+        comments.push(hardline);
+        if (comment.emptyLineAfter) {
+            comments.push(hardline);
+        }
+    }
+
     if (isTopLevel && options.svelteSortOrder !== 'none') {
         // top level embedded nodes have been moved from their normal position in the
         // node tree. if there is a comment referring to it, it must be recreated at
         // the new position.
-        if (previousComment) {
-            result = concat(['<!--', previousComment.data, '-->', hardline, result, hardline]);
-        } else {
-            result = concat([result, hardline]);
-        }
+        return concat([...comments, result, hardline]);
+    } else {
+        return comments.length ? concat([...comments, result]) : result;
     }
-
-    return result;
 }
diff --git a/src/print/index.ts b/src/print/index.ts
index 98f8c3a..18ec471 100644
--- a/src/print/index.ts
+++ b/src/print/index.ts
@@ -49,6 +49,7 @@ import {
 import {
     ASTNode,
     AttributeNode,
+    CommentInfo,
     CommentNode,
     IfBlockNode,
     Node,
@@ -100,6 +101,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
     }
 
     if (isASTNode(n)) {
+        assignCommentsToNodes(n);
         return printTopLevelParts(n, options, path, print);
     }
 
@@ -786,6 +788,70 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
     throw new Error('unknown node type: ' + node.type);
 }
 
+function assignCommentsToNodes(ast: ASTNode) {
+    if (ast.module) {
+        ast.module.comments = removeAndGetLeadingComments(ast, ast.module);
+    }
+    if (ast.instance) {
+        ast.instance.comments = removeAndGetLeadingComments(ast, ast.instance);
+    }
+    if (ast.css) {
+        ast.css.comments = removeAndGetLeadingComments(ast, ast.css);
+    }
+}
+
+/**
+ * Returns the comments that are above the current node and deletes them from the html ast.
+ */
+function removeAndGetLeadingComments(ast: ASTNode, current: Node): CommentInfo[] {
+    const siblings = getChildren(ast.html);
+    const comments: CommentNode[] = [];
+    const newlines: TextNode[] = [];
+
+    if (!siblings.length) {
+        return [];
+    }
+
+    let node: Node = current;
+    let prev: Node | undefined = siblings.find((child) => child.end === node.start);
+    while (prev) {
+        if (
+            prev.type === 'Comment' &&
+            !isIgnoreStartDirective(prev) &&
+            !isIgnoreEndDirective(prev)
+        ) {
+            comments.push(prev);
+            if (comments.length !== newlines.length) {
+                newlines.push({ type: 'Text', data: '', raw: '', start: -1, end: -1 });
+            }
+        } else if (isEmptyTextNode(prev)) {
+            newlines.push(prev);
+        } else {
+            break;
+        }
+
+        node = prev;
+        prev = siblings.find((child) => child.end === node.start);
+    }
+
+    newlines.length = comments.length; // could be one more if first comment is preceeded by empty text node
+
+    for (const comment of comments) {
+        siblings.splice(siblings.indexOf(comment), 1);
+    }
+
+    for (const text of newlines) {
+        siblings.splice(siblings.indexOf(text), 1);
+    }
+
+    return comments
+        .map((comment, i) => ({
+            comment,
+            emptyLineAfter: getUnencodedText(newlines[i]).split('\n').length > 2,
+        }))
+        .reverse();
+}
+
 function printTopLevelParts(
     n: ASTNode,
     options: ParserOptions,
diff --git a/src/print/nodes.ts b/src/print/nodes.ts
index c9e00d5..48c247a 100644
--- a/src/print/nodes.ts
+++ b/src/print/nodes.ts
@@ -207,17 +207,20 @@ export interface StyleNode extends BaseNode {
     attributes: Node[];
     children: Node[];
     content: StyleProgramNode;
+    comments: CommentInfo[]; // doesn't exist on original node but we use it to store comments
 }
 
 export interface ScriptNode extends BaseNode {
     type: 'Script';
     attributes: Node[];
     content: Node;
+    comments: CommentInfo[]; // doesn't exist on original node but we use it to store comments
 }
 
 export interface StyleProgramNode extends BaseNode {
     type: 'StyleProgram';
     styles: string;
+    comments: CommentInfo[]; // doesn't exist on original node but we use it to store comments
 }
 
 export interface ProgramNode extends BaseNode {
@@ -283,6 +286,11 @@ export interface ConstTagNode extends BaseNode {
     expression: Node;
 }
 
+export interface CommentInfo {
+    comment: CommentNode;
+    emptyLineAfter: boolean;
+}
+
 export type Node =
     | FragmentNode
     | ElementNode
@@ -334,13 +342,7 @@ export type Node =
  */
 export interface ASTNode {
     html: Node;
-    css?: Node & {
-        attributes: Node[];
-        children: Node[];
-        content: Node & {
-            styles: string;
-        };
-    };
+    css?: StyleNode;
     js?: ScriptNode;
     instance?: ScriptNode;
     module?: ScriptNode;


Test patch:
diff --git a/test/formatting/samples/script-style-tags-multiple-nested/output.html b/test/formatting/samples/script-style-tags-multiple-nested/output.html
index cc4e12a..df34455 100644
--- a/test/formatting/samples/script-style-tags-multiple-nested/output.html
+++ b/test/formatting/samples/script-style-tags-multiple-nested/output.html
@@ -22,6 +22,7 @@
 </div>
 
 <!-- i stay above style -->
+
 <style>
     .a {
         color: blue;
diff --git a/test/printer/index.ts b/test/printer/index.ts
index 8165b56..b21681f 100644
--- a/test/printer/index.ts
+++ b/test/printer/index.ts
@@ -17,7 +17,7 @@ for (const file of files) {
     const ending = file.split('.').pop();
     const input = readFileSync(`test/printer/samples/${file}`, 'utf-8').replace(/\r?\n/g, '\n');
     const options = readOptions(
-        `test/printer/samples/${file.replace(`.${ending}`, '.options.json')}`,
+        `test/printer/samples/${file.replace('.only', '').replace(`.${ending}`, '.options.json')}`,
     );
 
     test(`printer: ${file.slice(0, file.length - `.${ending}`.length)}`, (t) => {
diff --git a/test/printer/samples/comments-multiple-above-blocks.html b/test/printer/samples/comments-multiple-above-blocks.html
new file mode 100644
index 0000000..16b01b7
--- /dev/null
+++ b/test/printer/samples/comments-multiple-above-blocks.html
@@ -0,0 +1,11 @@
+<!-- asd1 -->
+<!-- asd2 -->
+<script></script>
+
+<!-- asd3 -->
+<!-- asd4 -->
+<div />
+
+<!-- asd5 -->
+<!-- asd6 -->
+<style></style>
diff --git a/test/printer/samples/sort-order-none2.html b/test/printer/samples/sort-order-none2.html
index df86335..667ab2a 100644
--- a/test/printer/samples/sort-order-none2.html
+++ b/test/printer/samples/sort-order-none2.html
@@ -6,7 +6,9 @@
 
 <svelte:options />
 
+<!-- hi -->
 <p>yeah why not</p>
+
 <!-- hi -->
 <script context="module">
     const name1 = "world";