Skip to content

Commit c61cc94

Browse files
committed
more unclosed tag handling
1 parent 7b588e5 commit c61cc94

File tree

6 files changed

+465
-57
lines changed

6 files changed

+465
-57
lines changed

packages/svelte/src/compiler/phases/1-parse/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ export class Parser {
2222
*/
2323
template;
2424

25+
/**
26+
* @readonly
27+
* @type {string}
28+
*/
29+
template_untrimmed;
30+
2531
/**
2632
* Whether or not we're in loose parsing mode, in which
2733
* case we try to continue parsing as much as possible
@@ -60,6 +66,7 @@ export class Parser {
6066
}
6167

6268
this.loose = loose;
69+
this.template_untrimmed = template;
6370
this.template = template.trimEnd();
6471

6572
let match_lang;

packages/svelte/src/compiler/phases/1-parse/state/element.js

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ import { decode_character_references } from '../utils/html.js';
99
import * as e from '../../../errors.js';
1010
import * as w from '../../../warnings.js';
1111
import { create_fragment } from '../utils/create.js';
12-
import { create_attribute, create_expression_metadata } from '../../nodes.js';
12+
import { create_attribute, create_expression_metadata, is_element_node } from '../../nodes.js';
1313
import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js';
1414
import { closing_tag_omitted } from '../../../../html-tree-validation.js';
1515
import { list } from '../../../utils/string.js';
16+
import { regex_whitespace } from '../../patterns.js';
1617

1718
const regex_invalid_unquoted_attribute_value = /^(\/>|[\s"'=<>`])/;
1819
const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
@@ -80,6 +81,18 @@ export default function element(parser) {
8081

8182
// close any elements that don't have their own closing tags, e.g. <div><p></div>
8283
while (/** @type {AST.RegularElement} */ (parent).name !== name) {
84+
if (parser.loose) {
85+
// If the previous element did interpret the next opening tag as an attribute, backtrack
86+
if (is_element_node(parent)) {
87+
const last = parent.attributes.at(-1);
88+
if (last?.type === 'Attribute' && last.name === `<${name}`) {
89+
parser.index = last.start;
90+
parent.attributes.pop();
91+
break;
92+
}
93+
}
94+
}
95+
8396
if (parent.type !== 'RegularElement' && !parser.loose) {
8497
if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) {
8598
e.element_invalid_closing_tag_autoclosed(start, name, parser.last_auto_closed_tag.reason);
@@ -328,16 +341,27 @@ export default function element(parser) {
328341
if (last?.type === 'Attribute' && last.name === '<') {
329342
parser.index = last.start;
330343
element.attributes.pop();
331-
}
332-
// ... or we may have eaten part of a following block
333-
else {
344+
} else {
345+
// ... or we may have eaten part of a following block ...
334346
const prev_1 = parser.template[parser.index - 1];
335347
const prev_2 = parser.template[parser.index - 2];
336348
const current = parser.template[parser.index];
337349
if (prev_2 === '{' && prev_1 === '/') {
338350
parser.index -= 2;
339351
} else if (prev_1 === '{' && (current === '#' || current === '@' || current === ':')) {
340352
parser.index -= 1;
353+
} else {
354+
// ... or we're followed by whitespace, for example near the end of the template,
355+
// which we want to take in so that language tools has more room to work with
356+
parser.allow_whitespace();
357+
if (parser.index === parser.template.length) {
358+
while (
359+
parser.index < parser.template_untrimmed.length &&
360+
regex_whitespace.test(parser.template_untrimmed[parser.index])
361+
) {
362+
parser.index++;
363+
}
364+
}
341365
}
342366
}
343367
}

packages/svelte/tests/parser-legacy/samples/loose-unclosed-tag/input.svelte

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,22 @@
33
</div>
44

55
<div>
6-
<span>
6+
<Comp foo={bar}
7+
</div>
8+
9+
<div>
10+
<span
711
</div>
812

913
{#if foo}
1014
<div>
1115
{/if}
1216

17+
{#if foo}
18+
<Comp foo={bar}
19+
{/if}
20+
1321
<div>
1422
<p>hi</p>
23+
24+
<open-ended

packages/svelte/tests/parser-legacy/samples/loose-unclosed-tag/output.json

Lines changed: 170 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"html": {
33
"type": "Fragment",
44
"start": 0,
5-
"end": 83,
5+
"end": 160,
66
"children": [
77
{
88
"type": "Element",
@@ -46,7 +46,7 @@
4646
{
4747
"type": "Element",
4848
"start": 22,
49-
"end": 42,
49+
"end": 51,
5050
"name": "div",
5151
"attributes": [],
5252
"children": [
@@ -58,45 +58,98 @@
5858
"data": "\n\t"
5959
},
6060
{
61-
"type": "Element",
61+
"type": "InlineComponent",
6262
"start": 29,
63-
"end": 36,
64-
"name": "span",
65-
"attributes": [],
66-
"children": [
63+
"end": 45,
64+
"name": "Comp",
65+
"attributes": [
6766
{
68-
"type": "Text",
67+
"type": "Attribute",
6968
"start": 35,
70-
"end": 36,
71-
"raw": "\n",
72-
"data": "\n"
69+
"end": 44,
70+
"name": "foo",
71+
"value": [
72+
{
73+
"type": "MustacheTag",
74+
"start": 39,
75+
"end": 44,
76+
"expression": {
77+
"type": "Identifier",
78+
"start": 40,
79+
"end": 43,
80+
"loc": {
81+
"start": {
82+
"line": 6,
83+
"column": 12
84+
},
85+
"end": {
86+
"line": 6,
87+
"column": 15
88+
}
89+
},
90+
"name": "bar"
91+
}
92+
}
93+
]
7394
}
74-
]
95+
],
96+
"children": []
97+
}
98+
]
99+
},
100+
{
101+
"type": "Text",
102+
"start": 51,
103+
"end": 53,
104+
"raw": "\n\n",
105+
"data": "\n\n"
106+
},
107+
{
108+
"type": "Element",
109+
"start": 53,
110+
"end": 72,
111+
"name": "div",
112+
"attributes": [],
113+
"children": [
114+
{
115+
"type": "Text",
116+
"start": 58,
117+
"end": 60,
118+
"raw": "\n\t",
119+
"data": "\n\t"
120+
},
121+
{
122+
"type": "Element",
123+
"start": 60,
124+
"end": 66,
125+
"name": "span",
126+
"attributes": [],
127+
"children": []
75128
}
76129
]
77130
},
78131
{
79132
"type": "Text",
80-
"start": 42,
81-
"end": 44,
133+
"start": 72,
134+
"end": 74,
82135
"raw": "\n\n",
83136
"data": "\n\n"
84137
},
85138
{
86139
"type": "IfBlock",
87-
"start": 44,
88-
"end": 66,
140+
"start": 74,
141+
"end": 96,
89142
"expression": {
90143
"type": "Identifier",
91-
"start": 49,
92-
"end": 52,
144+
"start": 79,
145+
"end": 82,
93146
"loc": {
94147
"start": {
95-
"line": 9,
148+
"line": 13,
96149
"column": 5
97150
},
98151
"end": {
99-
"line": 9,
152+
"line": 13,
100153
"column": 8
101154
}
102155
},
@@ -105,15 +158,15 @@
105158
"children": [
106159
{
107160
"type": "Element",
108-
"start": 55,
109-
"end": 61,
161+
"start": 85,
162+
"end": 91,
110163
"name": "div",
111164
"attributes": [],
112165
"children": [
113166
{
114167
"type": "Text",
115-
"start": 60,
116-
"end": 61,
168+
"start": 90,
169+
"end": 91,
117170
"raw": "\n",
118171
"data": "\n"
119172
}
@@ -123,40 +176,123 @@
123176
},
124177
{
125178
"type": "Text",
126-
"start": 66,
127-
"end": 68,
179+
"start": 96,
180+
"end": 98,
181+
"raw": "\n\n",
182+
"data": "\n\n"
183+
},
184+
{
185+
"type": "IfBlock",
186+
"start": 98,
187+
"end": 130,
188+
"expression": {
189+
"type": "Identifier",
190+
"start": 103,
191+
"end": 106,
192+
"loc": {
193+
"start": {
194+
"line": 17,
195+
"column": 5
196+
},
197+
"end": {
198+
"line": 17,
199+
"column": 8
200+
}
201+
},
202+
"name": "foo"
203+
},
204+
"children": [
205+
{
206+
"type": "InlineComponent",
207+
"start": 109,
208+
"end": 125,
209+
"name": "Comp",
210+
"attributes": [
211+
{
212+
"type": "Attribute",
213+
"start": 115,
214+
"end": 124,
215+
"name": "foo",
216+
"value": [
217+
{
218+
"type": "MustacheTag",
219+
"start": 119,
220+
"end": 124,
221+
"expression": {
222+
"type": "Identifier",
223+
"start": 120,
224+
"end": 123,
225+
"loc": {
226+
"start": {
227+
"line": 18,
228+
"column": 12
229+
},
230+
"end": {
231+
"line": 18,
232+
"column": 15
233+
}
234+
},
235+
"name": "bar"
236+
}
237+
}
238+
]
239+
}
240+
],
241+
"children": []
242+
}
243+
]
244+
},
245+
{
246+
"type": "Text",
247+
"start": 130,
248+
"end": 132,
128249
"raw": "\n\n",
129250
"data": "\n\n"
130251
},
131252
{
132253
"type": "Element",
133-
"start": 68,
134-
"end": 83,
254+
"start": 132,
255+
"end": 160,
135256
"name": "div",
136257
"attributes": [],
137258
"children": [
138259
{
139260
"type": "Text",
140-
"start": 73,
141-
"end": 74,
261+
"start": 137,
262+
"end": 138,
142263
"raw": "\n",
143264
"data": "\n"
144265
},
145266
{
146267
"type": "Element",
147-
"start": 74,
148-
"end": 83,
268+
"start": 138,
269+
"end": 147,
149270
"name": "p",
150271
"attributes": [],
151272
"children": [
152273
{
153274
"type": "Text",
154-
"start": 77,
155-
"end": 79,
275+
"start": 141,
276+
"end": 143,
156277
"raw": "hi",
157278
"data": "hi"
158279
}
159280
]
281+
},
282+
{
283+
"type": "Text",
284+
"start": 147,
285+
"end": 149,
286+
"raw": "\n\n",
287+
"data": "\n\n"
288+
},
289+
{
290+
"type": "Element",
291+
"start": 149,
292+
"end": 160,
293+
"name": "open-ended",
294+
"attributes": [],
295+
"children": []
160296
}
161297
]
162298
}

0 commit comments

Comments
 (0)