Initial {#switch ...} support

pull/17010/head
Tim Londschien 10 months ago
parent 3009a0208f
commit 2c877e0bd4

@ -9,6 +9,7 @@ import read_pattern from '../read/context.js';
import read_expression, { get_loose_identifier } from '../read/expression.js'; import read_expression, { get_loose_identifier } from '../read/expression.js';
import { create_fragment } from '../utils/create.js'; import { create_fragment } from '../utils/create.js';
import { match_bracket } from '../utils/bracket.js'; import { match_bracket } from '../utils/bracket.js';
import { regex_whitespaces_strict } from '../../patterns.js';
const regex_whitespace_with_closing_curly_brace = /^\s*}/; const regex_whitespace_with_closing_curly_brace = /^\s*}/;
@ -78,6 +79,39 @@ function open(parser) {
return; 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')) { if (parser.eat('each')) {
parser.require_whitespace(); parser.require_whitespace();
@ -493,6 +527,32 @@ function next(parser) {
return; 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 (block.type === 'EachBlock') {
if (!parser.eat('else')) e.expected_token(start, '{:else}'); if (!parser.eat('else')) e.expected_token(start, '{:else}');
@ -584,6 +644,31 @@ function close(parser) {
parser.pop(); parser.pop();
return; 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': case 'EachBlock':
matched = parser.eat('each', true, false); matched = parser.eat('each', true, false);
break; break;

@ -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 {Direction} direction
* @param {boolean} adjacent_only * @param {boolean} adjacent_only
* @param {Set<Compiler.AST.SnippetBlock>} seen * @param {Set<Compiler.AST.SnippetBlock>} seen
@ -960,6 +960,10 @@ function get_possible_nested_siblings(node, direction, adjacent_only, seen = new
fragments.push(node.consequent, node.alternate); fragments.push(node.consequent, node.alternate);
break; break;
case 'SwitchBlock':
fragments.push(...node.consequences);
break;
case 'AwaitBlock': case 'AwaitBlock':
fragments.push(node.pending, node.then, node.catch); fragments.push(node.pending, node.then, node.catch);
break; break;
@ -1087,11 +1091,12 @@ function loop_child(children, direction, adjacent_only, seen) {
/** /**
* @param {Compiler.AST.SvelteNode} node * @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) { function is_block(node) {
return ( return (
node.type === 'IfBlock' || node.type === 'IfBlock' ||
node.type === 'SwitchBlock' ||
node.type === 'EachBlock' || node.type === 'EachBlock' ||
node.type === 'AwaitBlock' || node.type === 'AwaitBlock' ||
node.type === 'KeyBlock' || node.type === 'KeyBlock' ||

@ -43,6 +43,7 @@ import { FunctionExpression } from './visitors/FunctionExpression.js';
import { HtmlTag } from './visitors/HtmlTag.js'; import { HtmlTag } from './visitors/HtmlTag.js';
import { Identifier } from './visitors/Identifier.js'; import { Identifier } from './visitors/Identifier.js';
import { IfBlock } from './visitors/IfBlock.js'; import { IfBlock } from './visitors/IfBlock.js';
import { SwitchBlock } from './visitors/SwitchBlock.js';
import { ImportDeclaration } from './visitors/ImportDeclaration.js'; import { ImportDeclaration } from './visitors/ImportDeclaration.js';
import { KeyBlock } from './visitors/KeyBlock.js'; import { KeyBlock } from './visitors/KeyBlock.js';
import { LabeledStatement } from './visitors/LabeledStatement.js'; import { LabeledStatement } from './visitors/LabeledStatement.js';
@ -163,6 +164,7 @@ const visitors = {
HtmlTag, HtmlTag,
Identifier, Identifier,
IfBlock, IfBlock,
SwitchBlock,
ImportDeclaration, ImportDeclaration,
KeyBlock, KeyBlock,
LabeledStatement, LabeledStatement,
@ -733,6 +735,7 @@ export function analyze_component(root, source, options) {
const type = path[j].type; const type = path[j].type;
if ( if (
type === 'IfBlock' || type === 'IfBlock' ||
type === 'SwitchBlock' ||
type === 'EachBlock' || type === 'EachBlock' ||
type === 'AwaitBlock' || type === 'AwaitBlock' ||
type === 'KeyBlock' type === 'KeyBlock'

@ -18,6 +18,7 @@ export function ConstTag(node, context) {
if ( if (
parent?.type !== 'Fragment' || parent?.type !== 'Fragment' ||
(grand_parent?.type !== 'IfBlock' && (grand_parent?.type !== 'IfBlock' &&
grand_parent?.type !== 'SwitchBlock' &&
grand_parent?.type !== 'SvelteFragment' && grand_parent?.type !== 'SvelteFragment' &&
grand_parent?.type !== 'Component' && grand_parent?.type !== 'Component' &&
grand_parent?.type !== 'SvelteComponent' && grand_parent?.type !== 'SvelteComponent' &&

@ -123,6 +123,7 @@ export function RegularElement(node, context) {
if ( if (
ancestor.type === 'IfBlock' || ancestor.type === 'IfBlock' ||
ancestor.type === 'SwitchBlock' ||
ancestor.type === 'EachBlock' || ancestor.type === 'EachBlock' ||
ancestor.type === 'AwaitBlock' || ancestor.type === 'AwaitBlock' ||
ancestor.type === 'KeyBlock' ancestor.type === 'KeyBlock'

@ -13,6 +13,7 @@ export function SvelteSelf(node, context) {
const valid = context.path.some( const valid = context.path.some(
(node) => (node) =>
node.type === 'IfBlock' || node.type === 'IfBlock' ||
node.type === 'SwitchBlock' ||
node.type === 'EachBlock' || node.type === 'EachBlock' ||
node.type === 'Component' || node.type === 'Component' ||
node.type === 'SnippetBlock' node.type === 'SnippetBlock'

@ -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();
}

@ -33,6 +33,7 @@ import { FunctionExpression } from './visitors/FunctionExpression.js';
import { HtmlTag } from './visitors/HtmlTag.js'; import { HtmlTag } from './visitors/HtmlTag.js';
import { Identifier } from './visitors/Identifier.js'; import { Identifier } from './visitors/Identifier.js';
import { IfBlock } from './visitors/IfBlock.js'; import { IfBlock } from './visitors/IfBlock.js';
import { SwitchBlock } from './visitors/SwitchBlock.js';
import { ImportDeclaration } from './visitors/ImportDeclaration.js'; import { ImportDeclaration } from './visitors/ImportDeclaration.js';
import { KeyBlock } from './visitors/KeyBlock.js'; import { KeyBlock } from './visitors/KeyBlock.js';
import { LabeledStatement } from './visitors/LabeledStatement.js'; import { LabeledStatement } from './visitors/LabeledStatement.js';
@ -111,6 +112,7 @@ const visitors = {
HtmlTag, HtmlTag,
Identifier, Identifier,
IfBlock, IfBlock,
SwitchBlock,
ImportDeclaration, ImportDeclaration,
KeyBlock, KeyBlock,
LabeledStatement, LabeledStatement,

@ -31,6 +31,7 @@ export function BindDirective(node, context) {
context.path.some( context.path.some(
({ type }) => ({ type }) =>
type === 'IfBlock' || type === 'IfBlock' ||
type === 'SwitchBlock' ||
type === 'EachBlock' || type === 'EachBlock' ||
type === 'AwaitBlock' || type === 'AwaitBlock' ||
type === 'KeyBlock' type === 'KeyBlock'

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

@ -22,6 +22,7 @@ import { Fragment } from './visitors/Fragment.js';
import { HtmlTag } from './visitors/HtmlTag.js'; import { HtmlTag } from './visitors/HtmlTag.js';
import { Identifier } from './visitors/Identifier.js'; import { Identifier } from './visitors/Identifier.js';
import { IfBlock } from './visitors/IfBlock.js'; import { IfBlock } from './visitors/IfBlock.js';
import { SwitchBlock } from './visitors/SwitchBlock.js';
import { KeyBlock } from './visitors/KeyBlock.js'; import { KeyBlock } from './visitors/KeyBlock.js';
import { LabeledStatement } from './visitors/LabeledStatement.js'; import { LabeledStatement } from './visitors/LabeledStatement.js';
import { MemberExpression } from './visitors/MemberExpression.js'; import { MemberExpression } from './visitors/MemberExpression.js';
@ -68,6 +69,7 @@ const template_visitors = {
Fragment, Fragment,
HtmlTag, HtmlTag,
IfBlock, IfBlock,
SwitchBlock,
KeyBlock, KeyBlock,
RegularElement, RegularElement,
RenderTag, RenderTag,

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

@ -407,6 +407,7 @@ function check_nodes_for_namespace(nodes, namespace) {
if ( if (
node.type === 'EachBlock' || node.type === 'EachBlock' ||
node.type === 'IfBlock' || node.type === 'IfBlock' ||
node.type === 'SwitchBlock' ||
node.type === 'AwaitBlock' || node.type === 'AwaitBlock' ||
node.type === 'Fragment' || node.type === 'Fragment' ||
node.type === 'KeyBlock' || node.type === 'KeyBlock' ||

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

@ -610,6 +610,32 @@ function if_builder(test, consequent, alternate) {
return { type: 'IfStatement', test, consequent, alternate }; return { type: 'IfStatement', test, consequent, alternate };
} }
/**
* @param {ESTree.Expression} discriminant
* @param {ESTree.SwitchCase[]} cases
* @returns {ESTree.SwitchStatement}
*/
function switch_statement_builder(discriminant, cases) {
return { type: 'SwitchStatement', discriminant, cases };
}
/**
* @param {ESTree.Identifier | null} label
* @returns {ESTree.BreakStatement}
*/
function break_builder(label = null) {
return { type: 'BreakStatement', label };
}
/**
* @param {ESTree.Expression | null} test
* @param {ESTree.Statement[]} consequent
* @returns {ESTree.SwitchCase}
*/
function switch_case_builder(test, consequent) {
return { type: 'SwitchCase', test, consequent };
}
/** /**
* @param {string} as * @param {string} as
* @param {string} source * @param {string} source
@ -672,6 +698,9 @@ export {
function_builder as function, function_builder as function,
return_builder as return, return_builder as return,
if_builder as if, if_builder as if,
switch_statement_builder as switch_statement,
switch_case_builder as switch_case,
break_builder as break,
this_instance as this, this_instance as this,
null_instance as null, null_instance as null,
debugger_builder as debugger debugger_builder as debugger

@ -0,0 +1,66 @@
/** @import { Effect, TemplateNode } from '#client' */
import {
hydrate_next,
hydrating,
read_hydration_instruction,
set_hydrate_node,
set_hydrating,
skip_nodes
} from '../hydration.js';
import { block } from '../../reactivity/effects.js';
import { HYDRATION_START } from '../../../../constants.js';
import { BranchManager } from './branches.js';
/**
* @param {TemplateNode} node
* @param {(branch: (fn: (anchor: Node) => void, index: number) => void) => void} fn
* @returns {void}
*/
export function switch_block(node, fn) {
if (hydrating) {
hydrate_next();
}
var branches = new BranchManager(node);
/**
* @param {number} index,
* @param {null | ((anchor: Node) => void)} fn
*/
function update_branch(index, fn) {
if (hydrating) {
const hydration_tag = read_hydration_instruction(node);
const hydration_index = Number(hydration_tag.slice(HYDRATION_START.length));
if (index !== hydration_index) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
// This could happen with `{#switch browser}...{/switch}`, for example
var anchor = skip_nodes();
set_hydrate_node(anchor);
branches.anchor = anchor;
set_hydrating(false);
branches.ensure(index, fn);
set_hydrating(true);
return;
}
}
branches.ensure(index, fn);
}
block(() => {
var has_branch = false;
fn((fn, index) => {
has_branch = true;
update_branch(index, fn);
});
if (!has_branch) {
update_branch(-1, null);
}
});
}

@ -95,7 +95,7 @@ export function skip_nodes(remove = true) {
if (data === HYDRATION_END) { if (data === HYDRATION_END) {
if (depth === 0) return node; if (depth === 0) return node;
depth -= 1; depth -= 1;
} else if (data === HYDRATION_START || data === HYDRATION_START_ELSE) { } else if (data[0] === HYDRATION_START) {
depth += 1; depth += 1;
} }
} }

@ -13,6 +13,7 @@ export { async } from './dom/blocks/async.js';
export { validate_snippet_args } from './dev/validation.js'; export { validate_snippet_args } from './dev/validation.js';
export { await_block as await } from './dom/blocks/await.js'; export { await_block as await } from './dom/blocks/await.js';
export { if_block as if } from './dom/blocks/if.js'; export { if_block as if } from './dom/blocks/if.js';
export { switch_block as switch } from './dom/blocks/switch.js';
export { key } from './dom/blocks/key.js'; export { key } from './dom/blocks/key.js';
export { css_props } from './dom/blocks/css-props.js'; export { css_props } from './dom/blocks/css-props.js';
export { index, each } from './dom/blocks/each.js'; export { index, each } from './dom/blocks/each.js';

@ -0,0 +1,93 @@
{
"css": null,
"js": [],
"start": 0,
"end": 46,
"type": "Root",
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "SwitchBlock",
"start": 0,
"end": 46,
"value": {
"type": "Identifier",
"start": 9,
"end": 12,
"loc": {
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 12
}
},
"name": "foo"
},
"consequences": [
{
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 24,
"end": 26,
"raw": "\n\t",
"data": "\n\t"
},
{
"type": "RegularElement",
"start": 26,
"end": 36,
"name": "p",
"attributes": [],
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 29,
"end": 32,
"raw": "foo",
"data": "foo"
}
]
}
},
{
"type": "Text",
"start": 36,
"end": 37,
"raw": "\n",
"data": "\n"
}
]
}
],
"values": [
{
"type": "Literal",
"start": 18,
"end": 23,
"loc": {
"start": {
"line": 1,
"column": 18
},
"end": {
"line": 1,
"column": 23
}
},
"value": "bar",
"raw": "\"bar\""
}
]
}
]
},
"options": null
}

@ -0,0 +1,49 @@
{
"css": null,
"js": [],
"start": 0,
"end": 25,
"type": "Root",
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "SwitchBlock",
"start": 0,
"end": 25,
"value": {
"type": "Identifier",
"start": 9,
"end": 12,
"loc": {
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 12
}
},
"name": "foo"
},
"consequences": [
{
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 13,
"end": 16,
"raw": "bar",
"data": "bar"
}
]
}
],
"values": [null]
}
]
},
"options": null
}

@ -0,0 +1,7 @@
{#switch foo}
{:case "foo"}
{#switch bar}
{:case "bar"}
<p>bar</p>
{/switch}
{/switch}

@ -0,0 +1,154 @@
{
"css": null,
"js": [],
"start": 0,
"end": 91,
"type": "Root",
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "SwitchBlock",
"start": 0,
"end": 91,
"value": {
"type": "Identifier",
"start": 9,
"end": 12,
"loc": {
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 12
}
},
"name": "foo"
},
"consequences": [
{
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 27,
"end": 29,
"raw": "\n\t",
"data": "\n\t"
},
{
"type": "SwitchBlock",
"start": 29,
"end": 81,
"value": {
"type": "Identifier",
"start": 38,
"end": 41,
"loc": {
"start": {
"line": 3,
"column": 10
},
"end": {
"line": 3,
"column": 13
}
},
"name": "bar"
},
"consequences": [
{
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 57,
"end": 60,
"raw": "\n\t\t",
"data": "\n\t\t"
},
{
"type": "RegularElement",
"start": 60,
"end": 70,
"name": "p",
"attributes": [],
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 63,
"end": 66,
"raw": "bar",
"data": "bar"
}
]
}
},
{
"type": "Text",
"start": 70,
"end": 72,
"raw": "\n\t",
"data": "\n\t"
}
]
}
],
"values": [
{
"type": "Literal",
"start": 51,
"end": 56,
"loc": {
"start": {
"line": 4,
"column": 8
},
"end": {
"line": 4,
"column": 13
}
},
"value": "bar",
"raw": "\"bar\""
}
]
},
{
"type": "Text",
"start": 81,
"end": 82,
"raw": "\n",
"data": "\n"
}
]
}
],
"values": [
{
"type": "Literal",
"start": 21,
"end": 26,
"loc": {
"start": {
"line": 2,
"column": 7
},
"end": {
"line": 2,
"column": 12
}
},
"value": "foo",
"raw": "\"foo\""
}
]
}
]
},
"options": null
}

@ -0,0 +1,6 @@
{#switch foo}
{:case "bar"}
<p>foo</p>
{:case "baz"}
<p>baz</p>
{/switch}

@ -0,0 +1,148 @@
{
"css": null,
"js": [],
"start": 0,
"end": 75,
"type": "Root",
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "SwitchBlock",
"start": 0,
"end": 75,
"value": {
"type": "Identifier",
"start": 9,
"end": 12,
"loc": {
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 12
}
},
"name": "foo"
},
"consequences": [
{
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 27,
"end": 29,
"raw": "\n\t",
"data": "\n\t"
},
{
"type": "RegularElement",
"start": 29,
"end": 39,
"name": "p",
"attributes": [],
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 32,
"end": 35,
"raw": "foo",
"data": "foo"
}
]
}
},
{
"type": "Text",
"start": 39,
"end": 40,
"raw": "\n",
"data": "\n"
}
]
},
{
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 53,
"end": 55,
"raw": "\n\t",
"data": "\n\t"
},
{
"type": "RegularElement",
"start": 55,
"end": 65,
"name": "p",
"attributes": [],
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 58,
"end": 61,
"raw": "baz",
"data": "baz"
}
]
}
},
{
"type": "Text",
"start": 65,
"end": 66,
"raw": "\n",
"data": "\n"
}
]
}
],
"values": [
{
"type": "Literal",
"start": 21,
"end": 26,
"loc": {
"start": {
"line": 2,
"column": 7
},
"end": {
"line": 2,
"column": 12
}
},
"value": "bar",
"raw": "\"bar\""
},
{
"type": "Literal",
"start": 47,
"end": 52,
"loc": {
"start": {
"line": 4,
"column": 7
},
"end": {
"line": 4,
"column": 12
}
},
"value": "baz",
"raw": "\"baz\""
}
]
}
]
},
"options": null
}

@ -0,0 +1,19 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await Promise.resolve();
let [btn1] = target.querySelectorAll('button');
flushSync(() => {
btn1?.click();
});
assert.htmlEqual(
target.innerHTML,
`false\ntrue\n<button>Toggle</button>\nfirst:\nfalse\n<br>\nsecond:\ntrue`
);
}
});

@ -0,0 +1,22 @@
<script>
let first = $state(true)
let second = $state(false)
let derivedSecond = $derived(second)
queueMicrotask(() => {
first = false
});
</script>
{first} {second}
<button onclick={() => {
second = true
}}>Toggle</button>
{#switch first || derivedSecond}
{:case true}
first: {first}
<br />
second: {derivedSecond}
{/switch}

@ -0,0 +1,9 @@
<script>
let { post } = $props();
</script>
<svelte:head>
<title>{post.title}</title>
</svelte:head>
<p>{post.title}</p>

@ -0,0 +1,22 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, window }) {
const [btn1] = target.querySelectorAll('button');
assert.htmlEqual(window.document.head.innerHTML, ``);
flushSync(() => {
btn1.click();
});
assert.htmlEqual(window.document.head.innerHTML, `<title>hello world</title>`);
flushSync(() => {
btn1.click();
});
assert.htmlEqual(window.document.head.innerHTML, `<title>hello world</title>`);
}
});

@ -0,0 +1,17 @@
<script>
import Seo from './Seo.svelte';
let post = $state(null);
function toggle() {
post = post ? null : { title: 'hello world' };
}
</script>
<button onclick={toggle}>
toggle
</button>
{#switch !!post case true}
<Seo {post} />
{/switch}

@ -0,0 +1,16 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>Click</button><p>expires in 1 click</p>`,
async test({ assert, target }) {
const [btn1] = target.querySelectorAll('button');
flushSync(() => {
btn1.click();
});
assert.htmlEqual(target.innerHTML, ``);
}
});

@ -0,0 +1,13 @@
<script>
let data = $state({ num: 1 });
function expire() {
data.num = data.num - 1;
if (data.num <= 0) data = undefined;
}
</script>
{#switch data?.num case 1}
<button onclick={expire}>Click</button>
<p>expires in {data.num} click</p>
{/switch}

@ -0,0 +1,16 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>hide</button><div>hello</div>`,
async test({ assert, target }) {
const [btn1, btn2] = target.querySelectorAll('button');
flushSync(() => {
btn1.click();
});
assert.htmlEqual(target.innerHTML, `<button>hide</button><div style="opacity: 0;">hello</div>`);
}
});

@ -0,0 +1,16 @@
<script>
import { fade } from "svelte/transition";
let state = $state("hello");
</script>
<button onclick={() => state = ''}>hide</button>
{#switch !!state case true}
<div in:fade={{ duration: 2000 }} out:fade={{ duration: 2000 }}>
{#if true}
{state}
{/if}
</div>
{/switch}

@ -0,0 +1,40 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>show</button><button>animate</button>`,
async test({ assert, target }) {
const [btn1, btn2] = target.querySelectorAll('button');
flushSync(() => {
btn1.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>show</button><button>animate</button><h1>Hello\n!</h1>`
);
flushSync(() => {
btn1.click();
});
assert.htmlEqual(target.innerHTML, `<button>show</button><button>animate</button>`);
flushSync(() => {
btn2.click();
});
assert.htmlEqual(target.innerHTML, `<button>show</button><button>animate</button>`);
flushSync(() => {
btn1.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>show</button><button>animate</button><h1 style="opacity: 0;">Hello\n!</h1>`
);
}
});

@ -0,0 +1,16 @@
<script>
import { fade } from 'svelte/transition';
let show = $state(false);
let animate = $state(false);
function maybe(node, animate) {
if (animate) return fade(node);
}
</script>
<button onclick={() => show = !show}>show</button><button onclick={() => animate = !animate}>animate</button>
{#switch show case true}
<h1 transition:maybe={animate}>Hello {name}!</h1>
{/switch}

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
props: {
foo: true
}
});

@ -0,0 +1,7 @@
<script>
export let foo;
</script>
{#switch foo case true}
<p>foo is true</p>
{/switch}

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
props: {
foo: false
}
});

@ -0,0 +1,7 @@
<script>
export let foo;
</script>
{#switch foo case true}
<p>foo is false</p>
{/switch}

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
props: {
foo: false
}
});

@ -0,0 +1,7 @@
<script>
export let foo;
</script>
{#switch foo}
<p>default</p>
{/switch}

@ -1453,6 +1453,14 @@ declare module 'svelte/compiler' {
alternate: Fragment | null; alternate: Fragment | null;
} }
/** A `{#switch ...}` block */
export interface SwitchBlock extends BaseNode {
type: 'SwitchBlock';
value: Expression;
consequences: Array<Fragment>;
values: Array<Expression | null>;
}
/** An `{#await ...}` block */ /** An `{#await ...}` block */
export interface AwaitBlock extends BaseNode { export interface AwaitBlock extends BaseNode {
type: 'AwaitBlock'; type: 'AwaitBlock';
@ -1528,6 +1536,7 @@ declare module 'svelte/compiler' {
export type Block = export type Block =
| AST.EachBlock | AST.EachBlock
| AST.IfBlock | AST.IfBlock
| AST.SwitchBlock
| AST.AwaitBlock | AST.AwaitBlock
| AST.KeyBlock | AST.KeyBlock
| AST.SnippetBlock; | AST.SnippetBlock;

Loading…
Cancel
Save