Repository: sveltejs/prettier-plugin-svelte
Pull request #130 (2020-08-29 10:59:25)

Skip formatting <style> or <script> if unsupported language or prettier-ignored
This addresses #55 : pug and stylus code being broken on format.

This change means any code in a `script` or a `style` tag with a `lang` attribute we do not support simply doesn't get reformatted. 

It's not exactly a completely solution but should at least be an improvement on the status quo. One thing that is not handled are languages configured as [default language](https://github.com/sveltejs/svelte-preprocess/blob/master/docs/getting-started.md#31-setting-default-languages) in `svelte-preprocess`.

Two other important changes:
 * if e.g. a `style` or `script` cannot be parsed, it is left as-is (and the rest of the file is formatted) rather than throwing `Error: unknown node type: Script`.
 * support `<!-- prettier-ignore -->` for style and script tags.


ehrencrona: Functionally I'd say this should be pretty much done now. I'll just see if I can add some unit tests before we merge it.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5a969e0..a441e8c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,11 +2,15 @@
 
 ## Unreleased
 
-* Fix white space issues ([#58](https://github.com/sveltejs/prettier-plugin-svelte/issues/58), [#103](https://github.com/sveltejs/prettier-plugin-svelte/issues/103), [#24](https://github.com/sveltejs/prettier-plugin-svelte/issues/24))
-* Don't format inside `<pre>` and attributes except `class` ([#28](https://github.com/sveltejs/prettier-plugin-svelte/issues/28))
-* Add curly braces when attribute shorthand is used but not allowed ([#110](https://github.com/sveltejs/prettier-plugin-svelte/issues/110))
+* (fix) make error location properties compatible with both svelte and prettier ([#71](https://github.com/sveltejs/prettier-plugin-svelte/issues/71))
+
+## 1.2.0
+
+* Don't format contents of `<pre>` or its attributes, apart from `class` ([#28](https://github.com/sveltejs/prettier-plugin-svelte/issues/28))
+* Fix whitespace issues ([#58](https://github.com/sveltejs/prettier-plugin-svelte/issues/58), [#103](https://github.com/sveltejs/prettier-plugin-svelte/issues/103), [#24](https://github.com/sveltejs/prettier-plugin-svelte/issues/24))
+* Add option to disable first level of indentation in `<script>` and `<style>` tags ([#105](https://github.com/sveltejs/prettier-plugin-svelte/issues/105))
+* Fix output when rewriting shorthand attributes to not use the shorthand syntax ([#110](https://github.com/sveltejs/prettier-plugin-svelte/issues/110))
 * Add support for object destructuring reassignment ([#113](https://github.com/sveltejs/prettier-plugin-svelte/issues/113))
-* Add option to disable first level of indent in script and style tags ([#105](https://github.com/sveltejs/prettier-plugin-svelte/issues/105))
 
 ## 1.1.1
 
diff --git a/README.md b/README.md
index 5c8840d..91cf907 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ and add your preferred configuration options:
 
 - **`svelteStrictMode`**
   - Default: `false`
-  - More strict HTML syntax: self-closed tags, quotes in attributes.
+  - More strict HTML syntax: self-closed tags, quotes in attributes, no attribute shorthand (overrules `svelteAllowShorthand`).
 
 - **`svelteAllowShorthand`**
   - Default: `true`
diff --git a/package-lock.json b/package-lock.json
index e905587..5054717 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
     "name": "prettier-plugin-svelte",
-    "version": "1.1.1",
+    "version": "1.2.0",
     "lockfileVersion": 1,
     "requires": true,
     "dependencies": {
diff --git a/package.json b/package.json
index de6643d..ec34287 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "prettier-plugin-svelte",
-    "version": "1.1.1",
+    "version": "1.2.0",
     "description": "Svelte plugin for prettier",
     "main": "plugin.js",
     "files": [
diff --git a/src/embed.ts b/src/embed.ts
index 898400e..fa1b700 100644
--- a/src/embed.ts
+++ b/src/embed.ts
@@ -1,10 +1,17 @@
-import { FastPath, Doc, doc, ParserOptions } from 'prettier';
-import { PrintFn } from './print';
-import { Node, AttributeNode, TextNode } from './print/nodes';
+import { Doc, doc, FastPath, ParserOptions } from 'prettier';
 import { getText } from './lib/getText';
+import { snippedTagContentAttribute } from './lib/snipTagContent';
+import { PrintFn } from './print';
+import {
+    getAttributeTextValue,
+    isNodeSupportedLanguage,
+    isIgnoreDirective,
+    getPreviousNode,
+} from './print/node-helpers';
+import { Node } from './print/nodes';
 
 const {
-    builders: { concat, hardline, group, indent },
+    builders: { concat, hardline, group, indent, align, literalline },
     utils: { removeLines },
 } = doc;
 
@@ -15,6 +22,7 @@ export function embed(
     options: ParserOptions,
 ): Doc | null {
     const node: Node = path.getNode();
+
     if (node.isJS) {
         return removeLines(
             textToDoc(getText(node, options), {
@@ -24,15 +32,28 @@ export function embed(
         );
     }
 
-    const indentContent = options.svelteIndentScriptAndStyle;
+    const embedType = (tag: string, parser: 'typescript' | 'css', isTopLevel: boolean) =>
+        embedTag(
+            tag,
+            path,
+            (content) => formatBodyContent(content, parser, textToDoc, options),
+            print,
+            isTopLevel,
+        );
+
+    const embedScript = (isTopLevel: boolean) => embedType('script', 'typescript', isTopLevel);
+    const embedStyle = (isTopLevel: boolean) => embedType('style', 'css', isTopLevel);
+
     switch (node.type) {
         case 'Script':
-            return embedTag('script', path, print, textToDoc, node, false, indentContent);
+            return embedScript(true);
         case 'Style':
-            return embedTag('style', path, print, textToDoc, node, false, indentContent);
+            return embedStyle(true);
         case 'Element': {
-            if (node.name === 'script' || node.name === 'style') {
-                return embedTag(node.name, path, print, textToDoc, node, true, indentContent);
+            if (node.name === 'script') {
+                return embedScript(false);
+            } else if (node.name === 'style') {
+                return embedStyle(false);
             }
         }
     }
@@ -86,44 +107,101 @@ function nukeLastLine(doc: Doc): Doc {
     return doc;
 }
 
+function preformattedBody(str: string): Doc {
+    const firstNewline = /^[\t\f\r ]*\n/;
+    const lastNewline = /\n[\t\f\r ]*$/;
+    
+    // If we do not start with a new line prettier might try to break the opening tag
+    // to keep it together with the string. Use a literal line to skip indentation.
+    return concat([
+        literalline,
+        str.replace(firstNewline, '').replace(lastNewline, ''),
+        hardline,
+    ]);
+}
+
+function getSnippedContent(node: Node) {
+    const encodedContent = getAttributeTextValue(snippedTagContentAttribute, node);
+
+    if (encodedContent) {
+        return Buffer.from(encodedContent, 'base64').toString('utf-8');
+    } else {
+        return '';
+    }
+}
+
+function formatBodyContent(
+    content: string,
+    parser: 'typescript' | 'css',
+    textToDoc: (text: string, options: object) => Doc,
+    options: ParserOptions,
+) {
+    const indentContent = options.svelteIndentScriptAndStyle;
+
+    try {
+        const indentIfDesired = (doc: Doc) => (indentContent ? indent(doc) : doc);
+
+        return concat([
+            indentIfDesired(concat([hardline, nukeLastLine(textToDoc(content, { parser }))])),
+            hardline,
+        ]);
+    } catch (error) {
+        if (process.env.PRETTIER_DEBUG) {
+            throw error;
+        }
+
+        // We will wind up here if there is a syntax error in the embedded code. If we throw an error,
+        // prettier will try to print the node with the printer. That will fail with a hard-to-interpret
+        // error message (e.g. "Unsupported node type", referring to `<script>`).
+        // Therefore, fall back on just returning the unformatted text.
+        console.error(error);
+
+        return preformattedBody(content);
+    }
+}
+
 function embedTag(
     tag: string,
     path: FastPath,
+    formatBodyContent: (content: string) => Doc,
     print: PrintFn,
-    textToDoc: (text: string, options: object) => Doc,
-    node: Node & { attributes: Node[] },
-    inline: boolean,
-    indentContent: boolean,
+    isTopLevel: boolean,
 ) {
-    const parser = tag === 'script' ? 'typescript' : 'css';
-    const contentAttribute = (node.attributes as AttributeNode[]).find(
-        n => n.name === '✂prettier:content✂',
-    );
-    let content = '';
-    if (
-        contentAttribute &&
-        Array.isArray(contentAttribute.value) &&
-        contentAttribute.value.length > 0
-    ) {
-        const encodedContent = (contentAttribute.value[0] as TextNode).data;
-        content = Buffer.from(encodedContent, 'base64').toString('utf-8');
-    }
-    node.attributes = node.attributes.filter(n => n !== contentAttribute);
+    const node: Node = path.getNode();
+    const content = getSnippedContent(node);
+    const isIgnored = isIgnoreDirective(getPreviousNode(path));
 
-    const docContent = concat([hardline, nukeLastLine(textToDoc(content, { parser }))]);
+    const body: Doc =
+        isNodeSupportedLanguage(node) && !isIgnored
+            ? content.trim() !== ''
+                ? formatBodyContent(content)
+                : hardline
+            : preformattedBody(content);
 
-    return group(
-        concat([
-            '<',
-            tag,
-            indent(group(concat(path.map(childPath => childPath.call(print), 'attributes')))),
-            '>',
-            indentContent ? indent(docContent) : docContent,
-            hardline,
-            '</',
-            tag,
-            '>',
-            inline ? '' : hardline,
-        ]),
+    const attributes = concat(
+        path.map(
+            (childPath) =>
+                childPath.getNode().name !== snippedTagContentAttribute
+                    ? childPath.call(print)
+                    : '',
+            'attributes',
+        ),
+    );
+
+    let result: Doc = group(
+        concat(['<', tag, indent(group(attributes)), '>', body, '</', tag, '>']),
     );
+
+    if (isTopLevel) {
+        // top level embedded nodes have been moved from their normal position in the
+        // node tree. if there is an ignore directive referring to it, it must be
+        // recreated at the new position.
+        if (isIgnored) {
+            result = concat(['<!-- prettier-ignore -->', hardline, result]);
+        } else {
+            result = concat([result, hardline]);
+        }
+    }
+
+    return result;
 }
diff --git a/src/index.ts b/src/index.ts
index cebe10a..be3605a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -26,12 +26,15 @@ export const parsers: Record<string, Parser> = {
             try {
                 return <ASTNode>{ ...require(`svelte/compiler`).parse(text), __isRoot: true };
             } catch (err) {
-                err.loc = {
-                    start: err.start,
-                    end: err.end,
-                };
-                delete err.start;
-                delete err.end;
+                if (err.start != null && err.end != null) {
+                    // Prettier expects error objects to have loc.start and loc.end fields. 
+                    // Svelte uses start and end directly on the error.
+                    err.loc = {
+                        start: err.start,
+                        end: err.end,
+                    };
+                }
+
                 throw err;
             }
         },
diff --git a/src/lib/snipTagContent.ts b/src/lib/snipTagContent.ts
index fb62f02..dfa7a55 100644
--- a/src/lib/snipTagContent.ts
+++ b/src/lib/snipTagContent.ts
@@ -1,20 +1,22 @@
+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');
     return source.replace(regex, (_, attributes, content) => {
         const encodedContent = Buffer.from(content).toString('base64');
-        return `<${tagName}${attributes} ✂prettier:content✂="${encodedContent}">${placeholder}</${tagName}>`;
+        return `<${tagName}${attributes} ${snippedTagContentAttribute}="${encodedContent}">${placeholder}</${tagName}>`;
     });
 }
 
 export function hasSnippedContent(text: string) {
-    return text.includes('✂prettier:content✂');
+    return text.includes(snippedTagContentAttribute);
 }
 
 export function unsnipContent(text: string): string {
-    const regex = /(<\w+.*?)\s*✂prettier:content✂="(.*?)">.*?(?=<\/)/gi
+    const regex = /(<\w+.*?)\s*✂prettier:content✂="(.*?)">.*?(?=<\/)/gi;
+
     return text.replace(regex, (_, start, encodedContent) => {
         const content = Buffer.from(encodedContent, 'base64').toString('utf8');
         return `${start}>${content}`;
     });
-    return text;
 }
diff --git a/src/options.ts b/src/options.ts
index e02399b..483285f 100644
--- a/src/options.ts
+++ b/src/options.ts
@@ -39,12 +39,14 @@ export const options: Record<keyof PluginOptions, SupportOption> = {
     svelteAllowShorthand: {
         type: 'boolean',
         default: true,
-        description: 'Option to enable/disable component attribute shorthand if attribute name and expressions are same',
+        description:
+            'Option to enable/disable component attribute shorthand if attribute name and expressions are same',
     },
     svelteIndentScriptAndStyle: {
         type: 'boolean',
         default: true,
-        description: 'Whether or not to indent the code inside <script> and <style> tags in Svelte files',
+        description:
+            'Whether or not to indent the code inside <script> and <style> tags in Svelte files',
     },
 };
 
diff --git a/src/print/index.ts b/src/print/index.ts
index 2621e9e..3d07d34 100644
--- a/src/print/index.ts
+++ b/src/print/index.ts
@@ -12,8 +12,12 @@ import {
     isInlineElement,
     isInlineNode,
     isEmptyNode,
+    printRaw,
+    isNodeSupportedLanguage,
     isLoneMustacheTag,
     isOrCanBeConvertedToShorthand,
+    isIgnoreDirective,
+    getNextNode,
 } from './node-helpers';
 import {
     isLine,
@@ -90,6 +94,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
             },
         };
         parseSortOrder(options.svelteSortOrder).forEach((p) => addParts[p]());
+        ignoreNext = false;
         return group(join(hardline, parts));
     }
 
@@ -159,6 +164,9 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
         case 'Window':
         case 'Head':
         case 'Title': {
+            const isSupportedLanguage = !(
+                node.name === 'template' && !isNodeSupportedLanguage(node)
+            );
             const isEmpty = node.children.every((child) => isEmptyNode(child));
 
             const isSelfClosingTag =
@@ -167,6 +175,18 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
                     node.type !== 'Element' ||
                     selfClosingTags.indexOf(node.name) !== -1);
 
+            let body: Doc;
+
+            if (isEmpty) {
+                body = '';
+            } else if (!isSupportedLanguage) {
+                body = printRaw(node);
+            } else if (isInlineElement(node) || isPreTagContent(path)) {
+                body = printIndentedPreservingWhitespace(path, print);
+            } else {
+                body = printIndentedWithNewlines(path, print);
+            }
+
             return group(
                 concat([
                     '<',
@@ -194,15 +214,7 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
 
                     ...(isSelfClosingTag
                         ? [options.svelteBracketNewLine ? '' : ' ', `/>`]
-                        : [
-                              '>',
-                              isEmpty
-                                  ? ''
-                                  : isInlineElement(node) || isPreTagContent(path)
-                                  ? printIndentedPreservingWhitespace(path, print)
-                                  : printIndentedWithNewlines(path, print),
-                              `</${node.name}>`,
-                          ]),
+                        : ['>', body, `</${node.name}>`]),
                 ]),
             );
         }
@@ -423,8 +435,22 @@ export function print(path: FastPath, options: ParserOptions, print: PrintFn): D
         case 'Ref':
             return concat([line, 'ref:', node.name]);
         case 'Comment': {
+            if (isIgnoreDirective(node)) {
+                /**
+                 * If there no sibling node that starts right after us, that means that node
+                 * was actually an embedded `<style>` or `<script>` node that was cut out.
+                 * If so, the ignore directive does not refer to the next line we will see.
+                 * The `embed` function handles printing the ignore directive in the right place.
+                 */
+                if (!getNextNode(path)) {
+                    return '';
+                } else {
+                    ignoreNext = true;
+                }
+            }
+
             let text = node.data;
-            ignoreNext = text.trim() === 'prettier-ignore';
+
             if (hasSnippedContent(text)) {
                 text = unsnipContent(text);
             }
diff --git a/src/print/node-helpers.ts b/src/print/node-helpers.ts
index e6e2de7..843ded4 100644
--- a/src/print/node-helpers.ts
+++ b/src/print/node-helpers.ts
@@ -1,5 +1,16 @@
-import { Node, MustacheTagNode, AttributeShorthandNode, AttributeNode } from './nodes';
+import {
+    Node,
+    ElementNode,
+    TextNode,
+    AttributeNode,
+    MustacheTagNode,
+    AttributeShorthandNode,
+} from './nodes';
 import { inlineElements, TagName } from '../lib/elements';
+import { FastPath } from 'prettier';
+import { isASTNode } from './helpers';
+
+const unsupportedLanguages = ['coffee', 'coffeescript', 'pug', 'styl', 'stylus', 'sass'];
 
 export function isInlineElement(node: Node) {
     return node.type === 'Element' && inlineElements.includes(node.name as TagName);
@@ -49,10 +60,109 @@ export function isInlineNode(node: Node): boolean {
     }
 }
 
+export function isNodeWithChildren(node: Node): node is (Node & {children: Node[]}) {
+    return (node as any).children;
+}
+
+export function getChildren(node: Node): Node[] {
+    return isNodeWithChildren(node) ? node.children : [];
+}
+
+/**
+ * Returns the previous sibling node.
+ */
+export function getPreviousNode(path: FastPath): Node | undefined {
+    const node: Node = path.getNode();
+    let parent: Node = path.getParentNode();
+
+    if (isASTNode(parent)) {
+        parent = parent.html;
+    }
+
+    return getChildren(parent).find((child) => child.end === node.start);
+}
+
+/**
+ * Returns the next sibling node.
+ */
+export function getNextNode(path: FastPath): Node | undefined {
+    const node: Node = path.getNode();
+    let parent: Node = path.getParentNode();
+
+    if (isASTNode(parent)) {
+        parent = parent.html;
+    }
+
+    return getChildren(parent).find((child) => child.start === node.end);
+}
+
 export function isEmptyNode(node: Node): boolean {
     return node.type === 'Text' && (node.raw || node.data).trim() === '';
 }
 
+export function isIgnoreDirective(node: Node | undefined): boolean {
+    return !!node && node.type === 'Comment' && node.data.trim() === 'prettier-ignore';
+}
+
+export function printRaw(node: Node): string {
+    const children: Node[] | undefined = (node as ElementNode).children;
+
+    if (children) {
+        return children.map(printRaw).join('');
+    } else {
+        return (node as TextNode).raw || '';
+    }
+}
+
+function isTextNode(node: Node): node is TextNode {
+    return node.type === 'Text';
+}
+
+function getAttributeValue(attributeName: string, node: Node) {
+    const attributes = (node as ElementNode)['attributes'] as AttributeNode[];
+
+    const langAttribute = attributes.find(
+        (attribute) => attribute.name === attributeName,
+    ) as AttributeNode | null;
+
+    return langAttribute && langAttribute.value;
+}
+
+export function getAttributeTextValue(attributeName: string, node: Node): string | null {
+    const value = getAttributeValue(attributeName, node);
+
+    if (value != null && typeof value === 'object') {
+        const textValue = value.find(isTextNode);
+
+        if (textValue) {
+            return textValue.data;
+        }
+    }
+
+    return null;
+}
+
+function getLangAttribute(node: Node): string | null {
+    const value = getAttributeTextValue('lang', node);
+
+    if (value != null) {
+        return value.replace(/^text\//, '');
+    } else {
+        return null;
+    }
+}
+
+/**
+ * Checks whether the node contains a `lang` attribute with a value corresponding to
+ * a language we cannot format. This might for example be `<template lang="pug">`.
+ * If the node does not contain a `lang` attribute, the result is true.
+ */
+export function isNodeSupportedLanguage(node: Node) {
+    const lang = getLangAttribute(node);
+
+    return !(lang && unsupportedLanguages.includes(lang));
+}
+
 export function isLoneMustacheTag(node: true | Node[]): node is [MustacheTagNode] {
     return node !== true && node.length === 1 && node[0].type === 'MustacheTag';
 }


Test patch:
diff --git a/test/formatting/index.ts b/test/formatting/index.ts
index 13ce2a8..df978bd 100644
--- a/test/formatting/index.ts
+++ b/test/formatting/index.ts
@@ -15,17 +15,51 @@ for (const dir of dirs) {
     ).replace(/\r?\n/g, '\n');
     const options = readOptions(`test/formatting/samples/${dir}/options.json`);
 
-    test(`formatting: ${dir}`, t => {
-        const actualOutput = format(input, {
-            parser: 'svelte' as any,
-            plugins: [require.resolve('../../src')],
-            tabWidth: 4,
-            ...options,
-        });
-        t.is(expectedOutput, actualOutput);
+    test(`formatting: ${dir}`, (t) => {
+        let onTestCompleted;
+
+        if ((options as any).expectSyntaxErrors) {
+            onTestCompleted = doNotLogSyntaxErrors();
+        }
+
+        try {
+            const actualOutput = format(input, {
+                parser: 'svelte' as any,
+                plugins: [require.resolve('../../src')],
+                tabWidth: 4,
+                ...options,
+            });
+
+            t.is(expectedOutput, actualOutput);
+        } finally {
+            if (onTestCompleted) {
+                onTestCompleted();
+            }
+        }
     });
 }
 
+/**
+ * Overwrite `console.error` so as to not report any syntax errors
+ * (there are tests that intentionally produce them).
+ * Returns a function that restores the original `console.error`.
+ */
+function doNotLogSyntaxErrors(): () => {} {
+    const delegate = console.error;
+
+    console.error = (...args: any[]) => {
+        const e = args[0];
+
+        if (e instanceof SyntaxError) {
+            // swallow
+        } else {
+            delegate(...args);
+        }
+    };
+
+    return () => (console.error = delegate);
+}
+
 function readOptions(fileName: string) {
     if (!existsSync(fileName)) {
         return {};
diff --git a/test/formatting/samples/syntax-error/input.html b/test/formatting/samples/syntax-error/input.html
new file mode 100644
index 0000000..b614cfb
--- /dev/null
+++ b/test/formatting/samples/syntax-error/input.html
@@ -0,0 +1,11 @@
+<script>
+
+  This is not valid JS. Do not format.
+
+</script>
+
+<p>
+
+  This should be formatted anyway.
+
+</p>
\ No newline at end of file
diff --git a/test/formatting/samples/syntax-error/options.json b/test/formatting/samples/syntax-error/options.json
new file mode 100644
index 0000000..da3820b
--- /dev/null
+++ b/test/formatting/samples/syntax-error/options.json
@@ -0,0 +1,3 @@
+{
+  "expectSyntaxErrors": true
+}
\ No newline at end of file
diff --git a/test/formatting/samples/syntax-error/output.html b/test/formatting/samples/syntax-error/output.html
new file mode 100644
index 0000000..b86679f
--- /dev/null
+++ b/test/formatting/samples/syntax-error/output.html
@@ -0,0 +1,7 @@
+<script>
+
+  This is not valid JS. Do not format.
+
+</script>
+
+<p>This should be formatted anyway.</p>
diff --git a/test/printer/samples/prettier-ignore-nested.html b/test/printer/samples/prettier-ignore-nested.html
new file mode 100644
index 0000000..58a833a
--- /dev/null
+++ b/test/printer/samples/prettier-ignore-nested.html
@@ -0,0 +1,8 @@
+<div>
+    <!-- prettier-ignore -->
+    <style>
+  .doNotFormatMe
+  {
+  }
+    </style>
+</div>
diff --git a/test/printer/samples/prettier-ignore-script.html b/test/printer/samples/prettier-ignore-script.html
new file mode 100644
index 0000000..f953bb0
--- /dev/null
+++ b/test/printer/samples/prettier-ignore-script.html
@@ -0,0 +1,7 @@
+<!-- prettier-ignore -->
+<script>
+  doNoFormat(
+)
+</script>
+
+<h1>Title</h1>
diff --git a/test/printer/samples/prettier-ignore-style.html b/test/printer/samples/prettier-ignore-style.html
new file mode 100644
index 0000000..5ce8adc
--- /dev/null
+++ b/test/printer/samples/prettier-ignore-style.html
@@ -0,0 +1,11 @@
+<script>
+</script>
+
+<!-- prettier-ignore -->
+<style>
+  .doNotFormatMe
+{
+}
+</style>
+
+<h1>Title</h1>
diff --git a/test/printer/samples/s-after-style-tag.html b/test/printer/samples/s-after-style-tag.html
index ec56051..97c3c21 100644
--- a/test/printer/samples/s-after-style-tag.html
+++ b/test/printer/samples/s-after-style-tag.html
@@ -1,5 +1,4 @@
 <style>
-
 </style>
 
 sssssweet
diff --git a/test/printer/samples/unsupported-language.html b/test/printer/samples/unsupported-language.html
new file mode 100644
index 0000000..ce39550
--- /dev/null
+++ b/test/printer/samples/unsupported-language.html
@@ -0,0 +1,19 @@
+<script lang="coffee">
+
+
+alert "This is not valid JS and should not be formatted."
+
+
+</script>
+
+<style lang="stylus">
+  h1
+    color: red
+</style>
+
+<template lang="pug">
+  h1 My Title
+
+  div
+    p Hey
+</template>