Repository: sveltejs/svelte (JavaScript)
Date: 2022-02-17 10:43:25+00:00
Task type: Bug Fix

Issue:
Handle `<pre>`whitespace formatting edge case (no new line)
The `<pre>` tag whitespace handling reported in #6437 was largely fixed. There's one detail though that's incorrect: Svelte seems to be adding an [extra whitespace when there's a new line at the beginning](https://svelte.dev/repl/986c1b817bd24ca3803e39f0db7506d0?version=3.46.4) when this does not happen [using plain HTML](https://codepen.io/pabloabc/pen/qBVXKOv) unless there's an explicit white space after the `<pre>` tag.

_Originally posted by @pablo-abc in https://github.com/sveltejs/svelte/issues/6437#issuecomment-1038543615_


Issue discussion:

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,8 @@
 * Fix `{@const}` tag not working inside Component when there's no `let:` [#7189](https://github.com/sveltejs/svelte/issues/7189)
 * Ignore comments in `{#each}` blocks when containing elements with `animate:` ([#3999](https://github.com/sveltejs/svelte/issues/3999))
 * Add a third parameter to the returned function of `createEventDispatcher` that allows passing an object of `{ cancelable: true }` to create a cancelable custom event. The returned function when called will also return a boolean depending on whether the event is cancelled ([#7064](https://github.com/sveltejs/svelte/pull/7064))
-* Fix value of `let:` bindings not updating in certain cases ([#7440](https://github.com/sveltejs/svelte/issues/7440)) 
+* Fix value of `let:` bindings not updating in certain cases ([#7440](https://github.com/sveltejs/svelte/issues/7440))
+* Strip leading newline after `<pre>` and `<textarea>` ([#7264](https://github.com/sveltejs/svelte/issues/7264))
 
 ## 3.47.0
 
diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts
--- a/src/compiler/compile/nodes/Element.ts
+++ b/src/compiler/compile/nodes/Element.ts
@@ -11,7 +11,7 @@ import StyleDirective from './StyleDirective';
 import Text from './Text';
 import { namespaces } from '../../utils/namespaces';
 import map_children from './shared/map_children';
-import { dimensions } from '../../utils/patterns';
+import { dimensions, start_newline } from '../../utils/patterns';
 import fuzzymatch from '../../utils/fuzzymatch';
 import list from '../../utils/list';
 import Let from './Let';
@@ -216,6 +216,20 @@ export default class Element extends Node {
 		this.namespace = get_namespace(parent as Element, this, component.namespace);
 
 		if (this.namespace !== namespaces.foreign) {
+			if (this.name === 'pre' || this.name === 'textarea') {
+				const first = info.children[0];
+				if (first && first.type === 'Text') {
+					// The leading newline character needs to be stripped because of a qirk,
+					// it is ignored by browsers if the tag and its contents are set through
+					// innerHTML (NOT if set through the innerHTML of the tag or dynamically).
+					// Therefore strip it here but add it back in the appropriate
+					// places if there's another newline afterwards.
+					// see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions
+					// see https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
+					first.data = first.data.replace(start_newline, '');
+				}
+			}
+
 			if (this.name === 'textarea') {
 				if (info.children.length > 0) {
 					const value_attribute = info.attributes.find(node => node.name === 'value');
diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts
--- a/src/compiler/compile/render_dom/wrappers/Element/index.ts
+++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts
@@ -12,7 +12,7 @@ import { namespaces } from '../../../../utils/namespaces';
 import AttributeWrapper from './Attribute';
 import StyleAttributeWrapper from './StyleAttribute';
 import SpreadAttributeWrapper from './SpreadAttribute';
-import { dimensions } from '../../../../utils/patterns';
+import { dimensions, start_newline } from '../../../../utils/patterns';
 import Binding from './Binding';
 import add_to_set from '../../../utils/add_to_set';
 import { add_event_handler } from '../shared/add_event_handlers';
@@ -1114,6 +1114,9 @@ export default class ElementWrapper extends Wrapper {
 function to_html(wrappers: Array<ElementWrapper | TextWrapper | MustacheTagWrapper | RawMustacheTagWrapper>, block: Block, literal: any, state: any, can_use_raw_text?: boolean) {
 	wrappers.forEach(wrapper => {
 		if (wrapper instanceof TextWrapper) {
+			// Don't add the <pre>/<textare> newline logic here because pre/textarea.innerHTML
+			// would keep the leading newline, too, only someParent.innerHTML = '..<pre/textarea>..' won't
+
 			if ((wrapper as TextWrapper).use_space()) state.quasi.value.raw += ' ';
 
 			const parent = wrapper.node.parent as Element;
@@ -1141,22 +1144,16 @@ function to_html(wrappers: Array<ElementWrapper | TextWrapper | MustacheTagWrapp
 			// element
 			state.quasi.value.raw += `<${wrapper.node.name}`;
 
+			const is_empty_textarea = wrapper.node.name === 'textarea' && wrapper.fragment.nodes.length === 0;
+
 			(wrapper as ElementWrapper).attributes.forEach((attr: AttributeWrapper) => {
+				if (is_empty_textarea && attr.node.name === 'value') {
+					// The value attribute of <textarea> renders as content.
+					return;
+				}
 				state.quasi.value.raw += ` ${fix_attribute_casing(attr.node.name)}="`;
 
-				attr.node.chunks.forEach(chunk => {
-					if (chunk.type === 'Text') {
-						state.quasi.value.raw += escape_html(chunk.data);
-					} else {
-						literal.quasis.push(state.quasi);
-						literal.expressions.push(chunk.manipulate(block));
-
-						state.quasi = {
-							type: 'TemplateElement',
-							value: { raw: '' }
-						};
-					}
-				});
+				to_html_for_attr_value(attr, block, literal, state);
 
 				state.quasi.value.raw += '"';
 			});
@@ -1164,6 +1161,29 @@ function to_html(wrappers: Array<ElementWrapper | TextWrapper | MustacheTagWrapp
 			if (!wrapper.void) {
 				state.quasi.value.raw += '>';
 
+				if (wrapper.node.name === 'pre') {
+					// Two or more leading newlines are required to restore the leading newline immediately after `<pre>`.
+					// see https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
+					const first = wrapper.fragment.nodes[0];
+					if (first && first.node.type === 'Text' && start_newline.test(first.node.data)) {
+						state.quasi.value.raw += '\n';
+					}
+				}
+
+				if (is_empty_textarea) {
+					// The <textarea> renders the value attribute as content because the content is stored in the value attribute.
+					const value_attribute = wrapper.attributes.find(attr => attr.node.name === 'value');
+					if (value_attribute) {
+						// Two or more leading newlines are required to restore the leading newline immediately after `<textarea>`.
+						// see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions
+						const first = value_attribute.node.chunks[0];
+						if (first && first.type === 'Text' && start_newline.test(first.data)) {
+							state.quasi.value.raw += '\n';
+						}
+						to_html_for_attr_value(value_attribute, block, literal, state);
+					}
+				}
+
 				to_html(wrapper.fragment.nodes as Array<ElementWrapper | TextWrapper>, block, literal, state);
 
 				state.quasi.value.raw += `</${wrapper.node.name}>`;
@@ -1173,3 +1193,19 @@ function to_html(wrappers: Array<ElementWrapper | TextWrapper | MustacheTagWrapp
 		}
 	});
 }
+
+function to_html_for_attr_value(attr: AttributeWrapper | StyleAttributeWrapper | SpreadAttributeWrapper, block: Block, literal: any, state: any) {
+	attr.node.chunks.forEach(chunk => {
+		if (chunk.type === 'Text') {
+			state.quasi.value.raw += escape_html(chunk.data);
+		} else {
+			literal.quasis.push(state.quasi);
+			literal.expressions.push(chunk.manipulate(block));
+
+			state.quasi = {
+				type: 'TemplateElement',
+				value: { raw: '' }
+			};
+		}
+	});
+}
diff --git a/src/compiler/compile/render_ssr/handlers/Element.ts b/src/compiler/compile/render_ssr/handlers/Element.ts
--- a/src/compiler/compile/render_ssr/handlers/Element.ts
+++ b/src/compiler/compile/render_ssr/handlers/Element.ts
@@ -8,6 +8,7 @@ import Expression from '../../nodes/shared/Expression';
 import remove_whitespace_children from './utils/remove_whitespace_children';
 import fix_attribute_casing from '../../render_dom/wrappers/Element/fix_attribute_casing';
 import { namespaces } from '../../../utils/namespaces';
+import { start_newline } from '../../../utils/patterns';
 import { Expression as ESExpression } from 'estree';
 
 export default function (node: Element, renderer: Renderer, options: RenderOptions) {
@@ -42,7 +43,7 @@ export default function (node: Element, renderer: Renderer, options: RenderOptio
 		const { name, expression: { node: expression } } = style_directive;
 		return p`"${name}": ${expression}`;
 	});
-	
+
 	const style_expression =
 		style_expression_list.length > 0 &&
 		x`{ ${style_expression_list} }`;
@@ -166,11 +167,31 @@ export default function (node: Element, renderer: Renderer, options: RenderOptio
 
 			renderer.add_expression(x`($$value => $$value === void 0 ? ${result} : $$value)(${node_contents})`);
 		} else {
+			if (node.name === 'textarea') {
+				// Two or more leading newlines are required to restore the leading newline immediately after `<textarea>`.
+				// see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions
+				const value_attribute = node.attributes.find(({ name }) => name === 'value');
+				if (value_attribute) {
+					const first = value_attribute.chunks[0];
+					if (first && first.type === 'Text' && start_newline.test(first.data)) {
+						renderer.add_string('\n');
+					}
+				}
+			}
 			renderer.add_expression(node_contents);
 		}
 
 		add_close_tag();
 	} else {
+		if (node.name === 'pre') {
+			// Two or more leading newlines are required to restore the leading newline immediately after `<pre>`.
+			// see https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
+			// see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions
+			const first = children[0];
+			if (first && first.type === 'Text' && start_newline.test(first.data)) {
+				renderer.add_string('\n');
+			}
+		}
 		renderer.render(children, options);
 		add_close_tag();
 	}
diff --git a/src/compiler/utils/patterns.ts b/src/compiler/utils/patterns.ts
--- a/src/compiler/utils/patterns.ts
+++ b/src/compiler/utils/patterns.ts
@@ -1,5 +1,6 @@
 export const whitespace = /[ \t\r\n]/;
 export const start_whitespace = /^[ \t\r\n]*/;
 export const end_whitespace = /[ \t\r\n]*$/;
+export const start_newline = /^\r?\n/;
 
 export const dimensions = /^(?:offset|client)(?:Width|Height)$/;


Test patch:
diff --git a/test/runtime/samples/pre-tag/.editorconfig b/test/runtime/samples/pre-tag/.editorconfig
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/pre-tag/.editorconfig
@@ -0,0 +1,2 @@
+[main.svelte]
+trim_trailing_whitespace = unset
diff --git a/test/runtime/samples/pre-tag/_config.js b/test/runtime/samples/pre-tag/_config.js
--- a/test/runtime/samples/pre-tag/_config.js
+++ b/test/runtime/samples/pre-tag/_config.js
@@ -6,23 +6,14 @@ export default {
 		const elementDiv = target.querySelector('#div');
 		// Test for <pre> tag in non <pre> tag
 		const elementDivWithPre = target.querySelector('#div-with-pre');
-
-		// There is a slight difference in innerHTML because there is a difference in HTML optimization (in jsdom)
-		// depending on how the innerHTML is set.
-		// (There is no difference in the display.)
-		// Reassign innerHTML to add the same optimizations to innerHTML.
-
-		// eslint-disable-next-line no-self-assign
-		elementPre.innerHTML = elementPre.innerHTML;
-		// eslint-disable-next-line no-self-assign
-		elementDiv.innerHTML = elementDiv.innerHTML;
-		// eslint-disable-next-line no-self-assign
-		elementDivWithPre.innerHTML = elementDivWithPre.innerHTML;
+		// Test for <pre> tag with leading newline
+		const elementPreWithLeadingNewline = target.querySelector('#pre-with-leading-newline');
+		const elementPreWithoutLeadingNewline = target.querySelector('#pre-without-leading-newline');
+		const elementPreWithMultipleLeadingNewline = target.querySelector('#pre-with-multiple-leading-newlines');
 
 		assert.equal(
 			elementPre.innerHTML,
-			`
-  A
+			`  A
   B
   <span>
     C
@@ -53,5 +44,12 @@ export default {
     F
   </pre>`
 		);
+		assert.equal(elementPreWithLeadingNewline.children[0].innerHTML, 'leading newline');
+		assert.equal(elementPreWithLeadingNewline.children[1].innerHTML, '  leading newline and spaces');
+		assert.equal(elementPreWithLeadingNewline.children[2].innerHTML, '\nleading newlines');
+		assert.equal(elementPreWithoutLeadingNewline.children[0].innerHTML, 'without spaces');
+		assert.equal(elementPreWithoutLeadingNewline.children[1].innerHTML, '  with spaces  ');
+		assert.equal(elementPreWithoutLeadingNewline.children[2].innerHTML, ' \nnewline after leading space');
+		assert.equal(elementPreWithMultipleLeadingNewline.innerHTML, '\n\nmultiple leading newlines');
 	}
 };
diff --git a/test/runtime/samples/pre-tag/main.svelte b/test/runtime/samples/pre-tag/main.svelte
--- a/test/runtime/samples/pre-tag/main.svelte
+++ b/test/runtime/samples/pre-tag/main.svelte
@@ -32,3 +32,25 @@
     F
   </pre>
 </div>
+
+<div id="pre-with-leading-newline">
+  <pre>
+leading newline</pre>
+  <pre>
+  leading newline and spaces</pre>
+  <pre>
+
+leading newlines</pre>
+</div>
+
+<div id="pre-without-leading-newline">
+  <pre>without spaces</pre>
+  <pre>  with spaces  </pre>
+  <pre> 
+newline after leading space</pre>
+</div>
+
+<pre id="pre-with-multiple-leading-newlines">
+
+
+multiple leading newlines</pre>
diff --git a/test/runtime/samples/preserve-whitespaces/_config.js b/test/runtime/samples/preserve-whitespaces/_config.js
--- a/test/runtime/samples/preserve-whitespaces/_config.js
+++ b/test/runtime/samples/preserve-whitespaces/_config.js
@@ -10,22 +10,9 @@ export default {
 		// Test for <pre> tag in non <pre> tag
 		const elementDivWithPre = target.querySelector('#div-with-pre');
 
-		// There is a slight difference in innerHTML because there is a difference in HTML optimization (in jsdom)
-		// depending on how the innerHTML is set.
-		// (There is no difference in the display.)
-		// Reassign innerHTML to add the same optimizations to innerHTML.
-
-		// eslint-disable-next-line no-self-assign
-		elementPre.innerHTML = elementPre.innerHTML;
-		// eslint-disable-next-line no-self-assign
-		elementDiv.innerHTML = elementDiv.innerHTML;
-		// eslint-disable-next-line no-self-assign
-		elementDivWithPre.innerHTML = elementDivWithPre.innerHTML;
-
 		assert.equal(
 			elementPre.innerHTML,
-			`
-  A
+			`  A
   B
   <span>
     C
diff --git a/test/runtime/samples/textarea-children/_config.js b/test/runtime/samples/textarea-children/_config.js
--- a/test/runtime/samples/textarea-children/_config.js
+++ b/test/runtime/samples/textarea-children/_config.js
@@ -9,9 +9,9 @@ export default {
 
 	test({ assert, component, target }) {
 		const textarea = target.querySelector( 'textarea' );
-		assert.strictEqual( textarea.value, '\n\t<p>not actually an element. 42</p>\n' );
+		assert.strictEqual( textarea.value, '\t<p>not actually an element. 42</p>\n' );
 
 		component.foo = 43;
-		assert.strictEqual( textarea.value, '\n\t<p>not actually an element. 43</p>\n' );
+		assert.strictEqual( textarea.value, '\t<p>not actually an element. 43</p>\n' );
 	}
 };
diff --git a/test/runtime/samples/textarea-content/.editorconfig b/test/runtime/samples/textarea-content/.editorconfig
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/textarea-content/.editorconfig
@@ -0,0 +1,2 @@
+[main.svelte]
+trim_trailing_whitespace = unset
diff --git a/test/runtime/samples/textarea-content/_config.js b/test/runtime/samples/textarea-content/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/textarea-content/_config.js
@@ -0,0 +1,34 @@
+export default {
+	test({ assert, target }) {
+		// Test for <textarea> tag
+		const elementTextarea = target.querySelector('#textarea');
+		// Test for <textarea> tag in non <textarea> tag
+		const elementDivWithTextarea = target.querySelector('#div-with-textarea');
+		// Test for <textarea> tag with leading newline
+		const elementTextareaWithLeadingNewline = target.querySelector('#textarea-with-leading-newline');
+		const elementTextareaWithoutLeadingNewline = target.querySelector('#textarea-without-leading-newline');
+		const elementTextareaWithMultipleLeadingNewline = target.querySelector('#textarea-with-multiple-leading-newlines');
+		const elementDivWithTextareaWithMultipleLeadingNewline = target.querySelector('#div-with-textarea-with-multiple-leading-newlines');
+
+		assert.equal(
+			elementTextarea.value,
+			`  A
+  B
+`
+		);
+		assert.equal(
+			elementDivWithTextarea.children[0].value,
+			`    A
+    B
+  `
+		);
+		assert.equal(elementTextareaWithLeadingNewline.children[0].value, 'leading newline');
+		assert.equal(elementTextareaWithLeadingNewline.children[1].value, '  leading newline and spaces');
+		assert.equal(elementTextareaWithLeadingNewline.children[2].value, '\nleading newlines');
+		assert.equal(elementTextareaWithoutLeadingNewline.children[0].value, 'without spaces');
+		assert.equal(elementTextareaWithoutLeadingNewline.children[1].value, '  with spaces  ');
+		assert.equal(elementTextareaWithoutLeadingNewline.children[2].value, ' \nnewline after leading space');
+		assert.equal(elementTextareaWithMultipleLeadingNewline.value, '\n\nmultiple leading newlines');
+		assert.equal(elementDivWithTextareaWithMultipleLeadingNewline.children[0].value, '\n\nmultiple leading newlines');
+	}
+};
diff --git a/test/runtime/samples/textarea-content/main.svelte b/test/runtime/samples/textarea-content/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/textarea-content/main.svelte
@@ -0,0 +1,40 @@
+<textarea id="textarea">
+  A
+  B
+</textarea>
+
+<div id="div-with-textarea">
+  <textarea>
+    A
+    B
+  </textarea>
+</div>
+
+<div id="textarea-with-leading-newline">
+  <textarea>
+leading newline</textarea>
+  <textarea>
+  leading newline and spaces</textarea>
+  <textarea>
+
+leading newlines</textarea>
+</div>
+
+<div id="textarea-without-leading-newline">
+  <textarea>without spaces</textarea>
+  <textarea>  with spaces  </textarea>
+  <textarea> 
+newline after leading space</textarea>
+</div>
+
+<textarea id="textarea-with-multiple-leading-newlines">
+
+
+multiple leading newlines</textarea>
+
+<div id="div-with-textarea-with-multiple-leading-newlines">
+  <textarea>
+
+
+multiple leading newlines</textarea>
+</div>
diff --git a/test/server-side-rendering/index.ts b/test/server-side-rendering/index.ts
--- a/test/server-side-rendering/index.ts
+++ b/test/server-side-rendering/index.ts
@@ -84,7 +84,7 @@ describe('ssr', () => {
 
 				try {
 					if (config.withoutNormalizeHtml) {
-						assert.strictEqual(html.trim(), expectedHtml.trim().replace(/\r\n/g, '\n'));
+						assert.strictEqual(html.trim().replace(/\r\n/g, '\n'), expectedHtml.trim().replace(/\r\n/g, '\n'));
 					} else {
 						(compileOptions.preserveComments
 							? assert.htmlEqualWithComments
diff --git a/test/server-side-rendering/samples/pre-tag/.editorconfig b/test/server-side-rendering/samples/pre-tag/.editorconfig
new file mode 100644
--- /dev/null
+++ b/test/server-side-rendering/samples/pre-tag/.editorconfig
@@ -0,0 +1,2 @@
+[{main.svelte,_expected.html}]
+trim_trailing_whitespace = unset
diff --git a/test/server-side-rendering/samples/pre-tag/_expected.html b/test/server-side-rendering/samples/pre-tag/_expected.html
--- a/test/server-side-rendering/samples/pre-tag/_expected.html
+++ b/test/server-side-rendering/samples/pre-tag/_expected.html
@@ -1,5 +1,4 @@
-<pre>
-  A
+<pre>  A
   B
   <span>
     C
@@ -18,8 +17,7 @@
   F
 </div>
 
-<div><pre>
-    A
+<div><pre>    A
     B
     <span>
       C
@@ -28,3 +26,14 @@
     E
     F
   </pre></div>
+
+<div id="pre-with-leading-newline"><pre>leading newline</pre>
+  <pre>  leading newline and spaces</pre>
+  <pre>
+
+leading newlines</pre></div>
+
+<div id="pre-without-leading-newline"><pre>without spaces</pre>
+  <pre>  with spaces  </pre>
+  <pre> 
+newline after leading space</pre></div>
diff --git a/test/server-side-rendering/samples/pre-tag/main.svelte b/test/server-side-rendering/samples/pre-tag/main.svelte
--- a/test/server-side-rendering/samples/pre-tag/main.svelte
+++ b/test/server-side-rendering/samples/pre-tag/main.svelte
@@ -32,3 +32,20 @@
     F
   </pre>
 </div>
+
+<div id="pre-with-leading-newline">
+  <pre>
+leading newline</pre>
+  <pre>
+  leading newline and spaces</pre>
+  <pre>
+
+leading newlines</pre>
+</div>
+
+<div id="pre-without-leading-newline">
+  <pre>without spaces</pre>
+  <pre>  with spaces  </pre>
+  <pre> 
+newline after leading space</pre>
+</div>
diff --git a/test/server-side-rendering/samples/preserve-whitespaces/_expected.html b/test/server-side-rendering/samples/preserve-whitespaces/_expected.html
--- a/test/server-side-rendering/samples/preserve-whitespaces/_expected.html
+++ b/test/server-side-rendering/samples/preserve-whitespaces/_expected.html
@@ -1,5 +1,4 @@
-<pre>
-  A
+<pre>  A
   B
   <span>
     C
@@ -21,8 +20,7 @@
 </div>
 
 <div>
-  <pre>
-    A
+  <pre>    A
     B
     <span>
       C
diff --git a/test/server-side-rendering/samples/textarea-content/.editorconfig b/test/server-side-rendering/samples/textarea-content/.editorconfig
new file mode 100644
--- /dev/null
+++ b/test/server-side-rendering/samples/textarea-content/.editorconfig
@@ -0,0 +1,2 @@
+[{main.svelte,_expected.html}]
+trim_trailing_whitespace = unset
diff --git a/test/server-side-rendering/samples/textarea-content/_config.js b/test/server-side-rendering/samples/textarea-content/_config.js
new file mode 100644
--- /dev/null
+++ b/test/server-side-rendering/samples/textarea-content/_config.js
@@ -0,0 +1,3 @@
+export default {
+	withoutNormalizeHtml: true
+};
diff --git a/test/server-side-rendering/samples/textarea-content/_expected.html b/test/server-side-rendering/samples/textarea-content/_expected.html
new file mode 100644
--- /dev/null
+++ b/test/server-side-rendering/samples/textarea-content/_expected.html
@@ -0,0 +1,28 @@
+<textarea id="textarea">  A
+  B
+</textarea>
+
+<div id="div-with-textarea"><textarea>    A
+    B
+  </textarea></div>
+
+<div id="textarea-with-leading-newline"><textarea>leading newline</textarea>
+  <textarea>  leading newline and spaces</textarea>
+  <textarea>
+
+leading newlines</textarea></div>
+
+<div id="textarea-without-leading-newline"><textarea>without spaces</textarea>
+  <textarea>  with spaces  </textarea>
+  <textarea> 
+newline after leading space</textarea></div>
+
+<textarea id="textarea-with-multiple-leading-newlines">
+
+
+multiple leading newlines</textarea>
+
+<div id="div-with-textarea-with-multiple-leading-newlines"><textarea>
+
+
+multiple leading newlines</textarea></div>
diff --git a/test/server-side-rendering/samples/textarea-content/main.svelte b/test/server-side-rendering/samples/textarea-content/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/server-side-rendering/samples/textarea-content/main.svelte
@@ -0,0 +1,40 @@
+<textarea id="textarea">
+  A
+  B
+</textarea>
+
+<div id="div-with-textarea">
+  <textarea>
+    A
+    B
+  </textarea>
+</div>
+
+<div id="textarea-with-leading-newline">
+  <textarea>
+leading newline</textarea>
+  <textarea>
+  leading newline and spaces</textarea>
+  <textarea>
+
+leading newlines</textarea>
+</div>
+
+<div id="textarea-without-leading-newline">
+  <textarea>without spaces</textarea>
+  <textarea>  with spaces  </textarea>
+  <textarea> 
+newline after leading space</textarea>
+</div>
+
+<textarea id="textarea-with-multiple-leading-newlines">
+
+
+multiple leading newlines</textarea>
+
+<div id="div-with-textarea-with-multiple-leading-newlines">
+  <textarea>
+
+
+multiple leading newlines</textarea>
+</div>