Repository: sveltejs/svelte (JavaScript) — Bug Fix

Issue:
{#each} pattern in textarea has inconsistent errors?
### Describe the bug

I am unsure if this is a unhelpful error message (bad DX) or a bug on something that should work (i think it's the latter).

```html
<textarea>
	{#each fruits as fruit}
		{fruit}
	{/each}
</textarea>
```
this gives the following error `Unexpected character '#' `

Compare this to the error you get when:
```html
<textarea readonly bind:value={somevar}>
	foo
</textarea>
```
`A <textarea> can have either a value attribute or (equivalently) child content, but not both `

A more *helpful* message.

### Reproduction

```html
<script>
	let fruits = [
		"spagetti",
		"pasta",
		"potato"
	]
</script>

<textarea >
	{#each fruits as fruit}
		{fruit}
	{/each}
</textarea>
```
[SvelteRepl](https://svelte.dev/repl/7632fbdbb52d4e64bdad0c4a6498bc4a?version=3.48.0)

### Logs

_No response_

### System Info

```shell
-svelte repl
-firefox
-windows10
```


### Severity

annoyance


Discussion:
This is just an error message that could be better. Textareas can't contain multiple DOM nodes or anything, so I really don't know what your code example would do besides something like `<textarea>{fruits.join('\n')}</textarea>` which is much more explicit about how the elements of the array should be joined.
I think users might expect an `each` behave like what you show `{fruits.join('\n')}` in your example, and that quickly becomes a source of confusion.

Relevant help thread:
<https://discord.com/channels/457912077277855764/977876658679853056>

I think a descriptive error would probably solve this

Resolving patch:
diff --git a/src/compiler/parse/errors.ts b/src/compiler/parse/errors.ts
--- a/src/compiler/parse/errors.ts
+++ b/src/compiler/parse/errors.ts
@@ -107,6 +107,14 @@ export default {
 		code: `invalid-${slug}-placement`,
 		message: `<${name}> tags cannot be inside elements or blocks`
 	}),
+	invalid_logic_block_placement: (location: string, name: string) => ({
+		code: 'invalid-logic-block-placement',
+		message: `{#${name}} logic block cannot be ${location}`
+	}),
+	invalid_tag_placement: (location: string, name: string) => ({
+		code: 'invalid-tag-placement',
+		message: `{@${name}} tag cannot be ${location}`
+	}),
 	invalid_ref_directive: (name: string) => ({
 		code: 'invalid-ref-directive',
 		message: `The ref directive is no longer supported — use \`bind:this={${name}}\` instead`
diff --git a/src/compiler/parse/state/tag.ts b/src/compiler/parse/state/tag.ts
--- a/src/compiler/parse/state/tag.ts
+++ b/src/compiler/parse/state/tag.ts
@@ -219,7 +219,8 @@ export default function tag(parser: Parser) {
 		element.children = read_sequence(
 			parser,
 			() =>
-				/^<\/textarea(\s[^>]*)?>/i.test(parser.template.slice(parser.index))
+				/^<\/textarea(\s[^>]*)?>/i.test(parser.template.slice(parser.index)),
+			'inside <textarea>'
 		);
 		parser.read(/^<\/textarea(\s[^>]*)?>/i);
 		element.end = parser.index;
@@ -473,7 +474,7 @@ function read_attribute_value(parser: Parser) {
 
 	let value;
 	try {
-		value = read_sequence(parser, () => !!parser.match_regex(regex));
+		value = read_sequence(parser, () => !!parser.match_regex(regex), 'in attribute value');
 	} catch (error) {
 		if (error.code === 'parse-error') {
 			// if the attribute value didn't close + self-closing tag
@@ -495,7 +496,7 @@ function read_attribute_value(parser: Parser) {
 	return value;
 }
 
-function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
+function read_sequence(parser: Parser, done: () => boolean, location: string): TemplateNode[] {
 	let current_chunk: Text = {
 		start: parser.index,
 		end: null,
@@ -521,6 +522,18 @@ function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
 			flush(parser.index);
 			return chunks;
 		} else if (parser.eat('{')) {
+			if (parser.match('#')) {
+				const index = parser.index - 1;
+				parser.eat('#');
+				const name = parser.read_until(/[^a-z]/);
+				parser.error(parser_errors.invalid_logic_block_placement(location, name), index);
+			} else if (parser.match('@')) {
+				const index = parser.index - 1;
+				parser.eat('@');
+				const name = parser.read_until(/[^a-z]/);
+				parser.error(parser_errors.invalid_tag_placement(location, name), index);
+			}
+
 			flush(parser.index - 1);
 
 			parser.allow_whitespace();


Test patch:
diff --git a/test/validator/samples/html-block-in-attribute/errors.json b/test/validator/samples/html-block-in-attribute/errors.json
new file mode 100644
--- /dev/null
+++ b/test/validator/samples/html-block-in-attribute/errors.json
@@ -0,0 +1,9 @@
+[
+	{
+		"code": "invalid-tag-placement",
+		"message": "{@html} tag cannot be in attribute value",
+		"start": { "line": 1, "column": 12, "character": 12 },
+		"end": { "line": 1, "column": 12, "character": 12 },
+		"pos": 12
+	}
+]
diff --git a/test/validator/samples/html-block-in-attribute/input.svelte b/test/validator/samples/html-block-in-attribute/input.svelte
new file mode 100644
--- /dev/null
+++ b/test/validator/samples/html-block-in-attribute/input.svelte
@@ -0,0 +1 @@
+<div style="{@html text}" />
\ No newline at end of file
diff --git a/test/validator/samples/html-block-in-textarea/errors.json b/test/validator/samples/html-block-in-textarea/errors.json
new file mode 100644
--- /dev/null
+++ b/test/validator/samples/html-block-in-textarea/errors.json
@@ -0,0 +1,9 @@
+[
+	{
+		"code": "invalid-tag-placement",
+		"message": "{@html} tag cannot be inside <textarea>",
+		"start": { "line": 2, "column": 1, "character": 12 },
+		"end": { "line": 2, "column": 1, "character": 12 },
+		"pos": 12
+	}
+]
diff --git a/test/validator/samples/html-block-in-textarea/input.svelte b/test/validator/samples/html-block-in-textarea/input.svelte
new file mode 100644
--- /dev/null
+++ b/test/validator/samples/html-block-in-textarea/input.svelte
@@ -0,0 +1,3 @@
+<textarea>
+	{@html fruit}
+</textarea>
\ No newline at end of file
diff --git a/test/validator/samples/logic-block-in-attribute/errors.json b/test/validator/samples/logic-block-in-attribute/errors.json
new file mode 100644
--- /dev/null
+++ b/test/validator/samples/logic-block-in-attribute/errors.json
@@ -0,0 +1,9 @@
+[
+	{
+		"code": "invalid-logic-block-placement",
+		"message": "{#if} logic block cannot be in attribute value",
+		"start": { "line": 1, "column": 12, "character": 12 },
+		"end": { "line": 1, "column": 12, "character": 12 },
+		"pos": 12
+	}
+]
diff --git a/test/validator/samples/logic-block-in-attribute/input.svelte b/test/validator/samples/logic-block-in-attribute/input.svelte
new file mode 100644
--- /dev/null
+++ b/test/validator/samples/logic-block-in-attribute/input.svelte
@@ -0,0 +1 @@
+<div style="{#if condition}a{/if}" />
\ No newline at end of file
diff --git a/test/validator/samples/logic-block-in-textarea/errors.json b/test/validator/samples/logic-block-in-textarea/errors.json
new file mode 100644
--- /dev/null
+++ b/test/validator/samples/logic-block-in-textarea/errors.json
@@ -0,0 +1,9 @@
+[
+	{
+		"code": "invalid-logic-block-placement",
+		"message": "{#each} logic block cannot be inside <textarea>",
+		"start": { "line": 2, "column": 1, "character": 12 },
+		"end": { "line": 2, "column": 1, "character": 12 },
+		"pos": 12
+	}
+]
diff --git a/test/validator/samples/logic-block-in-textarea/input.svelte b/test/validator/samples/logic-block-in-textarea/input.svelte
new file mode 100644
--- /dev/null
+++ b/test/validator/samples/logic-block-in-textarea/input.svelte
@@ -0,0 +1,5 @@
+<textarea>
+	{#each fruits as fruit}
+		{fruit}
+	{/each}
+</textarea>
\ No newline at end of file