From 1807e885199c8c125f9b23deacf7e60b5b225c41 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 16 Nov 2023 23:33:40 +0100 Subject: [PATCH 1/4] 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 --- .changeset/afraid-moose-matter.md | 5 + .../compiler/phases/1-parse/state/element.js | 50 ++++-- .../svelte/src/compiler/preprocess/index.js | 6 +- .../input.svelte | 5 + .../output.json | 150 ++++++++++++++++++ .../attributes-with-closing-tag/_config.js | 10 ++ .../attributes-with-closing-tag/input.svelte | 3 + .../attributes-with-closing-tag/output.svelte | 1 + 8 files changed, 217 insertions(+), 13 deletions(-) create mode 100644 .changeset/afraid-moose-matter.md create mode 100644 packages/svelte/tests/parser-legacy/samples/script-attribute-with-curly-braces/input.svelte create mode 100644 packages/svelte/tests/parser-legacy/samples/script-attribute-with-curly-braces/output.json create mode 100644 packages/svelte/tests/preprocess/samples/attributes-with-closing-tag/_config.js create mode 100644 packages/svelte/tests/preprocess/samples/attributes-with-closing-tag/input.svelte create mode 100644 packages/svelte/tests/preprocess/samples/attributes-with-closing-tag/output.svelte diff --git a/.changeset/afraid-moose-matter.md b/.changeset/afraid-moose-matter.md new file mode 100644 index 000000000000..d6be9c8546f7 --- /dev/null +++ b/.changeset/afraid-moose-matter.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: tweak script/style tag parsing/preprocessing logic diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index d193aeaeb6ab..5713734c87bd 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -194,8 +194,12 @@ export default function tag(parser) { /** @type {Set} */ const unique_names = new Set(); + const current = parser.current(); + const is_top_level_script_or_style = + (name === 'script' || name === 'style') && current.type === 'Root'; + let attribute; - while ((attribute = read_attribute(parser, unique_names))) { + while ((attribute = read_attribute(parser, unique_names, is_top_level_script_or_style))) { element.attributes.push(attribute); parser.allow_whitespace(); } @@ -245,10 +249,7 @@ export default function tag(parser) { : chunk.expression; } - const current = parser.current(); - - // special cases – top-level + +

Hello {name}!

\ No newline at end of file diff --git a/packages/svelte/tests/parser-legacy/samples/script-attribute-with-curly-braces/output.json b/packages/svelte/tests/parser-legacy/samples/script-attribute-with-curly-braces/output.json new file mode 100644 index 000000000000..af4ec56a68d2 --- /dev/null +++ b/packages/svelte/tests/parser-legacy/samples/script-attribute-with-curly-braces/output.json @@ -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" + } + } +} diff --git a/packages/svelte/tests/preprocess/samples/attributes-with-closing-tag/_config.js b/packages/svelte/tests/preprocess/samples/attributes-with-closing-tag/_config.js new file mode 100644 index 000000000000..ddb4fb4df0e9 --- /dev/null +++ b/packages/svelte/tests/preprocess/samples/attributes-with-closing-tag/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + preprocess: { + script: ({ attributes }) => + typeof attributes.generics === 'string' && attributes.generics.includes('>') + ? { code: '' } + : undefined + } +}); diff --git a/packages/svelte/tests/preprocess/samples/attributes-with-closing-tag/input.svelte b/packages/svelte/tests/preprocess/samples/attributes-with-closing-tag/input.svelte new file mode 100644 index 000000000000..aa1a533ac775 --- /dev/null +++ b/packages/svelte/tests/preprocess/samples/attributes-with-closing-tag/input.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/preprocess/samples/attributes-with-closing-tag/output.svelte b/packages/svelte/tests/preprocess/samples/attributes-with-closing-tag/output.svelte new file mode 100644 index 000000000000..d4ade07a7560 --- /dev/null +++ b/packages/svelte/tests/preprocess/samples/attributes-with-closing-tag/output.svelte @@ -0,0 +1 @@ + From 56cbc0d05d979ad98753102012f73389537dc305 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Nov 2023 06:13:54 -0500 Subject: [PATCH 2/4] separate static attribute logic out into its own function (#9505) Co-authored-by: Rich Harris --- .../compiler/phases/1-parse/state/element.js | 112 ++++++++++-------- 1 file changed, 61 insertions(+), 51 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index 5713734c87bd..690a5c1fefb8 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -191,15 +191,24 @@ export default function tag(parser) { }; } - /** @type {Set} */ - 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, is_top_level_script_or_style))) { + 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(); } @@ -376,22 +385,58 @@ const regex_starts_with_quote_characters = /^["']/; /** * @param {import('../index.js').Parser} parser - * @param {Set} unique_names - * @param {boolean} is_static If `true`, `{` and `}` are not treated as delimiters for expressions - * @returns {any} + * @returns {import('#compiler').Attribute | null} */ -function read_attribute(parser, unique_names, is_static) { +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} */ + 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 (!is_static && parser.eat('{')) { + 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(); if (parser.eat('...')) { @@ -421,8 +466,6 @@ function read_attribute(parser, unique_names, is_static) { error(start, 'empty-attribute-shorthand'); } - check_unique(name); - parser.allow_whitespace(); parser.eat('}', true); @@ -462,25 +505,19 @@ function read_attribute(parser, unique_names, is_static) { let value = true; if (parser.eat('=')) { parser.allow_whitespace(); - value = read_attribute_value(parser, is_static); + value = read_attribute_value(parser); end = parser.index; } else if (parser.match_regex(regex_starts_with_quote_characters)) { error(parser.index, 'expected-token', '='); } - if (!is_static && type) { + if (type) { const [directive_name, ...modifiers] = name.slice(colon_index + 1).split('|'); if (directive_name === '') { 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, @@ -548,8 +585,6 @@ function read_attribute(parser, unique_names, is_static) { return directive; } - check_unique(name); - return create_attribute(name, start, end, value); } @@ -573,33 +608,8 @@ const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]))/; /** * @param {import('../index.js').Parser} parser - * @param {boolean} is_static If `true`, `{` and `}` are not treated as delimiters for expressions */ -function read_attribute_value(parser, is_static) { - if (is_static) { - let value = parser.match_regex(regex_attribute_value); - if (!value) { - error(parser.index, 'missing-attribute-value'); - } - - parser.index += value.length; - - const quoted = value[0] === '"' || value[0] === "'"; - if (quoted) { - value = value.slice(1, -1); - } - - return [ - { - start: parser.index - value.length - (quoted ? 1 : 0), - end: quoted ? parser.index - 1 : parser.index, - type: 'Text', - raw: value, - data: decode_character_references(value, true) - } - ]; - } - +function read_attribute_value(parser) { const quote_mark = parser.eat("'") ? "'" : parser.eat('"') ? '"' : null; if (quote_mark && parser.eat(quote_mark)) { return [ From f6fcf4c42959fe50c9e60b6964bbfeddf2cff2ec Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 17 Nov 2023 12:24:17 +0100 Subject: [PATCH 3/4] fix lint --- packages/svelte/src/compiler/preprocess/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/preprocess/index.js b/packages/svelte/src/compiler/preprocess/index.js index a38e7d47c482..35cb56589a40 100644 --- a/packages/svelte/src/compiler/preprocess/index.js +++ b/packages/svelte/src/compiler/preprocess/index.js @@ -254,9 +254,9 @@ function stringify_tag_attributes(attributes) { } const regex_style_tags = - /|'"\/]+=(?:"[^"]*"|'[^']*'|[^>\s])|\s+[^=>'"\/]+)*\s*)(?:\/>|>([\S\s]*?)<\/style>)/g; + /|'"/]+=(?:"[^"]*"|'[^']*'|[^>\s])|\s+[^=>'"/]+)*\s*)(?:\/>|>([\S\s]*?)<\/style>)/g; const regex_script_tags = - /|'"\/]+=(?:"[^"]*"|'[^']*'|[^>\s])|\s+[^=>'"\/]+)*\s*)(?:\/>|>([\S\s]*?)<\/script>)/g; + /|'"/]+=(?:"[^"]*"|'[^']*'|[^>\s])|\s+[^=>'"/]+)*\s*)(?:\/>|>([\S\s]*?)<\/script>)/g; /** * Calculate the updates required to process all instances of the specified tag. From 0562bfe2dc42d2c34c8fc723aedde29192565d2a Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 17 Nov 2023 12:24:25 +0100 Subject: [PATCH 4/4] move code --- packages/svelte/src/compiler/phases/1-parse/state/element.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index 690a5c1fefb8..c649d5f0239c 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -382,6 +382,7 @@ 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 @@ -604,8 +605,6 @@ function get_directive_type(name) { return false; } -const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]))/; - /** * @param {import('../index.js').Parser} parser */