Skip to content

Commit 66a2155

Browse files
feat: allow generics on snippets (#15915)
* feat: allow generics on snippets * chore: fix lint * reuse bracket matching logic * remove some unused stuff * chore: update name, test and types * chore: fix lint --------- Co-authored-by: Rich Harris <[email protected]>
1 parent 51b858d commit 66a2155

File tree

8 files changed

+421
-103
lines changed

8 files changed

+421
-103
lines changed

.changeset/ten-colts-grab.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': minor
3+
---
4+
5+
feat: allow generics on snippets

packages/svelte/src/compiler/phases/1-parse/read/context.js

Lines changed: 4 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** @import { Location } from 'locate-character' */
22
/** @import { Pattern } from 'estree' */
33
/** @import { Parser } from '../index.js' */
4-
import { is_bracket_open, is_bracket_close, get_bracket_close } from '../utils/bracket.js';
4+
import { match_bracket } from '../utils/bracket.js';
55
import { parse_expression_at } from '../acorn.js';
66
import { regex_not_newline_characters } from '../../patterns.js';
77
import * as e from '../../../errors.js';
@@ -33,7 +33,9 @@ export default function read_pattern(parser) {
3333
};
3434
}
3535

36-
if (!is_bracket_open(parser.template[i])) {
36+
const char = parser.template[i];
37+
38+
if (char !== '{' && char !== '[') {
3739
e.expected_pattern(i);
3840
}
3941

@@ -71,75 +73,6 @@ export default function read_pattern(parser) {
7173
}
7274
}
7375

74-
/**
75-
* @param {Parser} parser
76-
* @param {number} start
77-
*/
78-
function match_bracket(parser, start) {
79-
const bracket_stack = [];
80-
81-
let i = start;
82-
83-
while (i < parser.template.length) {
84-
let char = parser.template[i++];
85-
86-
if (char === "'" || char === '"' || char === '`') {
87-
i = match_quote(parser, i, char);
88-
continue;
89-
}
90-
91-
if (is_bracket_open(char)) {
92-
bracket_stack.push(char);
93-
} else if (is_bracket_close(char)) {
94-
const popped = /** @type {string} */ (bracket_stack.pop());
95-
const expected = /** @type {string} */ (get_bracket_close(popped));
96-
97-
if (char !== expected) {
98-
e.expected_token(i - 1, expected);
99-
}
100-
101-
if (bracket_stack.length === 0) {
102-
return i;
103-
}
104-
}
105-
}
106-
107-
e.unexpected_eof(parser.template.length);
108-
}
109-
110-
/**
111-
* @param {Parser} parser
112-
* @param {number} start
113-
* @param {string} quote
114-
*/
115-
function match_quote(parser, start, quote) {
116-
let is_escaped = false;
117-
let i = start;
118-
119-
while (i < parser.template.length) {
120-
const char = parser.template[i++];
121-
122-
if (is_escaped) {
123-
is_escaped = false;
124-
continue;
125-
}
126-
127-
if (char === quote) {
128-
return i;
129-
}
130-
131-
if (char === '\\') {
132-
is_escaped = true;
133-
}
134-
135-
if (quote === '`' && char === '$' && parser.template[i] === '{') {
136-
i = match_bracket(parser, i);
137-
}
138-
}
139-
140-
e.unterminated_string_constant(start);
141-
}
142-
14376
/**
14477
* @param {Parser} parser
14578
* @returns {any}

packages/svelte/src/compiler/phases/1-parse/state/tag.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ import { parse_expression_at } from '../acorn.js';
88
import read_pattern from '../read/context.js';
99
import read_expression, { get_loose_identifier } from '../read/expression.js';
1010
import { create_fragment } from '../utils/create.js';
11+
import { match_bracket } from '../utils/bracket.js';
1112

1213
const regex_whitespace_with_closing_curly_brace = /^\s*}/;
1314

15+
const pointy_bois = { '<': '>' };
16+
1417
/** @param {Parser} parser */
1518
export default function tag(parser) {
1619
const start = parser.index;
@@ -351,6 +354,22 @@ function open(parser) {
351354

352355
const params_start = parser.index;
353356

357+
// snippets could have a generic signature, e.g. `#snippet foo<T>(...)`
358+
/** @type {string | undefined} */
359+
let type_params;
360+
361+
// if we match a generic opening
362+
if (parser.ts && parser.match('<')) {
363+
const start = parser.index;
364+
const end = match_bracket(parser, start, pointy_bois);
365+
366+
type_params = parser.template.slice(start + 1, end - 1);
367+
368+
parser.index = end;
369+
}
370+
371+
parser.allow_whitespace();
372+
354373
const matched = parser.eat('(', true, false);
355374

356375
if (matched) {
@@ -388,6 +407,7 @@ function open(parser) {
388407
end: name_end,
389408
name
390409
},
410+
typeParams: type_params,
391411
parameters: function_expression.params,
392412
body: create_fragment(),
393413
metadata: {

packages/svelte/src/compiler/phases/1-parse/utils/bracket.js

Lines changed: 81 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,5 @@
1-
const SQUARE_BRACKET_OPEN = '[';
2-
const SQUARE_BRACKET_CLOSE = ']';
3-
const CURLY_BRACKET_OPEN = '{';
4-
const CURLY_BRACKET_CLOSE = '}';
5-
const PARENTHESES_OPEN = '(';
6-
const PARENTHESES_CLOSE = ')';
7-
8-
/** @param {string} char */
9-
export function is_bracket_open(char) {
10-
return char === SQUARE_BRACKET_OPEN || char === CURLY_BRACKET_OPEN;
11-
}
12-
13-
/** @param {string} char */
14-
export function is_bracket_close(char) {
15-
return char === SQUARE_BRACKET_CLOSE || char === CURLY_BRACKET_CLOSE;
16-
}
17-
18-
/** @param {string} open */
19-
export function get_bracket_close(open) {
20-
if (open === SQUARE_BRACKET_OPEN) {
21-
return SQUARE_BRACKET_CLOSE;
22-
}
23-
24-
if (open === CURLY_BRACKET_OPEN) {
25-
return CURLY_BRACKET_CLOSE;
26-
}
27-
28-
if (open === PARENTHESES_OPEN) {
29-
return PARENTHESES_CLOSE;
30-
}
31-
}
1+
/** @import { Parser } from '../index.js' */
2+
import * as e from '../../../errors.js';
323

334
/**
345
* @param {number} num
@@ -121,7 +92,7 @@ function count_leading_backslashes(string, search_start_index) {
12192
* @returns {number | undefined} The index of the closing bracket, or undefined if not found.
12293
*/
12394
export function find_matching_bracket(template, index, open) {
124-
const close = get_bracket_close(open);
95+
const close = default_brackets[open];
12596
let brackets = 1;
12697
let i = index;
12798
while (brackets > 0 && i < template.length) {
@@ -162,3 +133,81 @@ export function find_matching_bracket(template, index, open) {
162133
}
163134
return undefined;
164135
}
136+
137+
/** @type {Record<string, string>} */
138+
const default_brackets = {
139+
'{': '}',
140+
'(': ')',
141+
'[': ']'
142+
};
143+
144+
/**
145+
* @param {Parser} parser
146+
* @param {number} start
147+
* @param {Record<string, string>} brackets
148+
*/
149+
export function match_bracket(parser, start, brackets = default_brackets) {
150+
const close = Object.values(brackets);
151+
const bracket_stack = [];
152+
153+
let i = start;
154+
155+
while (i < parser.template.length) {
156+
let char = parser.template[i++];
157+
158+
if (char === "'" || char === '"' || char === '`') {
159+
i = match_quote(parser, i, char);
160+
continue;
161+
}
162+
163+
if (char in brackets) {
164+
bracket_stack.push(char);
165+
} else if (close.includes(char)) {
166+
const popped = /** @type {string} */ (bracket_stack.pop());
167+
const expected = /** @type {string} */ (brackets[popped]);
168+
169+
if (char !== expected) {
170+
e.expected_token(i - 1, expected);
171+
}
172+
173+
if (bracket_stack.length === 0) {
174+
return i;
175+
}
176+
}
177+
}
178+
179+
e.unexpected_eof(parser.template.length);
180+
}
181+
182+
/**
183+
* @param {Parser} parser
184+
* @param {number} start
185+
* @param {string} quote
186+
*/
187+
function match_quote(parser, start, quote) {
188+
let is_escaped = false;
189+
let i = start;
190+
191+
while (i < parser.template.length) {
192+
const char = parser.template[i++];
193+
194+
if (is_escaped) {
195+
is_escaped = false;
196+
continue;
197+
}
198+
199+
if (char === quote) {
200+
return i;
201+
}
202+
203+
if (char === '\\') {
204+
is_escaped = true;
205+
}
206+
207+
if (quote === '`' && char === '$' && parser.template[i] === '{') {
208+
i = match_bracket(parser, i);
209+
}
210+
}
211+
212+
e.unterminated_string_constant(start);
213+
}

packages/svelte/src/compiler/types/template.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,7 @@ export namespace AST {
468468
type: 'SnippetBlock';
469469
expression: Identifier;
470470
parameters: Pattern[];
471+
typeParams?: string;
471472
body: Fragment;
472473
/** @internal */
473474
metadata: {
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script lang="ts">
2+
</script>
3+
4+
{#snippet generic<T extends string>(val: T)}
5+
{val}
6+
{/snippet}
7+
8+
{#snippet complex_generic<T extends { bracket: "<" } | "<" | Set<"<>">>(val: T)}
9+
{val}
10+
{/snippet}

0 commit comments

Comments
 (0)