Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
85 changes: 85 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 @@ -9,6 +9,7 @@ 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';
import { regex_whitespaces_strict } from '../../patterns.js';

const regex_whitespace_with_closing_curly_brace = /^\s*}/;

Expand Down Expand Up @@ -78,6 +79,39 @@ function open(parser) {
return;
}

if (parser.eat('switch')) {
parser.require_whitespace();

/** @type {AST.SwitchBlock} */
const block = parser.append({
type: 'SwitchBlock',
start,
end: -1,
value: read_expression(parser),
consequences: [create_fragment()],
values: [null]
});

parser.allow_whitespace();

if (parser.eat('case')) {
if (parser.match_regex(regex_whitespace_with_closing_curly_brace)) {
parser.allow_whitespace();
} else {
parser.require_whitespace();
block.values[0] = read_expression(parser);
parser.allow_whitespace();
}
}

parser.eat('}', true);

parser.stack.push(block);
parser.fragments.push(block.consequences[0]);

return;
}

if (parser.eat('each')) {
parser.require_whitespace();

Expand Down Expand Up @@ -493,6 +527,32 @@ function next(parser) {
return;
}

if (block.type === 'SwitchBlock') {
if (parser.eat('case')) {
parser.require_whitespace();

const value = read_expression(parser);

parser.allow_whitespace();
parser.eat('}', true);

let case_start = start - 1;
while (parser.template[case_start] !== '{') case_start -= 1;

const consequent = create_fragment();

block.consequences.push(consequent);
block.values.push(value);

parser.fragments.pop();
parser.fragments.push(consequent);
} else {
e.expected_token(parser.index - 1, '{:case}');
}

return;
}

if (block.type === 'EachBlock') {
if (!parser.eat('else')) e.expected_token(start, '{:else}');

Expand Down Expand Up @@ -584,6 +644,31 @@ function close(parser) {
parser.pop();
return;

case 'SwitchBlock':
matched = parser.eat('switch', true, false);

if (block.values[0] === null) {
const child_nodes = block.consequences[0].nodes;

if (
child_nodes.length === 0 ||
(child_nodes.length === 1 &&
child_nodes[0].type === 'Text' &&
child_nodes[0].data.replace(regex_whitespaces_strict, ' ').trim() === '')
) {
// in this situation we have an empty default case, we detect that and remove it
// {#switch show}
// {:case true}
block.consequences.shift();
block.values.shift();
} else {
// move default to end
block.values.push(/** @type {Expression | null} */ (block.values.shift()));
block.consequences.push(/** @type {AST.Fragment} */ (block.consequences.shift()));
}
}
break;

case 'EachBlock':
matched = parser.eat('each', true, false);
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -941,7 +941,7 @@ function get_possible_element_siblings(node, direction, adjacent_only, seen = ne
}

/**
* @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement | Compiler.AST.SnippetBlock | Compiler.AST.Component} node
* @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.SwitchBlock| Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement | Compiler.AST.SnippetBlock | Compiler.AST.Component} node
* @param {Direction} direction
* @param {boolean} adjacent_only
* @param {Set<Compiler.AST.SnippetBlock>} seen
Expand All @@ -960,6 +960,10 @@ function get_possible_nested_siblings(node, direction, adjacent_only, seen = new
fragments.push(node.consequent, node.alternate);
break;

case 'SwitchBlock':
fragments.push(...node.consequences);
break;

case 'AwaitBlock':
fragments.push(node.pending, node.then, node.catch);
break;
Expand Down Expand Up @@ -1087,11 +1091,12 @@ function loop_child(children, direction, adjacent_only, seen) {

/**
* @param {Compiler.AST.SvelteNode} node
* @returns {node is Compiler.AST.IfBlock | Compiler.AST.EachBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement}
* @returns {node is Compiler.AST.IfBlock | Compiler.AST.SwitchBlock | Compiler.AST.EachBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement}
*/
function is_block(node) {
return (
node.type === 'IfBlock' ||
node.type === 'SwitchBlock' ||
node.type === 'EachBlock' ||
node.type === 'AwaitBlock' ||
node.type === 'KeyBlock' ||
Expand Down
3 changes: 3 additions & 0 deletions packages/svelte/src/compiler/phases/2-analyze/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { FunctionExpression } from './visitors/FunctionExpression.js';
import { HtmlTag } from './visitors/HtmlTag.js';
import { Identifier } from './visitors/Identifier.js';
import { IfBlock } from './visitors/IfBlock.js';
import { SwitchBlock } from './visitors/SwitchBlock.js';
import { ImportDeclaration } from './visitors/ImportDeclaration.js';
import { KeyBlock } from './visitors/KeyBlock.js';
import { LabeledStatement } from './visitors/LabeledStatement.js';
Expand Down Expand Up @@ -163,6 +164,7 @@ const visitors = {
HtmlTag,
Identifier,
IfBlock,
SwitchBlock,
ImportDeclaration,
KeyBlock,
LabeledStatement,
Expand Down Expand Up @@ -733,6 +735,7 @@ export function analyze_component(root, source, options) {
const type = path[j].type;
if (
type === 'IfBlock' ||
type === 'SwitchBlock' ||
type === 'EachBlock' ||
type === 'AwaitBlock' ||
type === 'KeyBlock'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function ConstTag(node, context) {
if (
parent?.type !== 'Fragment' ||
(grand_parent?.type !== 'IfBlock' &&
grand_parent?.type !== 'SwitchBlock' &&
grand_parent?.type !== 'SvelteFragment' &&
grand_parent?.type !== 'Component' &&
grand_parent?.type !== 'SvelteComponent' &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export function RegularElement(node, context) {

if (
ancestor.type === 'IfBlock' ||
ancestor.type === 'SwitchBlock' ||
ancestor.type === 'EachBlock' ||
ancestor.type === 'AwaitBlock' ||
ancestor.type === 'KeyBlock'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export function SvelteSelf(node, context) {
const valid = context.path.some(
(node) =>
node.type === 'IfBlock' ||
node.type === 'SwitchBlock' ||
node.type === 'EachBlock' ||
node.type === 'Component' ||
node.type === 'SnippetBlock'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { mark_subtree_dynamic } from './shared/fragment.js';
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
import * as e from '../../../errors.js';

/**
* @param {AST.SwitchBlock} node
* @param {Context} context
*/
export function SwitchBlock(node, context) {
mark_subtree_dynamic(context.path);
node.consequences.forEach((consequence) => validate_block_not_empty(consequence, context));

if (context.state.analysis.runes) {
validate_opening_tag(node, context.state, '#');

for (const value of node.values) {
if (value === null) continue;

const start = /** @type {number} */ (value.start);
const match = context.state.analysis.source
.substring(start - 10, start)
.match(/{(\s*):case\s+$/);

if (match && match[1] !== '') {
// { :case ...} -- space after "{" not allowed
e.block_unexpected_character({ start: start - 10, end: start }, ':');
}
}
}

context.next();
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { FunctionExpression } from './visitors/FunctionExpression.js';
import { HtmlTag } from './visitors/HtmlTag.js';
import { Identifier } from './visitors/Identifier.js';
import { IfBlock } from './visitors/IfBlock.js';
import { SwitchBlock } from './visitors/SwitchBlock.js';
import { ImportDeclaration } from './visitors/ImportDeclaration.js';
import { KeyBlock } from './visitors/KeyBlock.js';
import { LabeledStatement } from './visitors/LabeledStatement.js';
Expand Down Expand Up @@ -111,6 +112,7 @@ const visitors = {
HtmlTag,
Identifier,
IfBlock,
SwitchBlock,
ImportDeclaration,
KeyBlock,
LabeledStatement,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function BindDirective(node, context) {
context.path.some(
({ type }) =>
type === 'IfBlock' ||
type === 'SwitchBlock' ||
type === 'EachBlock' ||
type === 'AwaitBlock' ||
type === 'KeyBlock'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/** @import { BlockStatement, Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';

/**
* @param {AST.SwitchBlock} node
* @param {ComponentContext} context
*/
export function SwitchBlock(node, context) {
context.state.template.push_comment();
const statements = [];

const value = /** @type {Expression} */ (context.visit(node.value));

/** @type {Expression[]} */
const args = [
context.state.node,
b.arrow(
[b.id('$$render')],
b.block([
b.switch_statement(
value,
node.consequences.map((node_consequent, index) => {
const consequent = /** @type {BlockStatement} */ (context.visit(node_consequent));
const consequent_id = context.state.scope.generate('consequent');
statements.push(b.var(b.id(consequent_id), b.arrow([b.id('$$anchor')], consequent)));

return b.switch_case(node.values[index], [
b.stmt(b.call(b.id('$$render'), b.id(consequent_id), b.literal(index))),
b.break()
]);
})
)
])
)
];

statements.push(b.stmt(b.call('$.switch', ...args)));

context.state.init.push(b.block(statements));
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { Fragment } from './visitors/Fragment.js';
import { HtmlTag } from './visitors/HtmlTag.js';
import { Identifier } from './visitors/Identifier.js';
import { IfBlock } from './visitors/IfBlock.js';
import { SwitchBlock } from './visitors/SwitchBlock.js';
import { KeyBlock } from './visitors/KeyBlock.js';
import { LabeledStatement } from './visitors/LabeledStatement.js';
import { MemberExpression } from './visitors/MemberExpression.js';
Expand Down Expand Up @@ -68,6 +69,7 @@ const template_visitors = {
Fragment,
HtmlTag,
IfBlock,
SwitchBlock,
KeyBlock,
RegularElement,
RenderTag,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/** @import { BlockStatement, Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { HYDRATION_START } from '../../../../../constants.js';
import * as b from '../../../../utils/builders.js';
import { block_close } from './shared/utils.js';

/**
* @param {AST.SwitchBlock} node
* @param {ComponentContext} context
*/
export function SwitchBlock(node, context) {
const discriminant = /** @type {Expression} */ (context.visit(node.value));

const cases = node.consequences.map((node_consequent, index) => {
const consequent = /** @type {BlockStatement} */ (context.visit(node_consequent));
consequent.body.unshift(
b.stmt(b.call(b.id('$$renderer.push'), b.literal(`<!--${HYDRATION_START}${index}-->`)))
);
consequent.body.push(b.break());

return b.switch_case(node.values[index], consequent.body);
});

// if there is no default block, we still have to create a hydration open marker
if (node.values.at(-1) !== null) {
const default_consequent = b.block([]);
default_consequent.body.unshift(
b.stmt(
b.call(b.id('$$renderer.push'), b.literal(`<!--${HYDRATION_START}${node.values.length}-->`))
)
);
cases.push(b.switch_case(null, default_consequent.body));
}

context.state.template.push(b.switch_statement(discriminant, cases), block_close);
}
1 change: 1 addition & 0 deletions packages/svelte/src/compiler/phases/3-transform/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ function check_nodes_for_namespace(nodes, namespace) {
if (
node.type === 'EachBlock' ||
node.type === 'IfBlock' ||
node.type === 'SwitchBlock' ||
node.type === 'AwaitBlock' ||
node.type === 'Fragment' ||
node.type === 'KeyBlock' ||
Expand Down
9 changes: 9 additions & 0 deletions packages/svelte/src/compiler/types/template.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,14 @@ export namespace AST {
};
}

/** A `{#switch ...}` block */
export interface SwitchBlock extends BaseNode {
type: 'SwitchBlock';
value: Expression;
consequences: Array<Fragment>;
values: Array<Expression | null>;
}

/** An `{#await ...}` block */
export interface AwaitBlock extends BaseNode {
type: 'AwaitBlock';
Expand Down Expand Up @@ -579,6 +587,7 @@ export namespace AST {
export type Block =
| AST.EachBlock
| AST.IfBlock
| AST.SwitchBlock
| AST.AwaitBlock
| AST.KeyBlock
| AST.SnippetBlock;
Expand Down
Loading
Loading