Skip to content

Commit 1beb5e8

Browse files
fix: tweak script/style tag parsing/preprocessing logic (#9502)
Related to sveltejs/language-tools#2204 / sveltejs/language-tools#2039 The Svelte 5 version of #9486 and #9498 --------- Co-authored-by: Rich Harris <[email protected]> Co-authored-by: Rich Harris <[email protected]>
1 parent 687b8f5 commit 1beb5e8

File tree

8 files changed

+241
-28
lines changed

8 files changed

+241
-28
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: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -191,11 +191,24 @@ export default function tag(parser) {
191191
};
192192
}
193193

194-
/** @type {Set<string>} */
195-
const unique_names = new Set();
194+
/** @type {string[]} */
195+
const unique_names = [];
196+
197+
const current = parser.current();
198+
const is_top_level_script_or_style =
199+
(name === 'script' || name === 'style') && current.type === 'Root';
200+
201+
const read = is_top_level_script_or_style ? read_static_attribute : read_attribute;
196202

197203
let attribute;
198-
while ((attribute = read_attribute(parser, unique_names))) {
204+
while ((attribute = read(parser))) {
205+
if (
206+
(attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
207+
unique_names.includes(attribute.name)
208+
) {
209+
error(attribute.start, 'duplicate-attribute');
210+
}
211+
199212
element.attributes.push(attribute);
200213
parser.allow_whitespace();
201214
}
@@ -245,10 +258,7 @@ export default function tag(parser) {
245258
: chunk.expression;
246259
}
247260

248-
const current = parser.current();
249-
250-
// special cases – top-level <script> and <style>
251-
if ((name === 'script' || name === 'style') && current.type === 'Root') {
261+
if (is_top_level_script_or_style) {
252262
parser.eat('>', true);
253263
if (name === 'script') {
254264
const content = read_script(parser, start, element.attributes);
@@ -372,23 +382,61 @@ function read_tag_name(parser) {
372382
// eslint-disable-next-line no-useless-escape
373383
const regex_token_ending_character = /[\s=\/>"']/;
374384
const regex_starts_with_quote_characters = /^["']/;
385+
const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]))/;
375386

376387
/**
377388
* @param {import('../index.js').Parser} parser
378-
* @param {Set<string>} unique_names
379-
* @returns {any}
389+
* @returns {import('#compiler').Attribute | null}
380390
*/
381-
function read_attribute(parser, unique_names) {
391+
function read_static_attribute(parser) {
382392
const start = parser.index;
383393

384-
/** @param {string} name */
385-
function check_unique(name) {
386-
if (unique_names.has(name)) {
387-
error(start, 'duplicate-attribute');
394+
const name = parser.read_until(regex_token_ending_character);
395+
if (!name) return null;
396+
397+
/** @type {true | Array<import('#compiler').Text | import('#compiler').ExpressionTag>} */
398+
let value = true;
399+
400+
if (parser.eat('=')) {
401+
parser.allow_whitespace();
402+
let raw = parser.match_regex(regex_attribute_value);
403+
if (!raw) {
404+
error(parser.index, 'missing-attribute-value');
405+
}
406+
407+
parser.index += raw.length;
408+
409+
const quoted = raw[0] === '"' || raw[0] === "'";
410+
if (quoted) {
411+
raw = raw.slice(1, -1);
388412
}
389-
unique_names.add(name);
413+
414+
value = [
415+
{
416+
start: parser.index - raw.length - (quoted ? 1 : 0),
417+
end: quoted ? parser.index - 1 : parser.index,
418+
type: 'Text',
419+
raw: raw,
420+
data: decode_character_references(raw, true),
421+
parent: null
422+
}
423+
];
390424
}
391425

426+
if (parser.match_regex(regex_starts_with_quote_characters)) {
427+
error(parser.index, 'expected-token', '=');
428+
}
429+
430+
return create_attribute(name, start, parser.index, value);
431+
}
432+
433+
/**
434+
* @param {import('../index.js').Parser} parser
435+
* @returns {import('#compiler').Attribute | import('#compiler').SpreadAttribute | import('#compiler').Directive | null}
436+
*/
437+
function read_attribute(parser) {
438+
const start = parser.index;
439+
392440
if (parser.eat('{')) {
393441
parser.allow_whitespace();
394442

@@ -419,8 +467,6 @@ function read_attribute(parser, unique_names) {
419467
error(start, 'empty-attribute-shorthand');
420468
}
421469

422-
check_unique(name);
423-
424470
parser.allow_whitespace();
425471
parser.eat('}', true);
426472

@@ -473,12 +519,6 @@ function read_attribute(parser, unique_names) {
473519
error(start + colon_index + 1, 'empty-directive-name', type);
474520
}
475521

476-
if (type === 'BindDirective' && directive_name !== 'this') {
477-
check_unique(directive_name);
478-
} else if (type !== 'OnDirective' && type !== 'UseDirective') {
479-
check_unique(name);
480-
}
481-
482522
if (type === 'StyleDirective') {
483523
return {
484524
start,
@@ -546,8 +586,6 @@ function read_attribute(parser, unique_names) {
546586
return directive;
547587
}
548588

549-
check_unique(name);
550-
551589
return create_attribute(name, start, end, value);
552590
}
553591

@@ -569,7 +607,6 @@ function get_directive_type(name) {
569607

570608
/**
571609
* @param {import('../index.js').Parser} parser
572-
* @returns {any[]}
573610
*/
574611
function read_attribute_value(parser) {
575612
const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null;

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)