diff --git a/packages/svelte/src/compiler/phases/1-parse/read/expression.js b/packages/svelte/src/compiler/phases/1-parse/read/expression.js index 4e33c23f28..2930dc5b0a 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/expression.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/expression.js @@ -39,6 +39,38 @@ export default function read_expression(parser) { return /** @type {Expression} */ (node); } catch (err) { + if (parser.loose) { + // Find the next } and treat it as the end of the expression + let index = parser.index; + let num_braces = 0; + while (index < parser.template.length) { + const char = parser.template[index]; + if (char === '{') num_braces += 1; + if (char === '}') { + if (num_braces === 0) { + // We assume that there's some kind of whitespace or the start of the closing tag after the closing brace, + // else this hints at a wrong counting of braces (e.g. in the case of foo={'hi}'}) + if (!/[\s>/]/.test(parser.template[index + 1])) { + num_braces += 1; + continue; + } + + const start = parser.index; + parser.index = index; + // We don't know what the expression is and signal this by returning an empty identifier + return { + type: 'Identifier', + start, + end: index, + name: '' + }; + } + num_braces -= 1; + } + index += 1; + } + } + parser.acorn_error(err); } } 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 b7473d073a..b778e19748 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -481,7 +481,7 @@ function read_attribute(parser) { return spread; } else { const value_start = parser.index; - const name = parser.read_identifier(); + let name = parser.read_identifier(); if (name === null) { if ( @@ -491,8 +491,12 @@ function read_attribute(parser) { // We're likely in an unclosed opening tag and did read part of a block. // Return null to not crash the parser so it can continue with closing the tag. return null; + } else if (parser.loose && parser.match('}')) { + // Likely in the middle of typing, just created the shorthand + name = ''; + } else { + e.attribute_empty_shorthand(start); } - e.attribute_empty_shorthand(start); } parser.allow_whitespace(); diff --git a/packages/svelte/tests/parser-legacy/samples/loose-invalid-expression/input.svelte b/packages/svelte/tests/parser-legacy/samples/loose-invalid-expression/input.svelte new file mode 100644 index 0000000000..bd1db72f53 --- /dev/null +++ b/packages/svelte/tests/parser-legacy/samples/loose-invalid-expression/input.svelte @@ -0,0 +1,8 @@ +
+
+ +
+
+ x.} /> + + diff --git a/packages/svelte/tests/parser-legacy/samples/loose-invalid-expression/output.json b/packages/svelte/tests/parser-legacy/samples/loose-invalid-expression/output.json new file mode 100644 index 0000000000..312a46d912 --- /dev/null +++ b/packages/svelte/tests/parser-legacy/samples/loose-invalid-expression/output.json @@ -0,0 +1,206 @@ +{ + "html": { + "type": "Fragment", + "start": 0, + "end": 140, + "children": [ + { + "type": "Element", + "start": 0, + "end": 14, + "name": "div", + "attributes": [ + { + "type": "Attribute", + "start": 5, + "end": 7, + "name": "", + "value": [ + { + "type": "AttributeShorthand", + "start": 6, + "end": 6, + "expression": { + "start": 6, + "end": 6, + "type": "Identifier", + "name": "" + } + } + ] + } + ], + "children": [] + }, + { + "type": "Text", + "start": 14, + "end": 15, + "raw": "\n", + "data": "\n" + }, + { + "type": "Element", + "start": 15, + "end": 33, + "name": "div", + "attributes": [ + { + "type": "Attribute", + "start": 20, + "end": 26, + "name": "foo", + "value": [ + { + "type": "MustacheTag", + "start": 24, + "end": 26, + "expression": { + "type": "Identifier", + "start": 25, + "end": 25, + "name": "" + } + } + ] + } + ], + "children": [] + }, + { + "type": "Text", + "start": 33, + "end": 35, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "Element", + "start": 35, + "end": 55, + "name": "div", + "attributes": [ + { + "type": "Attribute", + "start": 40, + "end": 48, + "name": "foo", + "value": [ + { + "type": "MustacheTag", + "start": 44, + "end": 48, + "expression": { + "type": "Identifier", + "start": 45, + "end": 47, + "name": "" + } + } + ] + } + ], + "children": [] + }, + { + "type": "Text", + "start": 55, + "end": 56, + "raw": "\n", + "data": "\n" + }, + { + "type": "Element", + "start": 56, + "end": 80, + "name": "div", + "attributes": [ + { + "type": "Attribute", + "start": 61, + "end": 73, + "name": "foo", + "value": [ + { + "type": "MustacheTag", + "start": 65, + "end": 73, + "expression": { + "type": "Identifier", + "start": 66, + "end": 72, + "name": "" + } + } + ] + } + ], + "children": [] + }, + { + "type": "Text", + "start": 80, + "end": 81, + "raw": "\n", + "data": "\n" + }, + { + "type": "InlineComponent", + "start": 81, + "end": 113, + "name": "Component", + "attributes": [ + { + "type": "Attribute", + "start": 92, + "end": 110, + "name": "onclick", + "value": [ + { + "type": "MustacheTag", + "start": 100, + "end": 110, + "expression": { + "type": "Identifier", + "start": 101, + "end": 109, + "name": "" + } + } + ] + } + ], + "children": [] + }, + { + "type": "Text", + "start": 113, + "end": 115, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "Element", + "start": 115, + "end": 140, + "name": "input", + "attributes": [ + { + "start": 122, + "end": 137, + "type": "Binding", + "name": "value", + "expression": { + "type": "Identifier", + "start": 134, + "end": 136, + "name": "" + }, + "modifiers": [] + } + ], + "children": [] + } + ] + } +} diff --git a/packages/svelte/tests/parser-modern/samples/loose-invalid-expression/input.svelte b/packages/svelte/tests/parser-modern/samples/loose-invalid-expression/input.svelte new file mode 100644 index 0000000000..bd1db72f53 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/loose-invalid-expression/input.svelte @@ -0,0 +1,8 @@ +
+
+ +
+
+ x.} /> + + diff --git a/packages/svelte/tests/parser-modern/samples/loose-invalid-expression/output.json b/packages/svelte/tests/parser-modern/samples/loose-invalid-expression/output.json new file mode 100644 index 0000000000..b1ca4b9c7e --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/loose-invalid-expression/output.json @@ -0,0 +1,218 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 140, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "RegularElement", + "start": 0, + "end": 14, + "name": "div", + "attributes": [ + { + "type": "Attribute", + "start": 5, + "end": 7, + "name": "", + "value": { + "type": "ExpressionTag", + "start": 6, + "end": 6, + "expression": { + "start": 6, + "end": 6, + "type": "Identifier", + "name": "" + } + } + } + ], + "fragment": { + "type": "Fragment", + "nodes": [] + } + }, + { + "type": "Text", + "start": 14, + "end": 15, + "raw": "\n", + "data": "\n" + }, + { + "type": "RegularElement", + "start": 15, + "end": 33, + "name": "div", + "attributes": [ + { + "type": "Attribute", + "start": 20, + "end": 26, + "name": "foo", + "value": { + "type": "ExpressionTag", + "start": 24, + "end": 26, + "expression": { + "type": "Identifier", + "start": 25, + "end": 25, + "name": "" + } + } + } + ], + "fragment": { + "type": "Fragment", + "nodes": [] + } + }, + { + "type": "Text", + "start": 33, + "end": 35, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "RegularElement", + "start": 35, + "end": 55, + "name": "div", + "attributes": [ + { + "type": "Attribute", + "start": 40, + "end": 48, + "name": "foo", + "value": { + "type": "ExpressionTag", + "start": 44, + "end": 48, + "expression": { + "type": "Identifier", + "start": 45, + "end": 47, + "name": "" + } + } + } + ], + "fragment": { + "type": "Fragment", + "nodes": [] + } + }, + { + "type": "Text", + "start": 55, + "end": 56, + "raw": "\n", + "data": "\n" + }, + { + "type": "RegularElement", + "start": 56, + "end": 80, + "name": "div", + "attributes": [ + { + "type": "Attribute", + "start": 61, + "end": 73, + "name": "foo", + "value": { + "type": "ExpressionTag", + "start": 65, + "end": 73, + "expression": { + "type": "Identifier", + "start": 66, + "end": 72, + "name": "" + } + } + } + ], + "fragment": { + "type": "Fragment", + "nodes": [] + } + }, + { + "type": "Text", + "start": 80, + "end": 81, + "raw": "\n", + "data": "\n" + }, + { + "type": "Component", + "start": 81, + "end": 113, + "name": "Component", + "attributes": [ + { + "type": "Attribute", + "start": 92, + "end": 110, + "name": "onclick", + "value": { + "type": "ExpressionTag", + "start": 100, + "end": 110, + "expression": { + "type": "Identifier", + "start": 101, + "end": 109, + "name": "" + } + } + } + ], + "fragment": { + "type": "Fragment", + "nodes": [] + } + }, + { + "type": "Text", + "start": 113, + "end": 115, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "RegularElement", + "start": 115, + "end": 140, + "name": "input", + "attributes": [ + { + "start": 122, + "end": 137, + "type": "BindDirective", + "name": "value", + "expression": { + "type": "Identifier", + "start": 134, + "end": 136, + "name": "" + }, + "modifiers": [] + } + ], + "fragment": { + "type": "Fragment", + "nodes": [] + } + } + ] + }, + "options": null +}