diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index a378055b51..cf80f0e1bd 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -3,7 +3,9 @@ import read_expression from '../read/expression.js'; import { error } from '../../../errors.js'; import { create_fragment } from '../utils/create.js'; import { walk } from 'zimmerframe'; -import { parse } from '../acorn.js'; +import { parse, parse_expression_at } from '../acorn.js'; +import { find_matching_bracket } from '../utils/bracket.js'; +import full_char_code_at from '../utils/full_char_code_at.js'; const regex_whitespace_with_closing_curly_brace = /^\s*}/; @@ -263,29 +265,40 @@ function open(parser) { return; } - if (parser.eat('snippet')) { - parser.require_whitespace(); - - const name_start = parser.index; - const name = parser.read_identifier(); - const name_end = parser.index; - - parser.eat('(', true); - - parser.allow_whitespace(); + if (parser.match('snippet')) { + const snippet_declaraion_end = find_matching_bracket( + parser, + full_char_code_at(parser.template, start) + ); + // we'll eat this later, so save it + const raw_snippet_declaration = parser.template.slice(start, snippet_declaraion_end); + const start_subtractions = '{#snippet '; + const end_subtractions = '}'; + + // now the snippet declaration is just a function declaration (`function snipName() {}`) + const snippet_declaration = `function ${raw_snippet_declaration.slice( + start_subtractions.length, + raw_snippet_declaration.length - end_subtractions.length + )} {}`; + + // we offset the index by one because `function` takes the same space as `#snippet` + const snippet_expression = parse_expression_at( + `${parser.template.slice(0, parser.index - 1)}${snippet_declaration}`, + parser.ts, + parser.index - 1 + ); - const elements = []; - while (!parser.match(')')) { - elements.push(read_context(parser)); - parser.eat(','); - parser.allow_whitespace(); + // TODO: handle error + if (snippet_expression.type !== 'FunctionExpression') { + throw new Error(); } - parser.allow_whitespace(); - parser.eat(')', true); + // TODO: handle error + if (!snippet_expression.id) { + throw new Error(); + } - parser.allow_whitespace(); - parser.eat('}', true); + parser.eat(raw_snippet_declaration); const block = parser.append( /** @type {Omit} */ @@ -293,15 +306,10 @@ function open(parser) { type: 'SnippetBlock', start, end: -1, - expression: { - type: 'Identifier', - start: name_start, - end: name_end, - name - }, + expression: snippet_expression.id, context: { type: 'ArrayPattern', - elements + elements: snippet_expression.params }, body: create_fragment() }) diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js b/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js index aa1e072e73..5406b001af 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js @@ -1,3 +1,5 @@ +import full_char_code_at from './full_char_code_at.js'; + const SQUARE_BRACKET_OPEN = '['.charCodeAt(0); const SQUARE_BRACKET_CLOSE = ']'.charCodeAt(0); const CURLY_BRACKET_OPEN = '{'.charCodeAt(0); @@ -33,3 +35,26 @@ export function get_bracket_close(open) { return CURLY_BRACKET_CLOSE; } } + +/** + * The function almost known as bracket_fight + * @param {import('..').Parser} parser + * @param {number} open + * @returns {number} + */ +export function find_matching_bracket(parser, open) { + const close = get_bracket_close(open); + let brackets = 1; + let i = parser.index; + while (brackets > 0 && i < parser.template.length) { + const code = full_char_code_at(parser.template, i); + if (code === open) { + brackets++; + } else if (code === close) { + brackets--; + } + i++; + } + // TODO: If not found + return i; +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index bd07a0e8ae..f3a2cbb2af 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -2440,6 +2440,7 @@ export const template_visitors = { }, SnippetBlock(node, context) { // TODO hoist where possible + /** @type {import('estree').Pattern[]} */ const args = [b.id('$$anchor')]; /** @type {import('estree').BlockStatement} */ @@ -2448,18 +2449,38 @@ export const template_visitors = { /** @type {import('estree').Statement[]} */ const declarations = []; - node.context.elements.forEach((element, i) => { - if (!element) return; - const id = element.type === 'Identifier' ? element : b.id(`$$arg${i}`); - args.push(id); + node.context.elements.forEach((argument, i) => { + if (!argument) return; + // If the argument is itself an identifier (`foo`) or is a simple rest identifier (`...foo`) + // we don't have to do anything fancy -- just push it onto the args array and make sure its + // binding expression is a call of itself. + /** @type {import('estree').Identifier | undefined} */ + let identifier; + /** @type {import('estree').Identifier | import('estree').RestElement | string} */ + let arg; + if (argument.type === 'Identifier') { + identifier = argument; + arg = argument; + } else if (argument.type === 'RestElement' && argument.argument.type === 'Identifier') { + identifier = argument.argument; + arg = argument; + } else if (argument.type === 'RestElement') { + arg = b.rest(b.id(`$$arg${i}`)); + } else { + arg = b.id(`$$arg${i}`); + } + args.push(arg); - if (element.type === 'Identifier') { + if (identifier) { const binding = /** @type {import('#compiler').Binding} */ ( - context.state.scope.get(id.name) + context.state.scope.get(identifier.name) ); - binding.expression = b.call(id); + binding.expression = b.call(identifier); } else { - const paths = extract_paths(element); + // If, on the other hand, it isn't an identifier, it's a destructuring expression, which could be + // a rest destructure (eg. `...[foo, bar, { baz }, ...rest]`, which, absurdly, is all valid syntax). + // In this case, we need to follow the destructuring expression to figure out what variables are being extracted. + const paths = extract_paths(argument.type === 'RestElement' ? argument.argument : argument); for (const path of paths) { const name = /** @type {import('estree').Identifier} */ (path.node).name;