From b3c9ed73fcdd3bfed2602534f908c38565742bdf Mon Sep 17 00:00:00 2001 From: Caique Torres Date: Wed, 4 Dec 2024 11:27:18 -0300 Subject: [PATCH 1/9] fix: enhance string/expression length check and fix closing character issues --- packages/svelte/src/compiler/errors.js | 11 +- .../compiler/phases/1-parse/read/context.js | 114 ++++++++++++++---- .../compiler/phases/1-parse/utils/quote.js | 14 +++ 3 files changed, 114 insertions(+), 25 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/1-parse/utils/quote.js diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 901ea1983ea7..97b284af8b35 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -1003,6 +1003,15 @@ export function expected_whitespace(node) { e(node, "expected_whitespace", "Expected whitespace"); } +/** + * Unterminated string constant + * @param {null | number | NodeLike} node + * @returns {never} + */ +export function unterminated_string_constant(node) { + e(node, "unterminated_string_constant", "Unterminated string constant"); +} + /** * `<%name%>` does not support non-event attributes or spread attributes * @param {null | number | NodeLike} node @@ -1544,4 +1553,4 @@ export function unexpected_reserved_word(node, word) { */ export function void_element_invalid_content(node) { e(node, "void_element_invalid_content", "Void elements cannot have children or closing tags"); -} \ No newline at end of file +} diff --git a/packages/svelte/src/compiler/phases/1-parse/read/context.js b/packages/svelte/src/compiler/phases/1-parse/read/context.js index e408b5024fa3..516e14fb1057 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/context.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/context.js @@ -14,6 +14,8 @@ import { parse_expression_at } from '../acorn.js'; import { regex_not_newline_characters } from '../../patterns.js'; import * as e from '../../../errors.js'; import { locator } from '../../../state.js'; +import read_expression from './expression.js'; +import { is_back_quote, is_quote } from '../utils/quote.js'; /** * @param {Parser} parser @@ -41,33 +43,11 @@ export default function read_pattern(parser) { }; } - if (!is_bracket_open(code)) { - e.expected_pattern(i); - } - - const bracket_stack = [code]; - i += code <= 0xffff ? 1 : 2; - - while (i < parser.template.length) { - const code = full_char_code_at(parser.template, i); - if (is_bracket_open(code)) { - bracket_stack.push(code); - } else if (is_bracket_close(code)) { - const popped = /** @type {number} */ (bracket_stack.pop()); - if (!is_bracket_pair(popped, code)) { - e.expected_token(i, String.fromCharCode(/** @type {number} */ (get_bracket_close(popped)))); - } - if (bracket_stack.length === 0) { - i += code <= 0xffff ? 1 : 2; - break; - } - } - i += code <= 0xffff ? 1 : 2; - } - + i = read_expression_length(parser, start); parser.index = i; const pattern_string = parser.template.slice(start, i); + try { // the length of the `space_with_newline` has to be start - 1 // because we added a `(` in front of the pattern_string, @@ -97,6 +77,92 @@ export default function read_pattern(parser) { } } +/** + * @param {Parser} parser + * @param {number} start + */ +function read_expression_length(parser, start) { + let i = start; + const code = full_char_code_at(parser.template, i); + + if (!is_bracket_open(code)) { + e.expected_pattern(i); + } + + const bracket_stack = [code]; + i += code <= 0xffff ? 1 : 2; + + while (i < parser.template.length) { + let code = full_char_code_at(parser.template, i); + if (is_quote(code)) { + i = read_string_length(parser, i, code); + } else { + if (is_bracket_open(code)) { + bracket_stack.push(code); + } else if (is_bracket_close(code)) { + const popped = /** @type {number} */ (bracket_stack.pop()); + if (!is_bracket_pair(popped, code)) { + e.expected_token(i, String.fromCharCode(/** @type {number} */(get_bracket_close(popped)))); + } + if (bracket_stack.length === 0) { + i += code <= 0xffff ? 1 : 2; + break; + } + } + i += code <= 0xffff ? 1 : 2; + } + } + + return i; +} + +/** + * @param {Parser} parser + * @param {number} start + * @param {number} quote + * @returns {number} + */ +function read_string_length(parser, start, quote) { + let i = start; + i += quote <= 0xffff ? 1 : 2; + + const BACKSLASH = '\\'.charCodeAt(0); + const DOLAR = '$'.charCodeAt(0); + const LEFT_BRACKET = '{'.charCodeAt(0); + + let is_escaped = false; + while (i < parser.template.length) { + const code = full_char_code_at(parser.template, i); + if (!is_escaped && code === quote) { + break; + } + + if (!is_escaped && code === BACKSLASH) { + is_escaped = true; + } else if (is_escaped) { + is_escaped = false; + } + + if ( + i < parser.template.length - 1 && + is_back_quote(quote) && + code === DOLAR && full_char_code_at(parser.template, i + 1) === LEFT_BRACKET + ) { + i++; + i = read_expression_length(parser, i) + } else { + i += code <= 0xffff ? 1 : 2; + } + } + + const code = full_char_code_at(parser.template, i); + if (code !== quote) { + e.unterminated_string_constant(start); + } + + return i + (code <= 0xffff ? 1 : 2); +} + /** * @param {Parser} parser * @returns {any} diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/quote.js b/packages/svelte/src/compiler/phases/1-parse/utils/quote.js new file mode 100644 index 000000000000..a790d5ecc44e --- /dev/null +++ b/packages/svelte/src/compiler/phases/1-parse/utils/quote.js @@ -0,0 +1,14 @@ +const SINGLE_QUOTE = '\''.charCodeAt(0); +const DOUBLE_QUOTE = '"'.charCodeAt(0); +const BACK_QUOTE = '`'.charCodeAt(0); + +/** @param {number} code */ +export function is_quote(code) { + return code === SINGLE_QUOTE || code === DOUBLE_QUOTE || code === BACK_QUOTE; +} + + +/** @param {number} code */ +export function is_back_quote(code) { + return code === BACK_QUOTE; +} From 41609857af7ba4a8cda2f958ed40c649dda3e44f Mon Sep 17 00:00:00 2001 From: Caique Torres Date: Wed, 4 Dec 2024 11:30:06 -0300 Subject: [PATCH 2/9] docs: add documentation for unterminated_string_constant error --- .../docs/98-reference/.generated/compile-errors.md | 6 ++++++ packages/svelte/messages/compile-errors/template.md | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 3bd162d8d74d..a7ff9e7f627d 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -400,6 +400,12 @@ Expected token %token% Expected whitespace ``` +### unterminated_string_constant + +``` +Unterminated string constant +``` + ### export_undefined ``` diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md index 9621a6457ba9..6540af7c76d7 100644 --- a/packages/svelte/messages/compile-errors/template.md +++ b/packages/svelte/messages/compile-errors/template.md @@ -172,6 +172,10 @@ > Expected whitespace +## unterminated_string_constant + +> Unterminated string constant + ## illegal_element_attribute > `<%name%>` does not support non-event attributes or spread attributes From 006911825d2831137379e08a74ff70f00cca1b84 Mon Sep 17 00:00:00 2001 From: Caique Torres Date: Wed, 4 Dec 2024 12:01:21 -0300 Subject: [PATCH 3/9] test: add tests for object destructuring with default values in "each" blocks --- .../input.svelte | 9 + .../output.json | 826 +++++++++++++++++ .../each-block-object-pattern/input.svelte | 8 + .../each-block-object-pattern/output.json | 869 +++++++++++++++++- 4 files changed, 1711 insertions(+), 1 deletion(-) create mode 100644 packages/svelte/tests/parser-modern/samples/each-block-object-pattern-special-characters/input.svelte create mode 100644 packages/svelte/tests/parser-modern/samples/each-block-object-pattern-special-characters/output.json diff --git a/packages/svelte/tests/parser-modern/samples/each-block-object-pattern-special-characters/input.svelte b/packages/svelte/tests/parser-modern/samples/each-block-object-pattern-special-characters/input.svelte new file mode 100644 index 000000000000..bc83f8e57fa8 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/each-block-object-pattern-special-characters/input.svelte @@ -0,0 +1,9 @@ +{#each x as { y = 'z' }}{/each} + +{#each x as { y = '{' }}{/each} + +{#each x as { y = ']' }}{/each} + +{#each x as { y = `${`"`}` }}{/each} + +{#each x as { y = `${`John`}` }}{/each} diff --git a/packages/svelte/tests/parser-modern/samples/each-block-object-pattern-special-characters/output.json b/packages/svelte/tests/parser-modern/samples/each-block-object-pattern-special-characters/output.json new file mode 100644 index 000000000000..b962db322640 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/each-block-object-pattern-special-characters/output.json @@ -0,0 +1,826 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 176, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "EachBlock", + "start": 0, + "end": 31, + "expression": { + "type": "Identifier", + "start": 7, + "end": 8, + "loc": { + "start": { + "line": 1, + "column": 7 + }, + "end": { + "line": 1, + "column": 8 + } + }, + "name": "x" + }, + "body": { + "type": "Fragment", + "nodes": [] + }, + "context": { + "type": "ObjectPattern", + "start": 12, + "end": 23, + "loc": { + "start": { + "line": 1, + "column": 12 + }, + "end": { + "line": 1, + "column": 23 + } + }, + "properties": [ + { + "type": "Property", + "start": 14, + "end": 21, + "loc": { + "start": { + "line": 1, + "column": 14 + }, + "end": { + "line": 1, + "column": 21 + } + }, + "method": false, + "shorthand": true, + "computed": false, + "key": { + "type": "Identifier", + "start": 14, + "end": 15, + "loc": { + "start": { + "line": 1, + "column": 14 + }, + "end": { + "line": 1, + "column": 15 + } + }, + "name": "y" + }, + "kind": "init", + "value": { + "type": "AssignmentPattern", + "start": 14, + "end": 21, + "loc": { + "start": { + "line": 1, + "column": 14 + }, + "end": { + "line": 1, + "column": 21 + } + }, + "left": { + "type": "Identifier", + "start": 14, + "end": 15, + "loc": { + "start": { + "line": 1, + "column": 14 + }, + "end": { + "line": 1, + "column": 15 + } + }, + "name": "y" + }, + "right": { + "type": "Literal", + "start": 18, + "end": 21, + "loc": { + "start": { + "line": 1, + "column": 18 + }, + "end": { + "line": 1, + "column": 21 + } + }, + "value": "z", + "raw": "'z'" + } + } + } + ] + } + }, + { + "type": "Text", + "start": 31, + "end": 33, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "EachBlock", + "start": 33, + "end": 64, + "expression": { + "type": "Identifier", + "start": 40, + "end": 41, + "loc": { + "start": { + "line": 3, + "column": 7 + }, + "end": { + "line": 3, + "column": 8 + } + }, + "name": "x" + }, + "body": { + "type": "Fragment", + "nodes": [] + }, + "context": { + "type": "ObjectPattern", + "start": 45, + "end": 56, + "loc": { + "start": { + "line": 3, + "column": 13 + }, + "end": { + "line": 3, + "column": 24 + } + }, + "properties": [ + { + "type": "Property", + "start": 47, + "end": 54, + "loc": { + "start": { + "line": 3, + "column": 15 + }, + "end": { + "line": 3, + "column": 22 + } + }, + "method": false, + "shorthand": true, + "computed": false, + "key": { + "type": "Identifier", + "start": 47, + "end": 48, + "loc": { + "start": { + "line": 3, + "column": 15 + }, + "end": { + "line": 3, + "column": 16 + } + }, + "name": "y" + }, + "kind": "init", + "value": { + "type": "AssignmentPattern", + "start": 47, + "end": 54, + "loc": { + "start": { + "line": 3, + "column": 15 + }, + "end": { + "line": 3, + "column": 22 + } + }, + "left": { + "type": "Identifier", + "start": 47, + "end": 48, + "loc": { + "start": { + "line": 3, + "column": 15 + }, + "end": { + "line": 3, + "column": 16 + } + }, + "name": "y" + }, + "right": { + "type": "Literal", + "start": 51, + "end": 54, + "loc": { + "start": { + "line": 3, + "column": 19 + }, + "end": { + "line": 3, + "column": 22 + } + }, + "value": "{", + "raw": "'{'" + } + } + } + ] + } + }, + { + "type": "Text", + "start": 64, + "end": 66, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "EachBlock", + "start": 66, + "end": 97, + "expression": { + "type": "Identifier", + "start": 73, + "end": 74, + "loc": { + "start": { + "line": 5, + "column": 7 + }, + "end": { + "line": 5, + "column": 8 + } + }, + "name": "x" + }, + "body": { + "type": "Fragment", + "nodes": [] + }, + "context": { + "type": "ObjectPattern", + "start": 78, + "end": 89, + "loc": { + "start": { + "line": 5, + "column": 13 + }, + "end": { + "line": 5, + "column": 24 + } + }, + "properties": [ + { + "type": "Property", + "start": 80, + "end": 87, + "loc": { + "start": { + "line": 5, + "column": 15 + }, + "end": { + "line": 5, + "column": 22 + } + }, + "method": false, + "shorthand": true, + "computed": false, + "key": { + "type": "Identifier", + "start": 80, + "end": 81, + "loc": { + "start": { + "line": 5, + "column": 15 + }, + "end": { + "line": 5, + "column": 16 + } + }, + "name": "y" + }, + "kind": "init", + "value": { + "type": "AssignmentPattern", + "start": 80, + "end": 87, + "loc": { + "start": { + "line": 5, + "column": 15 + }, + "end": { + "line": 5, + "column": 22 + } + }, + "left": { + "type": "Identifier", + "start": 80, + "end": 81, + "loc": { + "start": { + "line": 5, + "column": 15 + }, + "end": { + "line": 5, + "column": 16 + } + }, + "name": "y" + }, + "right": { + "type": "Literal", + "start": 84, + "end": 87, + "loc": { + "start": { + "line": 5, + "column": 19 + }, + "end": { + "line": 5, + "column": 22 + } + }, + "value": "]", + "raw": "']'" + } + } + } + ] + } + }, + { + "type": "Text", + "start": 97, + "end": 99, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "EachBlock", + "start": 99, + "end": 135, + "expression": { + "type": "Identifier", + "start": 106, + "end": 107, + "loc": { + "start": { + "line": 7, + "column": 7 + }, + "end": { + "line": 7, + "column": 8 + } + }, + "name": "x" + }, + "body": { + "type": "Fragment", + "nodes": [] + }, + "context": { + "type": "ObjectPattern", + "start": 111, + "end": 127, + "loc": { + "start": { + "line": 7, + "column": 13 + }, + "end": { + "line": 7, + "column": 29 + } + }, + "properties": [ + { + "type": "Property", + "start": 113, + "end": 125, + "loc": { + "start": { + "line": 7, + "column": 15 + }, + "end": { + "line": 7, + "column": 27 + } + }, + "method": false, + "shorthand": true, + "computed": false, + "key": { + "type": "Identifier", + "start": 113, + "end": 114, + "loc": { + "start": { + "line": 7, + "column": 15 + }, + "end": { + "line": 7, + "column": 16 + } + }, + "name": "y" + }, + "kind": "init", + "value": { + "type": "AssignmentPattern", + "start": 113, + "end": 125, + "loc": { + "start": { + "line": 7, + "column": 15 + }, + "end": { + "line": 7, + "column": 27 + } + }, + "left": { + "type": "Identifier", + "start": 113, + "end": 114, + "loc": { + "start": { + "line": 7, + "column": 15 + }, + "end": { + "line": 7, + "column": 16 + } + }, + "name": "y" + }, + "right": { + "type": "TemplateLiteral", + "start": 117, + "end": 125, + "loc": { + "start": { + "line": 7, + "column": 19 + }, + "end": { + "line": 7, + "column": 27 + } + }, + "expressions": [ + { + "type": "TemplateLiteral", + "start": 120, + "end": 123, + "loc": { + "start": { + "line": 7, + "column": 22 + }, + "end": { + "line": 7, + "column": 25 + } + }, + "expressions": [], + "quasis": [ + { + "type": "TemplateElement", + "start": 121, + "end": 122, + "loc": { + "start": { + "line": 7, + "column": 23 + }, + "end": { + "line": 7, + "column": 24 + } + }, + "value": { + "raw": "\"", + "cooked": "\"" + }, + "tail": true + } + ] + } + ], + "quasis": [ + { + "type": "TemplateElement", + "start": 118, + "end": 118, + "loc": { + "start": { + "line": 7, + "column": 20 + }, + "end": { + "line": 7, + "column": 20 + } + }, + "value": { + "raw": "", + "cooked": "" + }, + "tail": false + }, + { + "type": "TemplateElement", + "start": 124, + "end": 124, + "loc": { + "start": { + "line": 7, + "column": 26 + }, + "end": { + "line": 7, + "column": 26 + } + }, + "value": { + "raw": "", + "cooked": "" + }, + "tail": true + } + ] + } + } + } + ] + } + }, + { + "type": "Text", + "start": 135, + "end": 137, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "EachBlock", + "start": 137, + "end": 176, + "expression": { + "type": "Identifier", + "start": 144, + "end": 145, + "loc": { + "start": { + "line": 9, + "column": 7 + }, + "end": { + "line": 9, + "column": 8 + } + }, + "name": "x" + }, + "body": { + "type": "Fragment", + "nodes": [] + }, + "context": { + "type": "ObjectPattern", + "start": 149, + "end": 168, + "loc": { + "start": { + "line": 9, + "column": 13 + }, + "end": { + "line": 9, + "column": 32 + } + }, + "properties": [ + { + "type": "Property", + "start": 151, + "end": 166, + "loc": { + "start": { + "line": 9, + "column": 15 + }, + "end": { + "line": 9, + "column": 30 + } + }, + "method": false, + "shorthand": true, + "computed": false, + "key": { + "type": "Identifier", + "start": 151, + "end": 152, + "loc": { + "start": { + "line": 9, + "column": 15 + }, + "end": { + "line": 9, + "column": 16 + } + }, + "name": "y" + }, + "kind": "init", + "value": { + "type": "AssignmentPattern", + "start": 151, + "end": 166, + "loc": { + "start": { + "line": 9, + "column": 15 + }, + "end": { + "line": 9, + "column": 30 + } + }, + "left": { + "type": "Identifier", + "start": 151, + "end": 152, + "loc": { + "start": { + "line": 9, + "column": 15 + }, + "end": { + "line": 9, + "column": 16 + } + }, + "name": "y" + }, + "right": { + "type": "TemplateLiteral", + "start": 155, + "end": 166, + "loc": { + "start": { + "line": 9, + "column": 19 + }, + "end": { + "line": 9, + "column": 30 + } + }, + "expressions": [ + { + "type": "TemplateLiteral", + "start": 158, + "end": 164, + "loc": { + "start": { + "line": 9, + "column": 22 + }, + "end": { + "line": 9, + "column": 28 + } + }, + "expressions": [], + "quasis": [ + { + "type": "TemplateElement", + "start": 159, + "end": 163, + "loc": { + "start": { + "line": 9, + "column": 23 + }, + "end": { + "line": 9, + "column": 27 + } + }, + "value": { + "raw": "John", + "cooked": "John" + }, + "tail": true + } + ] + } + ], + "quasis": [ + { + "type": "TemplateElement", + "start": 156, + "end": 156, + "loc": { + "start": { + "line": 9, + "column": 20 + }, + "end": { + "line": 9, + "column": 20 + } + }, + "value": { + "raw": "", + "cooked": "" + }, + "tail": false + }, + { + "type": "TemplateElement", + "start": 165, + "end": 165, + "loc": { + "start": { + "line": 9, + "column": 29 + }, + "end": { + "line": 9, + "column": 29 + } + }, + "value": { + "raw": "", + "cooked": "" + }, + "tail": true + } + ] + } + } + } + ] + } + } + ] + }, + "options": null +} diff --git a/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/input.svelte b/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/input.svelte index 8ffe8a7287a5..c3bfdb46a6f3 100644 --- a/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/input.svelte +++ b/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/input.svelte @@ -1,3 +1,11 @@ {#each people as { name, cool = true }}

{name} is {cool ? 'cool' : 'not cool'}

{/each} + +{#each people as { name = `Jane ${"Doe"}`, cool = true }} +

{name} is {cool ? 'cool' : 'not cool'}

+{/each} + +{#each people as { name = (() => { return `Jane ${"Doe"}`; })(), cool = true }} +

{name} is {cool ? 'cool' : 'not cool'}

+{/each} diff --git a/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/output.json b/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/output.json index 8fc3feb91664..144016417b84 100644 --- a/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/output.json +++ b/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/output.json @@ -2,7 +2,7 @@ "css": null, "js": [], "start": 0, - "end": 94, + "end": 344, "type": "Root", "fragment": { "type": "Fragment", @@ -307,6 +307,873 @@ } ] } + }, + { + "type": "Text", + "start": 94, + "end": 96, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "EachBlock", + "start": 96, + "end": 208, + "expression": { + "type": "Identifier", + "start": 103, + "end": 109, + "loc": { + "start": { + "line": 5, + "column": 7 + }, + "end": { + "line": 5, + "column": 13 + } + }, + "name": "people" + }, + "body": { + "type": "Fragment", + "nodes": [ + { + "type": "Text", + "start": 153, + "end": 155, + "raw": "\n\t", + "data": "\n\t" + }, + { + "type": "RegularElement", + "start": 155, + "end": 200, + "name": "p", + "attributes": [], + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "ExpressionTag", + "start": 158, + "end": 164, + "expression": { + "type": "Identifier", + "start": 159, + "end": 163, + "loc": { + "start": { + "line": 6, + "column": 5 + }, + "end": { + "line": 6, + "column": 9 + } + }, + "name": "name" + } + }, + { + "type": "Text", + "start": 164, + "end": 168, + "raw": " is ", + "data": " is " + }, + { + "type": "ExpressionTag", + "start": 168, + "end": 196, + "expression": { + "type": "ConditionalExpression", + "start": 169, + "end": 195, + "loc": { + "start": { + "line": 6, + "column": 15 + }, + "end": { + "line": 6, + "column": 41 + } + }, + "test": { + "type": "Identifier", + "start": 169, + "end": 173, + "loc": { + "start": { + "line": 6, + "column": 15 + }, + "end": { + "line": 6, + "column": 19 + } + }, + "name": "cool" + }, + "consequent": { + "type": "Literal", + "start": 176, + "end": 182, + "loc": { + "start": { + "line": 6, + "column": 22 + }, + "end": { + "line": 6, + "column": 28 + } + }, + "value": "cool", + "raw": "'cool'" + }, + "alternate": { + "type": "Literal", + "start": 185, + "end": 195, + "loc": { + "start": { + "line": 6, + "column": 31 + }, + "end": { + "line": 6, + "column": 41 + } + }, + "value": "not cool", + "raw": "'not cool'" + } + } + } + ] + } + }, + { + "type": "Text", + "start": 200, + "end": 201, + "raw": "\n", + "data": "\n" + } + ] + }, + "context": { + "type": "ObjectPattern", + "start": 113, + "end": 152, + "loc": { + "start": { + "line": 5, + "column": 18 + }, + "end": { + "line": 5, + "column": 57 + } + }, + "properties": [ + { + "type": "Property", + "start": 115, + "end": 137, + "loc": { + "start": { + "line": 5, + "column": 20 + }, + "end": { + "line": 5, + "column": 42 + } + }, + "method": false, + "shorthand": true, + "computed": false, + "key": { + "type": "Identifier", + "start": 115, + "end": 119, + "loc": { + "start": { + "line": 5, + "column": 20 + }, + "end": { + "line": 5, + "column": 24 + } + }, + "name": "name" + }, + "kind": "init", + "value": { + "type": "AssignmentPattern", + "start": 115, + "end": 137, + "loc": { + "start": { + "line": 5, + "column": 20 + }, + "end": { + "line": 5, + "column": 42 + } + }, + "left": { + "type": "Identifier", + "start": 115, + "end": 119, + "loc": { + "start": { + "line": 5, + "column": 20 + }, + "end": { + "line": 5, + "column": 24 + } + }, + "name": "name" + }, + "right": { + "type": "TemplateLiteral", + "start": 122, + "end": 137, + "loc": { + "start": { + "line": 5, + "column": 27 + }, + "end": { + "line": 5, + "column": 42 + } + }, + "expressions": [ + { + "type": "Literal", + "start": 130, + "end": 135, + "loc": { + "start": { + "line": 5, + "column": 35 + }, + "end": { + "line": 5, + "column": 40 + } + }, + "value": "Doe", + "raw": "\"Doe\"" + } + ], + "quasis": [ + { + "type": "TemplateElement", + "start": 123, + "end": 128, + "loc": { + "start": { + "line": 5, + "column": 28 + }, + "end": { + "line": 5, + "column": 33 + } + }, + "value": { + "raw": "Jane ", + "cooked": "Jane " + }, + "tail": false + }, + { + "type": "TemplateElement", + "start": 136, + "end": 136, + "loc": { + "start": { + "line": 5, + "column": 41 + }, + "end": { + "line": 5, + "column": 41 + } + }, + "value": { + "raw": "", + "cooked": "" + }, + "tail": true + } + ] + } + } + }, + { + "type": "Property", + "start": 139, + "end": 150, + "loc": { + "start": { + "line": 5, + "column": 44 + }, + "end": { + "line": 5, + "column": 55 + } + }, + "method": false, + "shorthand": true, + "computed": false, + "key": { + "type": "Identifier", + "start": 139, + "end": 143, + "loc": { + "start": { + "line": 5, + "column": 44 + }, + "end": { + "line": 5, + "column": 48 + } + }, + "name": "cool" + }, + "kind": "init", + "value": { + "type": "AssignmentPattern", + "start": 139, + "end": 150, + "loc": { + "start": { + "line": 5, + "column": 44 + }, + "end": { + "line": 5, + "column": 55 + } + }, + "left": { + "type": "Identifier", + "start": 139, + "end": 143, + "loc": { + "start": { + "line": 5, + "column": 44 + }, + "end": { + "line": 5, + "column": 48 + } + }, + "name": "cool" + }, + "right": { + "type": "Literal", + "start": 146, + "end": 150, + "loc": { + "start": { + "line": 5, + "column": 51 + }, + "end": { + "line": 5, + "column": 55 + } + }, + "value": true, + "raw": "true" + } + } + } + ] + } + }, + { + "type": "Text", + "start": 208, + "end": 210, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "EachBlock", + "start": 210, + "end": 344, + "expression": { + "type": "Identifier", + "start": 217, + "end": 223, + "loc": { + "start": { + "line": 9, + "column": 7 + }, + "end": { + "line": 9, + "column": 13 + } + }, + "name": "people" + }, + "body": { + "type": "Fragment", + "nodes": [ + { + "type": "Text", + "start": 289, + "end": 291, + "raw": "\n\t", + "data": "\n\t" + }, + { + "type": "RegularElement", + "start": 291, + "end": 336, + "name": "p", + "attributes": [], + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "ExpressionTag", + "start": 294, + "end": 300, + "expression": { + "type": "Identifier", + "start": 295, + "end": 299, + "loc": { + "start": { + "line": 10, + "column": 5 + }, + "end": { + "line": 10, + "column": 9 + } + }, + "name": "name" + } + }, + { + "type": "Text", + "start": 300, + "end": 304, + "raw": " is ", + "data": " is " + }, + { + "type": "ExpressionTag", + "start": 304, + "end": 332, + "expression": { + "type": "ConditionalExpression", + "start": 305, + "end": 331, + "loc": { + "start": { + "line": 10, + "column": 15 + }, + "end": { + "line": 10, + "column": 41 + } + }, + "test": { + "type": "Identifier", + "start": 305, + "end": 309, + "loc": { + "start": { + "line": 10, + "column": 15 + }, + "end": { + "line": 10, + "column": 19 + } + }, + "name": "cool" + }, + "consequent": { + "type": "Literal", + "start": 312, + "end": 318, + "loc": { + "start": { + "line": 10, + "column": 22 + }, + "end": { + "line": 10, + "column": 28 + } + }, + "value": "cool", + "raw": "'cool'" + }, + "alternate": { + "type": "Literal", + "start": 321, + "end": 331, + "loc": { + "start": { + "line": 10, + "column": 31 + }, + "end": { + "line": 10, + "column": 41 + } + }, + "value": "not cool", + "raw": "'not cool'" + } + } + } + ] + } + }, + { + "type": "Text", + "start": 336, + "end": 337, + "raw": "\n", + "data": "\n" + } + ] + }, + "context": { + "type": "ObjectPattern", + "start": 227, + "end": 288, + "loc": { + "start": { + "line": 9, + "column": 18 + }, + "end": { + "line": 9, + "column": 79 + } + }, + "properties": [ + { + "type": "Property", + "start": 229, + "end": 273, + "loc": { + "start": { + "line": 9, + "column": 20 + }, + "end": { + "line": 9, + "column": 64 + } + }, + "method": false, + "shorthand": true, + "computed": false, + "key": { + "type": "Identifier", + "start": 229, + "end": 233, + "loc": { + "start": { + "line": 9, + "column": 20 + }, + "end": { + "line": 9, + "column": 24 + } + }, + "name": "name" + }, + "kind": "init", + "value": { + "type": "AssignmentPattern", + "start": 229, + "end": 273, + "loc": { + "start": { + "line": 9, + "column": 20 + }, + "end": { + "line": 9, + "column": 64 + } + }, + "left": { + "type": "Identifier", + "start": 229, + "end": 233, + "loc": { + "start": { + "line": 9, + "column": 20 + }, + "end": { + "line": 9, + "column": 24 + } + }, + "name": "name" + }, + "right": { + "type": "CallExpression", + "start": 236, + "end": 273, + "loc": { + "start": { + "line": 9, + "column": 27 + }, + "end": { + "line": 9, + "column": 64 + } + }, + "callee": { + "type": "ArrowFunctionExpression", + "start": 237, + "end": 270, + "loc": { + "start": { + "line": 9, + "column": 28 + }, + "end": { + "line": 9, + "column": 61 + } + }, + "id": null, + "expression": false, + "generator": false, + "async": false, + "params": [], + "body": { + "type": "BlockStatement", + "start": 243, + "end": 270, + "loc": { + "start": { + "line": 9, + "column": 34 + }, + "end": { + "line": 9, + "column": 61 + } + }, + "body": [ + { + "type": "ReturnStatement", + "start": 245, + "end": 268, + "loc": { + "start": { + "line": 9, + "column": 36 + }, + "end": { + "line": 9, + "column": 59 + } + }, + "argument": { + "type": "TemplateLiteral", + "start": 252, + "end": 267, + "loc": { + "start": { + "line": 9, + "column": 43 + }, + "end": { + "line": 9, + "column": 58 + } + }, + "expressions": [ + { + "type": "Literal", + "start": 260, + "end": 265, + "loc": { + "start": { + "line": 9, + "column": 51 + }, + "end": { + "line": 9, + "column": 56 + } + }, + "value": "Doe", + "raw": "\"Doe\"" + } + ], + "quasis": [ + { + "type": "TemplateElement", + "start": 253, + "end": 258, + "loc": { + "start": { + "line": 9, + "column": 44 + }, + "end": { + "line": 9, + "column": 49 + } + }, + "value": { + "raw": "Jane ", + "cooked": "Jane " + }, + "tail": false + }, + { + "type": "TemplateElement", + "start": 266, + "end": 266, + "loc": { + "start": { + "line": 9, + "column": 57 + }, + "end": { + "line": 9, + "column": 57 + } + }, + "value": { + "raw": "", + "cooked": "" + }, + "tail": true + } + ] + } + } + ] + } + }, + "arguments": [], + "optional": false + } + } + }, + { + "type": "Property", + "start": 275, + "end": 286, + "loc": { + "start": { + "line": 9, + "column": 66 + }, + "end": { + "line": 9, + "column": 77 + } + }, + "method": false, + "shorthand": true, + "computed": false, + "key": { + "type": "Identifier", + "start": 275, + "end": 279, + "loc": { + "start": { + "line": 9, + "column": 66 + }, + "end": { + "line": 9, + "column": 70 + } + }, + "name": "cool" + }, + "kind": "init", + "value": { + "type": "AssignmentPattern", + "start": 275, + "end": 286, + "loc": { + "start": { + "line": 9, + "column": 66 + }, + "end": { + "line": 9, + "column": 77 + } + }, + "left": { + "type": "Identifier", + "start": 275, + "end": 279, + "loc": { + "start": { + "line": 9, + "column": 66 + }, + "end": { + "line": 9, + "column": 70 + } + }, + "name": "cool" + }, + "right": { + "type": "Literal", + "start": 282, + "end": 286, + "loc": { + "start": { + "line": 9, + "column": 73 + }, + "end": { + "line": 9, + "column": 77 + } + }, + "value": true, + "raw": "true" + } + } + } + ] + } } ] }, From 47e97853840eeaea16ec01440ab6957333f4fab8 Mon Sep 17 00:00:00 2001 From: Caique Torres Date: Wed, 4 Dec 2024 12:07:35 -0300 Subject: [PATCH 4/9] Update .changeset/clean-planets-rush.md --- .changeset/clean-planets-rush.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/clean-planets-rush.md diff --git a/.changeset/clean-planets-rush.md b/.changeset/clean-planets-rush.md new file mode 100644 index 000000000000..d8500d431524 --- /dev/null +++ b/.changeset/clean-planets-rush.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: handle default values in object destructuring within "each" blocks when using characters like "}" and "]" From a8e585dd6340bd2df0273a1d3b6c1d966a66f7ef Mon Sep 17 00:00:00 2001 From: Caique Torres Date: Wed, 4 Dec 2024 12:09:39 -0300 Subject: [PATCH 5/9] refactor: clean up unnecessary comments and whitespace --- packages/svelte/src/compiler/phases/1-parse/read/context.js | 1 - packages/svelte/src/compiler/phases/1-parse/utils/quote.js | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/read/context.js b/packages/svelte/src/compiler/phases/1-parse/read/context.js index 516e14fb1057..20f021f3a38e 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/context.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/context.js @@ -120,7 +120,6 @@ function read_expression_length(parser, start) { * @param {Parser} parser * @param {number} start * @param {number} quote - * @returns {number} */ function read_string_length(parser, start, quote) { let i = start; diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/quote.js b/packages/svelte/src/compiler/phases/1-parse/utils/quote.js index a790d5ecc44e..25f9e1fac8ab 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/quote.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/quote.js @@ -7,7 +7,6 @@ export function is_quote(code) { return code === SINGLE_QUOTE || code === DOUBLE_QUOTE || code === BACK_QUOTE; } - /** @param {number} code */ export function is_back_quote(code) { return code === BACK_QUOTE; From 11236294eb836e72e740d27356025e923fbd6248 Mon Sep 17 00:00:00 2001 From: Caique Torres Date: Thu, 5 Dec 2024 08:22:34 -0300 Subject: [PATCH 6/9] fix: resolve formatting issues --- .../98-reference/.generated/compile-errors.md | 12 +++++------ .../messages/compile-errors/template.md | 8 ++++---- packages/svelte/src/compiler/errors.js | 20 +++++++++---------- .../compiler/phases/1-parse/read/context.js | 10 +++++++--- .../compiler/phases/1-parse/utils/quote.js | 2 +- 5 files changed, 28 insertions(+), 24 deletions(-) diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index a7ff9e7f627d..05304d747f43 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -400,12 +400,6 @@ Expected token %token% Expected whitespace ``` -### unterminated_string_constant - -``` -Unterminated string constant -``` - ### export_undefined ``` @@ -994,6 +988,12 @@ Unexpected end of input '%word%' is a reserved word in JavaScript and cannot be used here ``` +### unterminated_string_constant + +``` +Unterminated string constant +``` + ### void_element_invalid_content ``` diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md index 6540af7c76d7..599a1d77a57f 100644 --- a/packages/svelte/messages/compile-errors/template.md +++ b/packages/svelte/messages/compile-errors/template.md @@ -172,10 +172,6 @@ > Expected whitespace -## unterminated_string_constant - -> Unterminated string constant - ## illegal_element_attribute > `<%name%>` does not support non-event attributes or spread attributes @@ -414,6 +410,10 @@ See https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-ele > '%word%' is a reserved word in JavaScript and cannot be used here +## unterminated_string_constant + +> Unterminated string constant + ## void_element_invalid_content > Void elements cannot have children or closing tags diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 97b284af8b35..4c51a6422e67 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -1003,15 +1003,6 @@ export function expected_whitespace(node) { e(node, "expected_whitespace", "Expected whitespace"); } -/** - * Unterminated string constant - * @param {null | number | NodeLike} node - * @returns {never} - */ -export function unterminated_string_constant(node) { - e(node, "unterminated_string_constant", "Unterminated string constant"); -} - /** * `<%name%>` does not support non-event attributes or spread attributes * @param {null | number | NodeLike} node @@ -1546,6 +1537,15 @@ export function unexpected_reserved_word(node, word) { e(node, "unexpected_reserved_word", `'${word}' is a reserved word in JavaScript and cannot be used here`); } +/** + * Unterminated string constant + * @param {null | number | NodeLike} node + * @returns {never} + */ +export function unterminated_string_constant(node) { + e(node, "unterminated_string_constant", "Unterminated string constant"); +} + /** * Void elements cannot have children or closing tags * @param {null | number | NodeLike} node @@ -1553,4 +1553,4 @@ export function unexpected_reserved_word(node, word) { */ export function void_element_invalid_content(node) { e(node, "void_element_invalid_content", "Void elements cannot have children or closing tags"); -} +} \ No newline at end of file diff --git a/packages/svelte/src/compiler/phases/1-parse/read/context.js b/packages/svelte/src/compiler/phases/1-parse/read/context.js index 20f021f3a38e..08a0cc482295 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/context.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/context.js @@ -102,7 +102,10 @@ function read_expression_length(parser, start) { } else if (is_bracket_close(code)) { const popped = /** @type {number} */ (bracket_stack.pop()); if (!is_bracket_pair(popped, code)) { - e.expected_token(i, String.fromCharCode(/** @type {number} */(get_bracket_close(popped)))); + e.expected_token( + i, + String.fromCharCode(/** @type {number} */ (get_bracket_close(popped))) + ); } if (bracket_stack.length === 0) { i += code <= 0xffff ? 1 : 2; @@ -145,10 +148,11 @@ function read_string_length(parser, start, quote) { if ( i < parser.template.length - 1 && is_back_quote(quote) && - code === DOLAR && full_char_code_at(parser.template, i + 1) === LEFT_BRACKET + code === DOLAR && + full_char_code_at(parser.template, i + 1) === LEFT_BRACKET ) { i++; - i = read_expression_length(parser, i) + i = read_expression_length(parser, i); } else { i += code <= 0xffff ? 1 : 2; } diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/quote.js b/packages/svelte/src/compiler/phases/1-parse/utils/quote.js index 25f9e1fac8ab..517e7b3a55da 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/quote.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/quote.js @@ -1,4 +1,4 @@ -const SINGLE_QUOTE = '\''.charCodeAt(0); +const SINGLE_QUOTE = "'".charCodeAt(0); const DOUBLE_QUOTE = '"'.charCodeAt(0); const BACK_QUOTE = '`'.charCodeAt(0); From fe9d595238bc8f5a38ff0b42537be639adc358cf Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 7 Jan 2025 18:15:00 -0500 Subject: [PATCH 7/9] simplify --- .../compiler/phases/1-parse/read/context.js | 80 ++++++++----------- .../compiler/phases/1-parse/utils/quote.js | 13 --- 2 files changed, 33 insertions(+), 60 deletions(-) delete mode 100644 packages/svelte/src/compiler/phases/1-parse/utils/quote.js diff --git a/packages/svelte/src/compiler/phases/1-parse/read/context.js b/packages/svelte/src/compiler/phases/1-parse/read/context.js index 1d5eff21e8cb..b05133acc7be 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/context.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/context.js @@ -6,8 +6,6 @@ import { parse_expression_at } from '../acorn.js'; import { regex_not_newline_characters } from '../../patterns.js'; import * as e from '../../../errors.js'; import { locator } from '../../../state.js'; -import read_expression from './expression.js'; -import { is_back_quote, is_quote } from '../utils/quote.js'; /** * @param {Parser} parser @@ -35,7 +33,11 @@ export default function read_pattern(parser) { }; } - i = read_expression_length(parser, start); + if (!is_bracket_open(parser.template[i])) { + e.expected_pattern(i); + } + + i = match_bracket(parser, start); parser.index = i; const pattern_string = parser.template.slice(start, i); @@ -73,41 +75,36 @@ export default function read_pattern(parser) { * @param {Parser} parser * @param {number} start */ -function read_expression_length(parser, start) { +function match_bracket(parser, start) { + const bracket_stack = []; + let i = start; - if (!is_bracket_open(parser.template[i])) { - e.expected_pattern(i); - } + while (i < parser.template.length) { + let char = parser.template[i++]; - const bracket_stack = []; + if (char === "'" || char === '"' || char === '`') { + i = match_quote(parser, i, char); + continue; + } - while (i < parser.template.length) { - let char = parser.template[i]; - - if (is_quote(char)) { - i = read_string_length(parser, i + 1, char); - } else { - if (is_bracket_open(char)) { - bracket_stack.push(char); - } else if (is_bracket_close(char)) { - const popped = /** @type {string} */ (bracket_stack.pop()); - const expected = /** @type {string} */ (get_bracket_close(popped)); - - if (char !== expected) { - e.expected_token(i, expected); - } - - if (bracket_stack.length === 0) { - return i + 1; - } + if (is_bracket_open(char)) { + bracket_stack.push(char); + } else if (is_bracket_close(char)) { + const popped = /** @type {string} */ (bracket_stack.pop()); + const expected = /** @type {string} */ (get_bracket_close(popped)); + + if (char !== expected) { + e.expected_token(i - 1, expected); } - i += 1; + if (bracket_stack.length === 0) { + return i; + } } } - return i; + e.unexpected_eof(parser.template.length); } /** @@ -115,36 +112,25 @@ function read_expression_length(parser, start) { * @param {number} start * @param {string} quote */ -function read_string_length(parser, start, quote) { +function match_quote(parser, start, quote) { + let is_escaped = false; let i = start; - const BACKSLASH = '\\'; - const DOLAR = '$'; - const LEFT_BRACKET = '{'; - - let is_escaped = false; while (i < parser.template.length) { - const char = parser.template[i]; + const char = parser.template[i++]; if (!is_escaped && char === quote) { - return i + 1; + return i; } - if (!is_escaped && char === BACKSLASH) { + if (!is_escaped && char === '\\') { is_escaped = true; } else if (is_escaped) { is_escaped = false; } - if ( - i < parser.template.length - 1 && - is_back_quote(quote) && - char === DOLAR && - parser.template[i + 1] === LEFT_BRACKET - ) { - i = read_expression_length(parser, i + 1); - } else { - i += 1; + if (quote === '`' && char === '$' && parser.template[i] === '{') { + i = match_bracket(parser, i); } } diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/quote.js b/packages/svelte/src/compiler/phases/1-parse/utils/quote.js deleted file mode 100644 index efffbcf6eb68..000000000000 --- a/packages/svelte/src/compiler/phases/1-parse/utils/quote.js +++ /dev/null @@ -1,13 +0,0 @@ -const SINGLE_QUOTE = "'"; -const DOUBLE_QUOTE = '"'; -const BACK_QUOTE = '`'; - -/** @param {string} char */ -export function is_quote(char) { - return char === SINGLE_QUOTE || char === DOUBLE_QUOTE || char === BACK_QUOTE; -} - -/** @param {string} char */ -export function is_back_quote(char) { - return char === BACK_QUOTE; -} From 463c8f724e471505b064124fb69738ee8c5e21c5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 7 Jan 2025 18:20:01 -0500 Subject: [PATCH 8/9] tweak --- .../src/compiler/phases/1-parse/read/context.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/read/context.js b/packages/svelte/src/compiler/phases/1-parse/read/context.js index b05133acc7be..f4c73dcf403a 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/context.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/context.js @@ -119,14 +119,17 @@ function match_quote(parser, start, quote) { while (i < parser.template.length) { const char = parser.template[i++]; - if (!is_escaped && char === quote) { + if (is_escaped) { + is_escaped = false; + continue; + } + + if (char === quote) { return i; } - if (!is_escaped && char === '\\') { + if (char === '\\') { is_escaped = true; - } else if (is_escaped) { - is_escaped = false; } if (quote === '`' && char === '$' && parser.template[i] === '{') { From bfe3b715269e5bb59b296200fbc3e6012057f702 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 7 Jan 2025 18:28:28 -0500 Subject: [PATCH 9/9] regenerate --- packages/svelte/src/compiler/errors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 2a38909897aa..870cd9ac093d 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -1580,7 +1580,7 @@ export function unexpected_reserved_word(node, word) { * @returns {never} */ export function unterminated_string_constant(node) { - e(node, "unterminated_string_constant", "Unterminated string constant"); + e(node, "unterminated_string_constant", `Unterminated string constant\nhttps://svelte.dev/e/unterminated_string_constant`); } /**