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 <rich.harris@vercel.com>
pull/15916/head
Paolo Ricciuti 4 months ago committed by GitHub
parent 51b858dfd1
commit 66a21552f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: allow generics on snippets

@ -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}

@ -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<T>(...)`
/** @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: {

@ -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<string, string>} */
const default_brackets = {
'{': '}',
'(': ')',
'[': ']'
};
/**
* @param {Parser} parser
* @param {number} start
* @param {Record<string, string>} 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);
}

@ -468,6 +468,7 @@ export namespace AST {
type: 'SnippetBlock';
expression: Identifier;
parameters: Pattern[];
typeParams?: string;
body: Fragment;
/** @internal */
metadata: {

@ -0,0 +1,10 @@
<script lang="ts">
</script>
{#snippet generic<T extends string>(val: T)}
{val}
{/snippet}
{#snippet complex_generic<T extends { bracket: "<" } | "<" | Set<"<>">>(val: T)}
{val}
{/snippet}

@ -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"
}
]
}
]
}
}

@ -1301,6 +1301,7 @@ declare module 'svelte/compiler' {
type: 'SnippetBlock';
expression: Identifier;
parameters: Pattern[];
typeParams?: string;
body: Fragment;
}

Loading…
Cancel
Save