From af85d145572af19ac4136a9ece12c0d20addc452 Mon Sep 17 00:00:00 2001
From: mrkishi <mauriciokishi@gmail.com>
Date: Fri, 17 May 2019 18:22:58 -0300
Subject: [PATCH] allow end tag omission in blocks

---
 src/parse/state/mustache.ts                   |  7 ++
 src/parse/state/tag.ts                        | 37 +-------
 src/parse/utils/html.ts                       | 37 ++++++++
 .../implicitly-closed-li-block/input.svelte   |  7 ++
 .../implicitly-closed-li-block/output.json    | 89 +++++++++++++++++++
 5 files changed, 144 insertions(+), 33 deletions(-)
 create mode 100644 test/parser/samples/implicitly-closed-li-block/input.svelte
 create mode 100644 test/parser/samples/implicitly-closed-li-block/output.json

diff --git a/src/parse/state/mustache.ts b/src/parse/state/mustache.ts
index b4d0c6ad8d..e0b0301901 100644
--- a/src/parse/state/mustache.ts
+++ b/src/parse/state/mustache.ts
@@ -1,5 +1,6 @@
 import read_context from '../read/context';
 import read_expression from '../read/expression';
+import { closing_tag_omitted } from '../utils/html';
 import { whitespace } from '../../utils/patterns';
 import { trim_start, trim_end } from '../../utils/trim';
 import { Parser } from '../index';
@@ -41,6 +42,12 @@ export default function mustache(parser: Parser) {
 		let block = parser.current();
 		let expected;
 
+		if (closing_tag_omitted(block.name)) {
+			block.end = start;
+			parser.stack.pop();
+			block = parser.current();
+		}
+
 		if (block.type === 'ElseBlock' || block.type === 'PendingBlock' || block.type === 'ThenBlock' || block.type === 'CatchBlock') {
 			block.end = start;
 			parser.stack.pop();
diff --git a/src/parse/state/tag.ts b/src/parse/state/tag.ts
index 56195549d8..260f160bf2 100644
--- a/src/parse/state/tag.ts
+++ b/src/parse/state/tag.ts
@@ -1,7 +1,7 @@
 import read_expression from '../read/expression';
 import read_script from '../read/script';
 import read_style from '../read/style';
-import { decode_character_references } from '../utils/html';
+import { decode_character_references, closing_tag_omitted } from '../utils/html';
 import { is_void } from '../../utils/names';
 import { Parser } from '../index';
 import { Node } from '../../interfaces';
@@ -39,31 +39,6 @@ const specials = new Map([
 const SELF = /^svelte:self(?=[\s\/>])/;
 const COMPONENT = /^svelte:component(?=[\s\/>])/;
 
-// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission
-const disallowed_contents = new Map([
-	['li', new Set(['li'])],
-	['dt', new Set(['dt', 'dd'])],
-	['dd', new Set(['dt', 'dd'])],
-	[
-		'p',
-		new Set(
-			'address article aside blockquote div dl fieldset footer form h1 h2 h3 h4 h5 h6 header hgroup hr main menu nav ol p pre section table ul'.split(
-				' '
-			)
-		),
-	],
-	['rt', new Set(['rt', 'rp'])],
-	['rp', new Set(['rt', 'rp'])],
-	['optgroup', new Set(['optgroup'])],
-	['option', new Set(['option', 'optgroup'])],
-	['thead', new Set(['tbody', 'tfoot'])],
-	['tbody', new Set(['tbody', 'tfoot'])],
-	['tfoot', new Set(['tbody'])],
-	['tr', new Set(['tr', 'tbody'])],
-	['td', new Set(['td', 'th', 'tr'])],
-	['th', new Set(['td', 'th', 'tr'])],
-]);
-
 function parent_is_head(stack) {
 	let i = stack.length;
 	while (i--) {
@@ -173,13 +148,9 @@ export default function tag(parser: Parser) {
 		parser.stack.pop();
 
 		return;
-	} else if (disallowed_contents.has(parent.name)) {
-		// can this be a child of the parent element, or does it implicitly
-		// close it, like `<li>one<li>two`?
-		if (disallowed_contents.get(parent.name).has(name)) {
-			parent.end = start;
-			parser.stack.pop();
-		}
+	} else if (closing_tag_omitted(parent.name, name)) {
+		parent.end = start;
+		parser.stack.pop();
 	}
 
 	const unique_names = new Set();
diff --git a/src/parse/utils/html.ts b/src/parse/utils/html.ts
index b49989eacd..f5fc8a4590 100644
--- a/src/parse/utils/html.ts
+++ b/src/parse/utils/html.ts
@@ -112,3 +112,40 @@ function validate_code(code: number) {
 
 	return NUL;
 }
+
+// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission
+const disallowed_contents = new Map([
+	['li', new Set(['li'])],
+	['dt', new Set(['dt', 'dd'])],
+	['dd', new Set(['dt', 'dd'])],
+	[
+		'p',
+		new Set(
+			'address article aside blockquote div dl fieldset footer form h1 h2 h3 h4 h5 h6 header hgroup hr main menu nav ol p pre section table ul'.split(
+				' '
+			)
+		),
+	],
+	['rt', new Set(['rt', 'rp'])],
+	['rp', new Set(['rt', 'rp'])],
+	['optgroup', new Set(['optgroup'])],
+	['option', new Set(['option', 'optgroup'])],
+	['thead', new Set(['tbody', 'tfoot'])],
+	['tbody', new Set(['tbody', 'tfoot'])],
+	['tfoot', new Set(['tbody'])],
+	['tr', new Set(['tr', 'tbody'])],
+	['td', new Set(['td', 'th', 'tr'])],
+	['th', new Set(['td', 'th', 'tr'])],
+]);
+
+// can this be a child of the parent element, or does it implicitly
+// close it, like `<li>one<li>two`?
+export function closing_tag_omitted(current: string, next?: string) {
+	if (disallowed_contents.has(current)) {
+		if (!next || disallowed_contents.get(current).has(next)) {
+			return true;
+		}
+	}
+
+	return false;
+}
diff --git a/test/parser/samples/implicitly-closed-li-block/input.svelte b/test/parser/samples/implicitly-closed-li-block/input.svelte
new file mode 100644
index 0000000000..05fc778999
--- /dev/null
+++ b/test/parser/samples/implicitly-closed-li-block/input.svelte
@@ -0,0 +1,7 @@
+<ul>
+	<li>a
+	{#if true}
+		<li>b
+	{/if}
+	<li>c
+</ul>
diff --git a/test/parser/samples/implicitly-closed-li-block/output.json b/test/parser/samples/implicitly-closed-li-block/output.json
new file mode 100644
index 0000000000..bf8252f357
--- /dev/null
+++ b/test/parser/samples/implicitly-closed-li-block/output.json
@@ -0,0 +1,89 @@
+{
+	"html": {
+		"start": 0,
+		"end": 51,
+		"type": "Fragment",
+		"children": [
+			{
+				"start": 0,
+				"end": 51,
+				"type": "Element",
+				"name": "ul",
+				"attributes": [],
+				"children": [
+					{
+						"start": 4,
+						"end": 6,
+						"type": "Text",
+						"data": "\n\t"
+					},
+					{
+						"start": 6,
+						"end": 40,
+						"type": "Element",
+						"name": "li",
+						"attributes": [],
+						"children": [
+							{
+								"start": 10,
+								"end": 13,
+								"type": "Text",
+								"data": "a\n\t"
+							},
+							{
+								"start": 13,
+								"end": 38,
+								"type": "IfBlock",
+								"expression": {
+									"type": "Literal",
+									"start": 18,
+									"end": 22,
+									"value": true,
+									"raw": "true"
+								},
+								"children": [
+									{
+										"start": 26,
+										"end": 33,
+										"type": "Element",
+										"name": "li",
+										"attributes": [],
+										"children": [
+											{
+												"start": 30,
+												"end": 33,
+												"type": "Text",
+												"data": "b\n\t"
+											}
+										]
+									}
+								]
+							},
+							{
+								"start": 38,
+								"end": 40,
+								"type": "Text",
+								"data": "\n\t"
+							}
+						]
+					},
+					{
+						"start": 40,
+						"end": 46,
+						"type": "Element",
+						"name": "li",
+						"attributes": [],
+						"children": [
+							{
+								"start": 44,
+								"end": 46,
+								"type": "Text",
+								"data": "c\n"
+							}
+						]
+					}
+				]
+			}
+		]
+	}
+}