Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ten-colts-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

feat: allow generics on snippets
75 changes: 4 additions & 71 deletions packages/svelte/src/compiler/phases/1-parse/read/context.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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}
Expand Down
20 changes: 20 additions & 0 deletions packages/svelte/src/compiler/phases/1-parse/state/tag.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 generic;

// if we match a generic opening
if (parser.ts && parser.match('<')) {
const start = parser.index;
const end = match_bracket(parser, start, pointy_bois);

generic = parser.template.slice(start + 1, end - 1);

parser.index = end;
}

parser.allow_whitespace();

const matched = parser.eat('(', true, false);

if (matched) {
Expand Down Expand Up @@ -388,6 +407,7 @@ function open(parser) {
end: name_end,
name
},
generic,
parameters: function_expression.params,
body: create_fragment(),
metadata: {
Expand Down
113 changes: 81 additions & 32 deletions packages/svelte/src/compiler/phases/1-parse/utils/bracket.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script lang="ts">
</script>

{#snippet generic<T extends string>(val: T)}
{val}
{/snippet}
Loading