Skip to content

Commit 1807e88

Browse files
committed
fix: tweak script/style tag parsing/preprocessing logic
Related to sveltejs/language-tools#2204 / sveltejs/language-tools#2039 The Svelte 5 version of #9486 and #9498
1 parent 37f2493 commit 1807e88

File tree

8 files changed

+217
-13
lines changed

8 files changed

+217
-13
lines changed

.changeset/afraid-moose-matter.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: tweak script/style tag parsing/preprocessing logic

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

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,12 @@ export default function tag(parser) {
194194
/** @type {Set<string>} */
195195
const unique_names = new Set();
196196

197+
const current = parser.current();
198+
const is_top_level_script_or_style =
199+
(name === 'script' || name === 'style') && current.type === 'Root';
200+
197201
let attribute;
198-
while ((attribute = read_attribute(parser, unique_names))) {
202+
while ((attribute = read_attribute(parser, unique_names, is_top_level_script_or_style))) {
199203
element.attributes.push(attribute);
200204
parser.allow_whitespace();
201205
}
@@ -245,10 +249,7 @@ export default function tag(parser) {
245249
: chunk.expression;
246250
}
247251

248-
const current = parser.current();
249-
250-
// special cases – top-level <script> and <style>
251-
if ((name === 'script' || name === 'style') && current.type === 'Root') {
252+
if (is_top_level_script_or_style) {
252253
parser.eat('>', true);
253254
if (name === 'script') {
254255
const content = read_script(parser, start, element.attributes);
@@ -376,9 +377,10 @@ const regex_starts_with_quote_characters = /^["']/;
376377
/**
377378
* @param {import('../index.js').Parser} parser
378379
* @param {Set<string>} unique_names
380+
* @param {boolean} is_static If `true`, `{` and `}` are not treated as delimiters for expressions
379381
* @returns {any}
380382
*/
381-
function read_attribute(parser, unique_names) {
383+
function read_attribute(parser, unique_names, is_static) {
382384
const start = parser.index;
383385

384386
/** @param {string} name */
@@ -389,7 +391,7 @@ function read_attribute(parser, unique_names) {
389391
unique_names.add(name);
390392
}
391393

392-
if (parser.eat('{')) {
394+
if (!is_static && parser.eat('{')) {
393395
parser.allow_whitespace();
394396

395397
if (parser.eat('...')) {
@@ -460,13 +462,13 @@ function read_attribute(parser, unique_names) {
460462
let value = true;
461463
if (parser.eat('=')) {
462464
parser.allow_whitespace();
463-
value = read_attribute_value(parser);
465+
value = read_attribute_value(parser, is_static);
464466
end = parser.index;
465467
} else if (parser.match_regex(regex_starts_with_quote_characters)) {
466468
error(parser.index, 'expected-token', '=');
467469
}
468470

469-
if (type) {
471+
if (!is_static && type) {
470472
const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|');
471473

472474
if (directive_name === '') {
@@ -567,11 +569,37 @@ function get_directive_type(name) {
567569
return false;
568570
}
569571

572+
const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]))/;
573+
570574
/**
571575
* @param {import('../index.js').Parser} parser
572-
* @returns {any[]}
576+
* @param {boolean} is_static If `true`, `{` and `}` are not treated as delimiters for expressions
573577
*/
574-
function read_attribute_value(parser) {
578+
function read_attribute_value(parser, is_static) {
579+
if (is_static) {
580+
let value = parser.match_regex(regex_attribute_value);
581+
if (!value) {
582+
error(parser.index, 'missing-attribute-value');
583+
}
584+
585+
parser.index += value.length;
586+
587+
const quoted = value[0] === '"' || value[0] === "'";
588+
if (quoted) {
589+
value = value.slice(1, -1);
590+
}
591+
592+
return [
593+
{
594+
start: parser.index - value.length - (quoted ? 1 : 0),
595+
end: quoted ? parser.index - 1 : parser.index,
596+
type: 'Text',
597+
raw: value,
598+
data: decode_character_references(value, true)
599+
}
600+
];
601+
}
602+
575603
const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null;
576604
if (quote_mark && parser.eat(quote_mark)) {
577605
return [

packages/svelte/src/compiler/preprocess/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,10 @@ function stringify_tag_attributes(attributes) {
253253
return value;
254254
}
255255

256-
const regex_style_tags = /<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi;
257-
const regex_script_tags = /<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi;
256+
const regex_style_tags =
257+
/<!--[^]*?-->|<style((?:\s+[^=>'"\/]+=(?:"[^"]*"|'[^']*'|[^>\s])|\s+[^=>'"\/]+)*\s*)(?:\/>|>([\S\s]*?)<\/style>)/g;
258+
const regex_script_tags =
259+
/<!--[^]*?-->|<script((?:\s+[^=>'"\/]+=(?:"[^"]*"|'[^']*'|[^>\s])|\s+[^=>'"\/]+)*\s*)(?:\/>|>([\S\s]*?)<\/script>)/g;
258260

259261
/**
260262
* Calculate the updates required to process all instances of the specified tag.
Lines changed: 5 additions & 0 deletions
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>
Lines changed: 150 additions & 0 deletions
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+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
preprocess: {
5+
script: ({ attributes }) =>
6+
typeof attributes.generics === 'string' && attributes.generics.includes('>')
7+
? { code: '' }
8+
: undefined
9+
}
10+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<script generics="T extends Record<string, string>">
2+
foo {}
3+
</script>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<script generics="T extends Record<string, string>"></script>

0 commit comments

Comments
 (0)