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 <richard.a.harris@gmail.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/9492/head
Simon H 8 months ago committed by GitHub
parent 687b8f58e3
commit 1beb5e8dc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: tweak script/style tag parsing/preprocessing logic

@ -191,11 +191,24 @@ export default function tag(parser) {
};
}
/** @type {Set<string>} */
const unique_names = new Set();
/** @type {string[]} */
const unique_names = [];
const current = parser.current();
const is_top_level_script_or_style =
(name === 'script' || name === 'style') && current.type === 'Root';
const read = is_top_level_script_or_style ? read_static_attribute : read_attribute;
let attribute;
while ((attribute = read_attribute(parser, unique_names))) {
while ((attribute = read(parser))) {
if (
(attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
unique_names.includes(attribute.name)
) {
error(attribute.start, 'duplicate-attribute');
}
element.attributes.push(attribute);
parser.allow_whitespace();
}
@ -245,10 +258,7 @@ export default function tag(parser) {
: chunk.expression;
}
const current = parser.current();
// special cases top-level <script> and <style>
if ((name === 'script' || name === 'style') && current.type === 'Root') {
if (is_top_level_script_or_style) {
parser.eat('>', true);
if (name === 'script') {
const content = read_script(parser, start, element.attributes);
@ -372,23 +382,61 @@ function read_tag_name(parser) {
// eslint-disable-next-line no-useless-escape
const regex_token_ending_character = /[\s=\/>"']/;
const regex_starts_with_quote_characters = /^["']/;
const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]))/;
/**
* @param {import('../index.js').Parser} parser
* @param {Set<string>} unique_names
* @returns {any}
* @returns {import('#compiler').Attribute | null}
*/
function read_attribute(parser, unique_names) {
function read_static_attribute(parser) {
const start = parser.index;
/** @param {string} name */
function check_unique(name) {
if (unique_names.has(name)) {
error(start, 'duplicate-attribute');
const name = parser.read_until(regex_token_ending_character);
if (!name) return null;
/** @type {true | Array<import('#compiler').Text | import('#compiler').ExpressionTag>} */
let value = true;
if (parser.eat('=')) {
parser.allow_whitespace();
let raw = parser.match_regex(regex_attribute_value);
if (!raw) {
error(parser.index, 'missing-attribute-value');
}
parser.index += raw.length;
const quoted = raw[0] === '"' || raw[0] === "'";
if (quoted) {
raw = raw.slice(1, -1);
}
unique_names.add(name);
value = [
{
start: parser.index - raw.length - (quoted ? 1 : 0),
end: quoted ? parser.index - 1 : parser.index,
type: 'Text',
raw: raw,
data: decode_character_references(raw, true),
parent: null
}
];
}
if (parser.match_regex(regex_starts_with_quote_characters)) {
error(parser.index, 'expected-token', '=');
}
return create_attribute(name, start, parser.index, value);
}
/**
* @param {import('../index.js').Parser} parser
* @returns {import('#compiler').Attribute | import('#compiler').SpreadAttribute | import('#compiler').Directive | null}
*/
function read_attribute(parser) {
const start = parser.index;
if (parser.eat('{')) {
parser.allow_whitespace();
@ -419,8 +467,6 @@ function read_attribute(parser, unique_names) {
error(start, 'empty-attribute-shorthand');
}
check_unique(name);
parser.allow_whitespace();
parser.eat('}', true);
@ -473,12 +519,6 @@ function read_attribute(parser, unique_names) {
error(start + colon_index + 1, 'empty-directive-name', type);
}
if (type === 'BindDirective' && directive_name !== 'this') {
check_unique(directive_name);
} else if (type !== 'OnDirective' && type !== 'UseDirective') {
check_unique(name);
}
if (type === 'StyleDirective') {
return {
start,
@ -546,8 +586,6 @@ function read_attribute(parser, unique_names) {
return directive;
}
check_unique(name);
return create_attribute(name, start, end, value);
}
@ -569,7 +607,6 @@ function get_directive_type(name) {
/**
* @param {import('../index.js').Parser} parser
* @returns {any[]}
*/
function read_attribute_value(parser) {
const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null;

@ -253,8 +253,10 @@ function stringify_tag_attributes(attributes) {
return value;
}
const regex_style_tags = /<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi;
const regex_script_tags = /<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi;
const regex_style_tags =
/<!--[^]*?-->|<style((?:\s+[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s])|\s+[^=>'"/]+)*\s*)(?:\/>|>([\S\s]*?)<\/style>)/g;
const regex_script_tags =
/<!--[^]*?-->|<script((?:\s+[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s])|\s+[^=>'"/]+)*\s*)(?:\/>|>([\S\s]*?)<\/script>)/g;
/**
* Calculate the updates required to process all instances of the specified tag.

@ -0,0 +1,5 @@
<script generics="T extends { yes: boolean }">
let name = 'world';
</script>
<h1>Hello {name}!</h1>

@ -0,0 +1,150 @@
{
"html": {
"start": 79,
"end": 101,
"type": "Fragment",
"children": [
{
"start": 77,
"end": 79,
"type": "Text",
"raw": "\n\n",
"data": "\n\n"
},
{
"start": 79,
"end": 101,
"type": "Element",
"name": "h1",
"attributes": [],
"children": [
{
"start": 83,
"end": 89,
"type": "Text",
"raw": "Hello ",
"data": "Hello "
},
{
"start": 89,
"end": 95,
"type": "MustacheTag",
"expression": {
"type": "Identifier",
"start": 90,
"end": 94,
"loc": {
"start": {
"line": 5,
"column": 11
},
"end": {
"line": 5,
"column": 15
}
},
"name": "name"
}
},
{
"start": 95,
"end": 96,
"type": "Text",
"raw": "!",
"data": "!"
}
]
}
]
},
"instance": {
"type": "Script",
"start": 0,
"end": 77,
"context": "default",
"content": {
"type": "Program",
"start": 46,
"end": 68,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 3,
"column": 0
}
},
"body": [
{
"type": "VariableDeclaration",
"start": 48,
"end": 67,
"loc": {
"start": {
"line": 2,
"column": 1
},
"end": {
"line": 2,
"column": 20
}
},
"declarations": [
{
"type": "VariableDeclarator",
"start": 52,
"end": 66,
"loc": {
"start": {
"line": 2,
"column": 5
},
"end": {
"line": 2,
"column": 19
}
},
"id": {
"type": "Identifier",
"start": 52,
"end": 56,
"loc": {
"start": {
"line": 2,
"column": 5
},
"end": {
"line": 2,
"column": 9
}
},
"name": "name"
},
"init": {
"type": "Literal",
"start": 59,
"end": 66,
"loc": {
"start": {
"line": 2,
"column": 12
},
"end": {
"line": 2,
"column": 19
}
},
"value": "world",
"raw": "'world'"
}
}
],
"kind": "let"
}
],
"sourceType": "module"
}
}
}

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
preprocess: {
script: ({ attributes }) =>
typeof attributes.generics === 'string' && attributes.generics.includes('>')
? { code: '' }
: undefined
}
});

@ -0,0 +1,3 @@
<script generics="T extends Record<string, string>">
foo {}
</script>

@ -0,0 +1 @@
<script generics="T extends Record<string, string>"></script>
Loading…
Cancel
Save