diff --git a/.changeset/ten-colts-grab.md b/.changeset/ten-colts-grab.md new file mode 100644 index 000000000000..6e0e20bc84d7 --- /dev/null +++ b/.changeset/ten-colts-grab.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: allow generics on snippets 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 f4c73dcf403a..b1189018306c 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/context.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/context.js @@ -1,7 +1,7 @@ /** @import { Location } from 'locate-character' */ /** @import { Pattern } from 'estree' */ /** @import { Parser } from '../index.js' */ -import { is_bracket_open, is_bracket_close, get_bracket_close } from '../utils/bracket.js'; +import { match_bracket } from '../utils/bracket.js'; import { parse_expression_at } from '../acorn.js'; import { regex_not_newline_characters } from '../../patterns.js'; import * as e from '../../../errors.js'; @@ -33,7 +33,9 @@ export default function read_pattern(parser) { }; } - if (!is_bracket_open(parser.template[i])) { + const char = parser.template[i]; + + if (char !== '{' && char !== '[') { e.expected_pattern(i); } @@ -71,75 +73,6 @@ export default function read_pattern(parser) { } } -/** - * @param {Parser} parser - * @param {number} start - */ -function match_bracket(parser, start) { - const bracket_stack = []; - - let i = start; - - while (i < parser.template.length) { - let char = parser.template[i++]; - - if (char === "'" || char === '"' || char === '`') { - i = match_quote(parser, i, char); - continue; - } - - 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); - } - - if (bracket_stack.length === 0) { - return i; - } - } - } - - e.unexpected_eof(parser.template.length); -} - -/** - * @param {Parser} parser - * @param {number} start - * @param {string} quote - */ -function match_quote(parser, start, quote) { - let is_escaped = false; - let i = start; - - while (i < parser.template.length) { - const char = parser.template[i++]; - - if (is_escaped) { - is_escaped = false; - continue; - } - - if (char === quote) { - return i; - } - - if (char === '\\') { - is_escaped = true; - } - - if (quote === '`' && char === '$' && parser.template[i] === '{') { - i = match_bracket(parser, i); - } - } - - e.unterminated_string_constant(start); -} - /** * @param {Parser} parser * @returns {any} 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 0eb98c27e858..4153463c8361 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -8,9 +8,12 @@ import { parse_expression_at } from '../acorn.js'; import read_pattern from '../read/context.js'; import read_expression, { get_loose_identifier } from '../read/expression.js'; import { create_fragment } from '../utils/create.js'; +import { match_bracket } from '../utils/bracket.js'; const regex_whitespace_with_closing_curly_brace = /^\s*}/; +const pointy_bois = { '<': '>' }; + /** @param {Parser} parser */ export default function tag(parser) { const start = parser.index; @@ -351,6 +354,22 @@ function open(parser) { const params_start = parser.index; + // snippets could have a generic signature, e.g. `#snippet foo(...)` + /** @type {string | undefined} */ + let type_params; + + // if we match a generic opening + if (parser.ts && parser.match('<')) { + const start = parser.index; + const end = match_bracket(parser, start, pointy_bois); + + type_params = parser.template.slice(start + 1, end - 1); + + parser.index = end; + } + + parser.allow_whitespace(); + const matched = parser.eat('(', true, false); if (matched) { @@ -388,6 +407,7 @@ function open(parser) { end: name_end, name }, + typeParams: type_params, parameters: function_expression.params, body: create_fragment(), metadata: { 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 b7c8cb43cd00..8c69a58c9980 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js @@ -1,34 +1,5 @@ -const SQUARE_BRACKET_OPEN = '['; -const SQUARE_BRACKET_CLOSE = ']'; -const CURLY_BRACKET_OPEN = '{'; -const CURLY_BRACKET_CLOSE = '}'; -const PARENTHESES_OPEN = '('; -const PARENTHESES_CLOSE = ')'; - -/** @param {string} char */ -export function is_bracket_open(char) { - return char === SQUARE_BRACKET_OPEN || char === CURLY_BRACKET_OPEN; -} - -/** @param {string} char */ -export function is_bracket_close(char) { - return char === SQUARE_BRACKET_CLOSE || char === CURLY_BRACKET_CLOSE; -} - -/** @param {string} open */ -export function get_bracket_close(open) { - if (open === SQUARE_BRACKET_OPEN) { - return SQUARE_BRACKET_CLOSE; - } - - if (open === CURLY_BRACKET_OPEN) { - return CURLY_BRACKET_CLOSE; - } - - if (open === PARENTHESES_OPEN) { - return PARENTHESES_CLOSE; - } -} +/** @import { Parser } from '../index.js' */ +import * as e from '../../../errors.js'; /** * @param {number} num @@ -121,7 +92,7 @@ function count_leading_backslashes(string, search_start_index) { * @returns {number | undefined} The index of the closing bracket, or undefined if not found. */ export function find_matching_bracket(template, index, open) { - const close = get_bracket_close(open); + const close = default_brackets[open]; let brackets = 1; let i = index; while (brackets > 0 && i < template.length) { @@ -162,3 +133,81 @@ export function find_matching_bracket(template, index, open) { } return undefined; } + +/** @type {Record} */ +const default_brackets = { + '{': '}', + '(': ')', + '[': ']' +}; + +/** + * @param {Parser} parser + * @param {number} start + * @param {Record} brackets + */ +export function match_bracket(parser, start, brackets = default_brackets) { + const close = Object.values(brackets); + const bracket_stack = []; + + let i = start; + + while (i < parser.template.length) { + let char = parser.template[i++]; + + if (char === "'" || char === '"' || char === '`') { + i = match_quote(parser, i, char); + continue; + } + + if (char in brackets) { + bracket_stack.push(char); + } else if (close.includes(char)) { + const popped = /** @type {string} */ (bracket_stack.pop()); + const expected = /** @type {string} */ (brackets[popped]); + + if (char !== expected) { + e.expected_token(i - 1, expected); + } + + if (bracket_stack.length === 0) { + return i; + } + } + } + + e.unexpected_eof(parser.template.length); +} + +/** + * @param {Parser} parser + * @param {number} start + * @param {string} quote + */ +function match_quote(parser, start, quote) { + let is_escaped = false; + let i = start; + + while (i < parser.template.length) { + const char = parser.template[i++]; + + if (is_escaped) { + is_escaped = false; + continue; + } + + if (char === quote) { + return i; + } + + if (char === '\\') { + is_escaped = true; + } + + if (quote === '`' && char === '$' && parser.template[i] === '{') { + i = match_bracket(parser, i); + } + } + + e.unterminated_string_constant(start); +} diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 54fdda92b845..6dec1f2dbe15 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -468,6 +468,7 @@ export namespace AST { type: 'SnippetBlock'; expression: Identifier; parameters: Pattern[]; + typeParams?: string; body: Fragment; /** @internal */ metadata: { diff --git a/packages/svelte/tests/parser-modern/samples/generic-snippets/input.svelte b/packages/svelte/tests/parser-modern/samples/generic-snippets/input.svelte new file mode 100644 index 000000000000..4ee619728d17 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/generic-snippets/input.svelte @@ -0,0 +1,10 @@ + + +{#snippet generic(val: T)} + {val} +{/snippet} + +{#snippet complex_generic">>(val: T)} + {val} +{/snippet} \ No newline at end of file diff --git a/packages/svelte/tests/parser-modern/samples/generic-snippets/output.json b/packages/svelte/tests/parser-modern/samples/generic-snippets/output.json new file mode 100644 index 000000000000..b66ee7288f2e --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/generic-snippets/output.json @@ -0,0 +1,299 @@ +{ + "css": null, + "js": [], + "start": 30, + "end": 192, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "Text", + "start": 28, + "end": 30, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "SnippetBlock", + "start": 30, + "end": 92, + "expression": { + "type": "Identifier", + "start": 40, + "end": 47, + "name": "generic" + }, + "typeParams": "T extends string", + "parameters": [ + { + "type": "Identifier", + "start": 66, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 36 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "name": "val", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 69, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 39 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "typeAnnotation": { + "type": "TSTypeReference", + "start": 71, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 41 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "typeName": { + "type": "Identifier", + "start": 71, + "end": 72, + "loc": { + "start": { + "line": 4, + "column": 41 + }, + "end": { + "line": 4, + "column": 42 + } + }, + "name": "T" + } + } + } + } + ], + "body": { + "type": "Fragment", + "nodes": [ + { + "type": "Text", + "start": 74, + "end": 76, + "raw": "\n\t", + "data": "\n\t" + }, + { + "type": "ExpressionTag", + "start": 76, + "end": 81, + "expression": { + "type": "Identifier", + "start": 77, + "end": 80, + "loc": { + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 5 + } + }, + "name": "val" + } + }, + { + "type": "Text", + "start": 81, + "end": 82, + "raw": "\n", + "data": "\n" + } + ] + } + }, + { + "type": "Text", + "start": 92, + "end": 94, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "SnippetBlock", + "start": 94, + "end": 192, + "expression": { + "type": "Identifier", + "start": 104, + "end": 119, + "name": "complex_generic" + }, + "typeParams": "T extends { bracket: \"<\" } | \"<\" | Set<\"<>\">", + "parameters": [ + { + "type": "Identifier", + "start": 166, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 72 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "name": "val", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 169, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 75 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "typeAnnotation": { + "type": "TSTypeReference", + "start": 171, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 77 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "typeName": { + "type": "Identifier", + "start": 171, + "end": 172, + "loc": { + "start": { + "line": 8, + "column": 77 + }, + "end": { + "line": 8, + "column": 78 + } + }, + "name": "T" + } + } + } + } + ], + "body": { + "type": "Fragment", + "nodes": [ + { + "type": "Text", + "start": 174, + "end": 176, + "raw": "\n\t", + "data": "\n\t" + }, + { + "type": "ExpressionTag", + "start": 176, + "end": 181, + "expression": { + "type": "Identifier", + "start": 177, + "end": 180, + "loc": { + "start": { + "line": 9, + "column": 2 + }, + "end": { + "line": 9, + "column": 5 + } + }, + "name": "val" + } + }, + { + "type": "Text", + "start": 181, + "end": 182, + "raw": "\n", + "data": "\n" + } + ] + } + } + ] + }, + "options": null, + "instance": { + "type": "Script", + "start": 0, + "end": 28, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 19, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 2, + "column": 0 + } + }, + "body": [], + "sourceType": "module" + }, + "attributes": [ + { + "type": "Attribute", + "start": 8, + "end": 17, + "name": "lang", + "value": [ + { + "start": 14, + "end": 16, + "type": "Text", + "raw": "ts", + "data": "ts" + } + ] + } + ] + } +} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index ff9764b88b1a..bb958c510807 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1301,6 +1301,7 @@ declare module 'svelte/compiler' { type: 'SnippetBlock'; expression: Identifier; parameters: Pattern[]; + typeParams?: string; body: Fragment; }