Skip to content

Commit

Permalink
fix: tweak script/style tag parsing/preprocessing logic (#9502)
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
3 people authored Nov 17, 2023
1 parent 687b8f5 commit 1beb5e8
Show file tree
Hide file tree
Showing 8 changed files with 241 additions and 28 deletions.
5 changes: 5 additions & 0 deletions .changeset/afraid-moose-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: tweak script/style tag parsing/preprocessing logic
89 changes: 63 additions & 26 deletions packages/svelte/src/compiler/phases/1-parse/state/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -419,8 +467,6 @@ function read_attribute(parser, unique_names) {
error(start, 'empty-attribute-shorthand');
}

check_unique(name);

parser.allow_whitespace();
parser.eat('}', true);

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -546,8 +586,6 @@ function read_attribute(parser, unique_names) {
return directive;
}

check_unique(name);

return create_attribute(name, start, end, value);
}

Expand All @@ -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;
Expand Down
6 changes: 4 additions & 2 deletions packages/svelte/src/compiler/preprocess/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script generics="T extends { yes: boolean }">
let name = 'world';
</script>

<h1>Hello {name}!</h1>
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { test } from '../../test';

export default test({
preprocess: {
script: ({ attributes }) =>
typeof attributes.generics === 'string' && attributes.generics.includes('>')
? { code: '' }
: undefined
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<script generics="T extends Record<string, string>">
foo {}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<script generics="T extends Record<string, string>"></script>

1 comment on commit 1beb5e8

@vercel
Copy link

@vercel vercel bot commented on 1beb5e8 Nov 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

svelte-5-preview – ./sites/svelte-5-preview

svelte-5-preview-svelte.vercel.app
svelte-octane.vercel.app
svelte-5-preview.vercel.app
svelte-5-preview-git-main-svelte.vercel.app

Please sign in to comment.