feat: Better bracket matching, unit tests

pull/10320/head
S. Elliott Johnson 6 months ago
parent 79536e4875
commit 1dc662aa4d

@ -267,11 +267,17 @@ function open(parser) {
if (parser.match('snippet')) {
const snippet_declaraion_end = find_matching_bracket(
parser,
full_char_code_at(parser.template, start)
parser.template,
parser.index,
parser.template[start]
);
if (!snippet_declaraion_end) {
error(parser.current(), 'TODO', 'Expected a closing curly bracket');
}
// we'll eat this later, so save it
const raw_snippet_declaration = parser.template.slice(start, snippet_declaraion_end);
const raw_snippet_declaration = parser.template.slice(start, snippet_declaraion_end + 1);
const start_subtractions = '{#snippet ';
const end_subtractions = '}';
@ -288,12 +294,10 @@ function open(parser) {
parser.index - 1
);
// TODO: handle error
if (snippet_expression.type !== 'FunctionExpression') {
error(snippet_expression, 'TODO', 'expected a function expression');
}
// TODO: handle error
if (!snippet_expression.id) {
error(snippet_expression, 'TODO', 'expected a snippet name');
}

@ -37,24 +37,128 @@ export function get_bracket_close(open) {
}
/**
* The function almost known as bracket_fight
* @param {import('..').Parser} parser
* @param {number} open
* @param {number} number
* @returns {number}
*/
function infinity_if_negative(number) {
if (number < 0) {
return Infinity;
}
return number;
}
/**
* @param {string} string The string to search.
* @param {number} search_start_index The index to start searching at.
* @param {"'" | '"' | '`'} string_start_char The character that started this string.
* @returns {number}
*/
export function find_matching_bracket(parser, open) {
const close = get_bracket_close(open);
function find_string_end(string, search_start_index, string_start_char) {
let string_to_search;
if (string_start_char === '`') {
string_to_search = string;
} else {
// we could slice at the search start index, but this way the index remains valid
string_to_search = string.slice(
0,
infinity_if_negative(string.indexOf('\n', search_start_index))
);
}
return find_unescaped_char(string_to_search, search_start_index, string_start_char);
}
/**
* @param {string} string The string to search.
* @param {number} search_start_index The index to start searching at.
* @returns {number} Infinity if not found, else the index of the character.
*/
function find_regex_end(string, search_start_index) {
return find_unescaped_char(string, search_start_index, '/');
}
/**
*
* @param {string} string The string to search.
* @param {number} search_start_index The index to begin the search at.
* @param {string} char The character to search for.
* @returns
*/
function find_unescaped_char(string, search_start_index, char) {
let i = search_start_index;
while (true) {
const found_index = string.indexOf(char, i);
if (found_index === -1) {
return Infinity;
}
if (count_leading_backslashes(string, found_index - 1) % 2 === 0) {
return found_index;
}
i = found_index + 1;
}
}
/**
* @param {string} string
* @param {number} search_start_index
*/
function count_leading_backslashes(string, search_start_index) {
let i = search_start_index;
let count = 0;
while (string[i] === '\\') {
count++;
i--;
}
return count;
}
/**
* The function almost known as bracket_fight
* @param {string} template
* @param {number} index
* @param {string} open
* @returns {number | undefined}
*/
export function find_matching_bracket(template, index, open) {
const open_code = full_char_code_at(open, 0);
const close_code = get_bracket_close(open_code);
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--;
let i = index;
while (brackets > 0 && i < template.length) {
const char = template[i];
switch (char) {
case "'":
case '"':
case '`':
i = find_string_end(template, i + 1, char) + 1;
continue;
case '/': {
const next_char = template[i + 1];
if (!next_char) continue;
if (next_char === '/') {
i = infinity_if_negative(template.indexOf('\n', i + 1)) + '\n'.length;
continue;
}
if (next_char === '*') {
i = infinity_if_negative(template.indexOf('*/', i + 1)) + '*/'.length;
continue;
}
i = find_regex_end(template, i + 1) + '/'.length;
continue;
}
default: {
const code = full_char_code_at(template, i);
if (code === open_code) {
brackets++;
} else if (code === close_code) {
brackets--;
}
if (brackets === 0) {
return i;
}
i++;
}
}
i++;
}
// TODO: If not found
return i;
return undefined;
}

@ -0,0 +1,111 @@
import { describe, expect, it } from 'vitest';
import { find_matching_bracket, get_bracket_close } from './bracket';
import full_char_code_at from './full_char_code_at';
describe('find_matching_bracket', () => {
it.each([
{
name: 'no-gotchas-const',
template: `{@const foo = bar}`,
start: '{',
index: 1,
expected: 17
},
{
name: 'no-gotchas-snippet',
template: `{#snippet foo(bar, baz)}`,
start: '{',
index: 1,
expected: 23
},
{
name: 'multiple-bracket-pairs-snippet',
template: `{#snippet foo(bar, { baz })}`,
start: '{',
index: 1,
expected: 27
},
{
name: 'unbalanced-brackets-snippet',
template: `{#snippet foo(bar, { baz })`,
start: '{',
index: 1,
expected: undefined
},
{
name: 'singlequote-string-snippet',
template: `{#snippet foo(bar = '}')}`,
start: '{',
index: 1,
expected: 24
},
{
name: 'doublequote-string-snippet',
template: `{#snippet foo(bar = "}")}`,
start: '{',
index: 1,
expected: 24
},
{
name: 'backquote-string-snippet',
template: '{#snippet foo(bar = `}`)}',
start: '{',
index: 1,
expected: 24
},
{
name: 'multiline-backquote-string-snippet',
template: `{#snippet foo(bar = \`}
\`)}`,
start: '{',
index: 1,
expected: 28
},
{
name: 'escaped-string-delimiter-snippet',
template: `{#snippet foo(bar = '\\'')}`,
start: '{',
index: 1,
expected: 25
},
{
name: 'regex-snippet',
template: `{#snippet foo(bar = /foobar'"/)}`,
start: '{',
index: 1,
expected: 31
},
{
name: 'inline-multiline-comment-snippet',
template: `{#snippet foo(bar /*inline multiline comment*/)}`,
start: '{',
index: 1,
expected: 47
},
{
name: 'linebreak-multiline-comment-snippet',
template: `{#snippet foo(bar /*inline multiline comment
*/)
}`,
start: '{',
index: 1,
expected: 55
},
{
name: 'confusing-singleline-comment-snippet',
template: `{#snippet foo(bar) // this comment removes the bracket inside of it }
}`,
start: '{',
index: 1,
expected: 72
}
])('finds the matching bracket ($name)', ({ template, index, start, expected }) => {
const matched_bracket_location = find_matching_bracket(template, index, start);
expect(matched_bracket_location).toBe(expected);
if (matched_bracket_location) {
expect(get_bracket_close(full_char_code_at(start, 0))).toBe(
full_char_code_at(template[matched_bracket_location], 0)
);
}
});
});

@ -37,6 +37,7 @@ export default defineConfig({
dir: '.',
reporters: ['dot'],
include: [
'packages/svelte/**/*.test.ts',
'packages/svelte/tests/*/test.ts',
'packages/svelte/tests/runtime-browser/test-ssr.ts'
],

Loading…
Cancel
Save