Skip to content

Commit 45f0fc4

Browse files
committed
fix: ignore expressions in top level script/style tag attributes
Related to sveltejs/language-tools#2204
1 parent 2ea1764 commit 45f0fc4

File tree

4 files changed

+196
-8
lines changed

4 files changed

+196
-8
lines changed

.changeset/ten-ducks-jump.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: ignore expressions in top level script/style tag attributes

packages/svelte/src/compiler/parse/state/tag.js

+36-8
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,9 @@ export default function tag(parser) {
161161
* @type {Set<string>}
162162
*/
163163
const unique_names = new Set();
164+
const is_top_level_script_or_style = specials.has(name) && parser.stack.length === 1;
164165
let attribute;
165-
while ((attribute = read_attribute(parser, unique_names))) {
166+
while ((attribute = read_attribute(parser, unique_names, is_top_level_script_or_style))) {
166167
element.attributes.push(attribute);
167168
parser.allow_whitespace();
168169
}
@@ -196,8 +197,7 @@ export default function tag(parser) {
196197
}
197198
element.tag = definition.value[0].data || definition.value[0].expression;
198199
}
199-
// special cases – top-level <script> and <style>
200-
if (specials.has(name) && parser.stack.length === 1) {
200+
if (is_top_level_script_or_style) {
201201
const special = specials.get(name);
202202
parser.eat('>', true);
203203
const content = special.read(parser, start, element.attributes);
@@ -280,8 +280,9 @@ const regex_starts_with_quote_characters = /^["']/;
280280
/**
281281
* @param {import('../index.js').Parser} parser
282282
* @param {Set<string>} unique_names
283+
* @param {boolean} is_static If `true`, `{` and `}` are not treated as delimiters for expressions
283284
*/
284-
function read_attribute(parser, unique_names) {
285+
function read_attribute(parser, unique_names, is_static) {
285286
const start = parser.index;
286287

287288
/**
@@ -293,7 +294,7 @@ function read_attribute(parser, unique_names) {
293294
}
294295
unique_names.add(name);
295296
}
296-
if (parser.eat('{')) {
297+
if (!is_static && parser.eat('{')) {
297298
parser.allow_whitespace();
298299
if (parser.eat('...')) {
299300
const expression = read_expression(parser);
@@ -348,12 +349,12 @@ function read_attribute(parser, unique_names) {
348349
let value = true;
349350
if (parser.eat('=')) {
350351
parser.allow_whitespace();
351-
value = read_attribute_value(parser);
352+
value = read_attribute_value(parser, is_static);
352353
end = parser.index;
353354
} else if (parser.match_regex(regex_starts_with_quote_characters)) {
354355
parser.error(parser_errors.unexpected_token('='), parser.index);
355356
}
356-
if (type) {
357+
if (!is_static && type) {
357358
const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|');
358359
if (directive_name === '') {
359360
parser.error(parser_errors.empty_directive_name(type), start + colon_index + 1);
@@ -436,10 +437,37 @@ function get_directive_type(name) {
436437
if (name === 'in' || name === 'out' || name === 'transition') return 'Transition';
437438
}
438439

440+
const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]))/;
441+
439442
/**
440443
* @param {import('../index.js').Parser} parser
444+
* @param {boolean} is_static If `true`, `{` and `}` are not treated as delimiters for expressions
441445
*/
442-
function read_attribute_value(parser) {
446+
function read_attribute_value(parser, is_static) {
447+
if (is_static) {
448+
let value = parser.match_regex(regex_attribute_value);
449+
if (!value) {
450+
parser.error(parser_errors.missing_attribute_value);
451+
}
452+
453+
parser.index += value.length;
454+
455+
const quoted = value[0] === '"' || value[0] === "'";
456+
if (quoted) {
457+
value = value.slice(1, -1);
458+
}
459+
460+
return [
461+
{
462+
start: parser.index - value.length - (quoted ? 1 : 0),
463+
end: quoted ? parser.index - 1 : parser.index,
464+
type: 'Text',
465+
raw: value,
466+
data: decode_character_references(value, true)
467+
}
468+
];
469+
}
470+
443471
const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null;
444472
if (quote_mark && parser.eat(quote_mark)) {
445473
return [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script generics="T extends { yes: boolean }">
2+
let name = 'world';
3+
</script>
4+
5+
<h1>Hello {name}!</h1>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
{
2+
"html": {
3+
"start": 79,
4+
"end": 101,
5+
"type": "Fragment",
6+
"children": [
7+
{
8+
"start": 77,
9+
"end": 79,
10+
"type": "Text",
11+
"raw": "\n\n",
12+
"data": "\n\n"
13+
},
14+
{
15+
"start": 79,
16+
"end": 101,
17+
"type": "Element",
18+
"name": "h1",
19+
"attributes": [],
20+
"children": [
21+
{
22+
"start": 83,
23+
"end": 89,
24+
"type": "Text",
25+
"raw": "Hello ",
26+
"data": "Hello "
27+
},
28+
{
29+
"start": 89,
30+
"end": 95,
31+
"type": "MustacheTag",
32+
"expression": {
33+
"type": "Identifier",
34+
"start": 90,
35+
"end": 94,
36+
"loc": {
37+
"start": {
38+
"line": 5,
39+
"column": 11
40+
},
41+
"end": {
42+
"line": 5,
43+
"column": 15
44+
}
45+
},
46+
"name": "name"
47+
}
48+
},
49+
{
50+
"start": 95,
51+
"end": 96,
52+
"type": "Text",
53+
"raw": "!",
54+
"data": "!"
55+
}
56+
]
57+
}
58+
]
59+
},
60+
"instance": {
61+
"type": "Script",
62+
"start": 0,
63+
"end": 77,
64+
"context": "default",
65+
"content": {
66+
"type": "Program",
67+
"start": 46,
68+
"end": 68,
69+
"loc": {
70+
"start": {
71+
"line": 1,
72+
"column": 0
73+
},
74+
"end": {
75+
"line": 3,
76+
"column": 0
77+
}
78+
},
79+
"body": [
80+
{
81+
"type": "VariableDeclaration",
82+
"start": 48,
83+
"end": 67,
84+
"loc": {
85+
"start": {
86+
"line": 2,
87+
"column": 1
88+
},
89+
"end": {
90+
"line": 2,
91+
"column": 20
92+
}
93+
},
94+
"declarations": [
95+
{
96+
"type": "VariableDeclarator",
97+
"start": 52,
98+
"end": 66,
99+
"loc": {
100+
"start": {
101+
"line": 2,
102+
"column": 5
103+
},
104+
"end": {
105+
"line": 2,
106+
"column": 19
107+
}
108+
},
109+
"id": {
110+
"type": "Identifier",
111+
"start": 52,
112+
"end": 56,
113+
"loc": {
114+
"start": {
115+
"line": 2,
116+
"column": 5
117+
},
118+
"end": {
119+
"line": 2,
120+
"column": 9
121+
}
122+
},
123+
"name": "name"
124+
},
125+
"init": {
126+
"type": "Literal",
127+
"start": 59,
128+
"end": 66,
129+
"loc": {
130+
"start": {
131+
"line": 2,
132+
"column": 12
133+
},
134+
"end": {
135+
"line": 2,
136+
"column": 19
137+
}
138+
},
139+
"value": "world",
140+
"raw": "'world'"
141+
}
142+
}
143+
],
144+
"kind": "let"
145+
}
146+
],
147+
"sourceType": "module"
148+
}
149+
}
150+
}

0 commit comments

Comments
 (0)