First steps for adding switch-case syntax

pull/7655/head
Mathias Picker 4 years ago
parent 31e5f8b5de
commit 2cc56af2c6

@ -0,0 +1,32 @@
import AbstractBlock from './shared/AbstractBlock';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import ConstTag from './ConstTag';
import get_const_tags from './shared/get_const_tags';
import Expression from './shared/Expression';
import SwitchBlock from './SwitchBlock';
export default class CaseBlock extends AbstractBlock {
type: 'CaseBlock';
is_default: boolean;
test?: Expression;
scope: TemplateScope;
const_tags: ConstTag[];
constructor(component: Component, parent: SwitchBlock, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.scope = scope.child();
this.test = info.test;
this.is_default = info.isdefault ?? false;
if (!this.is_default) {
this.test = new Expression(component, this, this.scope, info.test);
}
([this.const_tags, this.children] = get_const_tags(info.children, component, this, this));
this.warn_if_empty_block();
}
}

@ -0,0 +1,27 @@
import AbstractBlock from './shared/AbstractBlock';
import Component from '../Component';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import Node from './shared/Node';
import CaseBlock from './CaseBlock';
import Expression from './shared/Expression';
export default class SwitchBlock extends AbstractBlock {
type: 'SwitchBlock';
cases: CaseBlock[];
discriminant: Expression;
scope: TemplateScope;
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);
this.scope = scope.child();
this.discriminant = new Expression(component, this, this.scope, info.disccriminant);
this.cases = info.cases?.length
? info.cases.map(c => new CaseBlock(component, this, scope, c))
: [];
this.warn_if_empty_block();
}
}

@ -6,6 +6,7 @@ import Attribute from './Attribute';
import AwaitBlock from './AwaitBlock';
import Binding from './Binding';
import Body from './Body';
import CaseBlock from './CaseBlock';
import CatchBlock from './CatchBlock';
import Class from './Class';
import StyleDirective from './StyleDirective';
@ -28,6 +29,7 @@ import PendingBlock from './PendingBlock';
import RawMustacheTag from './RawMustacheTag';
import Slot from './Slot';
import SlotTemplate from './SlotTemplate';
import SwitchBlock from './SwitchBlock';
import Text from './Text';
import ThenBlock from './ThenBlock';
import Title from './Title';
@ -43,6 +45,7 @@ export type INode = Action
| Binding
| Body
| CatchBlock
| CaseBlock
| Class
| Comment
| ConstTag
@ -64,6 +67,7 @@ export type INode = Action
| Slot
| SlotTemplate
| StyleDirective
| SwitchBlock
| Tag
| Text
| ThenBlock
@ -73,9 +77,11 @@ export type INode = Action
export type INodeAllowConstTag =
| IfBlock
| CaseBlock
| ElseBlock
| EachBlock
| CatchBlock
| ThenBlock
| InlineComponent
| SlotTemplate;
| SlotTemplate
| SwitchBlock;

@ -188,6 +188,7 @@ export default class Expression {
template_scope,
owner
} = this;
let scope = this.scope;
let function_expression;

@ -19,6 +19,8 @@ import Title from '../Title';
import Window from '../Window';
import { TemplateNode } from '../../../interfaces';
import { push_array } from '../../../utils/push_array';
import SwitchBlock from '../SwitchBlock';
import CaseBlock from '../CaseBlock';
export type Children = ReturnType<typeof map_children>;
@ -27,6 +29,7 @@ function get_constructor(type) {
case 'AwaitBlock': return AwaitBlock;
case 'Body': return Body;
case 'Comment': return Comment;
case 'CaseBlock': return CaseBlock;
case 'ConstTag': return ConstTag;
case 'EachBlock': return EachBlock;
case 'Element': return Element;
@ -40,6 +43,7 @@ function get_constructor(type) {
case 'DebugTag': return DebugTag;
case 'Slot': return Slot;
case 'SlotTemplate': return SlotTemplate;
case 'SwitchBlock': return SwitchBlock;
case 'Text': return Text;
case 'Title': return Title;
case 'Window': return Window;

@ -279,6 +279,10 @@ export default class Renderer {
return node;
}
convert_to_binary_expression(left: Node, operator: string, ctx: string | void = '#ctx') {
}
remove_block(block: Block | Node | Node[]) {
this.blocks.splice(this.blocks.indexOf(block), 1);
}

@ -6,6 +6,7 @@ import EachBlock from './EachBlock';
import Element from './Element/index';
import Head from './Head';
import IfBlock from './IfBlock';
import SwitchBlock from './SwitchBlock';
import KeyBlock from './KeyBlock';
import InlineComponent from './InlineComponent/index';
import MustacheTag from './MustacheTag';
@ -32,6 +33,7 @@ const wrappers = {
Head,
IfBlock,
InlineComponent,
SwitchBlock,
KeyBlock,
MustacheTag,
Options: null,

@ -1,17 +1,12 @@
import Wrapper from './shared/Wrapper';
import Renderer from '../Renderer';
import Block from '../Block';
import EachBlock from '../../nodes/EachBlock';
import IfBlock from '../../nodes/IfBlock';
import create_debugging_comment from './shared/create_debugging_comment';
import ElseBlock from '../../nodes/ElseBlock';
import FragmentWrapper from './Fragment';
import { b, x } from 'code-red';
import { walk } from 'estree-walker';
import { is_head } from './shared/is_head';
import { Identifier, Node } from 'estree';
import { Identifier } from 'estree';
import { push_array } from '../../../utils/push_array';
import { add_const_tags, add_const_tags_context } from './shared/add_const_tags';
import ConditionalBlockBranch from './shared/ConditionalBlockBranch';
import ConditionalBlockWrapper from './shared/ConditionalBlockWrapper';
function is_else_if(node: ElseBlock) {
return (
@ -19,76 +14,9 @@ function is_else_if(node: ElseBlock) {
);
}
class IfBlockBranch extends Wrapper {
block: Block;
fragment: FragmentWrapper;
dependencies?: string[];
condition?: any;
snippet?: Node;
is_dynamic: boolean;
node: IfBlock | ElseBlock;
var = null;
get_ctx_name: Node | undefined;
constructor(
renderer: Renderer,
block: Block,
parent: IfBlockWrapper,
node: IfBlock | ElseBlock,
strip_whitespace: boolean,
next_sibling: Wrapper
) {
super(renderer, block, parent, node);
const { expression } = (node as IfBlock);
const is_else = !expression;
if (expression) {
this.dependencies = expression.dynamic_dependencies();
// TODO is this the right rule? or should any non-reference count?
// const should_cache = !is_reference(expression.node, null) && dependencies.length > 0;
let should_cache = false;
walk(expression.node, {
enter(node) {
if (node.type === 'CallExpression' || node.type === 'NewExpression') {
should_cache = true;
}
}
});
if (should_cache) {
this.condition = block.get_unique_name('show_if');
this.snippet = (expression.manipulate(block) as Node);
} else {
this.condition = expression.manipulate(block);
}
}
add_const_tags_context(renderer, this.node.const_tags);
this.block = block.child({
comment: create_debugging_comment(node, parent.renderer.component),
name: parent.renderer.component.get_unique_name(
is_else ? 'create_else_block' : 'create_if_block'
),
type: (node as IfBlock).expression ? 'if' : 'else'
});
this.fragment = new FragmentWrapper(renderer, this.block, node.children, parent, strip_whitespace, next_sibling);
this.is_dynamic = this.block.dependencies.size > 0;
if (node.const_tags.length > 0) {
this.get_ctx_name = parent.renderer.component.get_unique_name(is_else ? 'get_else_ctx' : 'get_if_ctx');
}
}
}
export default class IfBlockWrapper extends Wrapper {
export default class IfBlockWrapper extends ConditionalBlockWrapper {
node: IfBlock;
branches: IfBlockBranch[];
branches: ConditionalBlockBranch[];
needs_update = false;
var: Identifier = { type: 'Identifier', name: 'if_block' };
@ -97,24 +25,19 @@ export default class IfBlockWrapper extends Wrapper {
renderer: Renderer,
block: Block,
parent: Wrapper,
node: EachBlock,
node: IfBlock,
strip_whitespace: boolean,
next_sibling: Wrapper
) {
super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
this.branches = [];
const blocks: Block[] = [];
let is_dynamic = false;
let has_intros = false;
let has_outros = false;
const create_branches = (node: IfBlock) => {
const branch = new IfBlockBranch(
const branch = new ConditionalBlockBranch(
renderer,
block,
this,
@ -145,7 +68,7 @@ export default class IfBlockWrapper extends Wrapper {
if (is_else_if(node.else)) {
create_branches(node.else.children[0] as IfBlock);
} else if (node.else) {
const branch = new IfBlockBranch(
const branch = new ConditionalBlockBranch(
renderer,
block,
this,
@ -178,502 +101,4 @@ export default class IfBlockWrapper extends Wrapper {
push_array(renderer.blocks, blocks);
}
render(
block: Block,
parent_node: Identifier,
parent_nodes: Identifier
) {
const name = this.var;
const needs_anchor = this.next ? !this.next.is_dom_node() : !parent_node || !this.parent.is_dom_node();
const anchor = needs_anchor
? block.get_unique_name(`${this.var.name}_anchor`)
: (this.next && this.next.var) || 'null';
const has_else = !(this.branches[this.branches.length - 1].condition);
const if_exists_condition = has_else ? null : name;
const dynamic = this.branches[0].block.has_update_method; // can use [0] as proxy for all, since they necessarily have the same value
const has_intros = this.branches[0].block.has_intro_method;
const has_outros = this.branches[0].block.has_outro_method;
const has_transitions = has_intros || has_outros;
this.branches.forEach(branch => {
if (branch.get_ctx_name) {
this.renderer.blocks.push(b`
function ${branch.get_ctx_name}(#ctx) {
const child_ctx = #ctx.slice();
${add_const_tags(block, branch.node.const_tags, 'child_ctx')}
return child_ctx;
}
`);
}
});
const vars = { name, anchor, if_exists_condition, has_else, has_transitions };
const detaching = parent_node && !is_head(parent_node) ? null : 'detaching';
if (this.node.else) {
this.branches.forEach(branch => {
if (branch.snippet) block.add_variable(branch.condition);
});
if (has_outros) {
this.render_compound_with_outros(block, parent_node, parent_nodes, dynamic, vars, detaching);
block.chunks.outro.push(b`@transition_out(${name});`);
} else {
this.render_compound(block, parent_node, parent_nodes, dynamic, vars, detaching);
}
} else {
this.render_simple(block, parent_node, parent_nodes, dynamic, vars, detaching);
if (has_outros) {
block.chunks.outro.push(b`@transition_out(${name});`);
}
}
if (if_exists_condition) {
block.chunks.create.push(b`if (${if_exists_condition}) ${name}.c();`);
} else {
block.chunks.create.push(b`${name}.c();`);
}
if (parent_nodes && this.renderer.options.hydratable) {
if (if_exists_condition) {
block.chunks.claim.push(
b`if (${if_exists_condition}) ${name}.l(${parent_nodes});`
);
} else {
block.chunks.claim.push(
b`${name}.l(${parent_nodes});`
);
}
}
if (has_intros || has_outros) {
block.chunks.intro.push(b`@transition_in(${name});`);
}
if (needs_anchor) {
block.add_element(
anchor as Identifier,
x`@empty()`,
parent_nodes && x`@empty()`,
parent_node
);
}
this.branches.forEach(branch => {
branch.fragment.render(branch.block, null, x`#nodes` as unknown as Identifier);
});
}
render_compound(
block: Block,
parent_node: Identifier,
_parent_nodes: Identifier,
dynamic,
{ name, anchor, has_else, if_exists_condition, has_transitions },
detaching
) {
const select_block_type = this.renderer.component.get_unique_name('select_block_type');
const current_block_type = block.get_unique_name('current_block_type');
const need_select_block_ctx = this.branches.some(branch => branch.get_ctx_name);
const select_block_ctx = need_select_block_ctx ? block.get_unique_name('select_block_ctx') : null;
const if_ctx = select_block_ctx ? x`${select_block_ctx}(#ctx, ${current_block_type})` : x`#ctx`;
const get_block = has_else
? x`${current_block_type}(${if_ctx})`
: x`${current_block_type} && ${current_block_type}(${if_ctx})`;
if (this.needs_update) {
block.chunks.init.push(b`
function ${select_block_type}(#ctx, #dirty) {
${this.branches.map(({ dependencies, condition, snippet }) => {
return b`${snippet && dependencies.length > 0 ? b`if (${block.renderer.dirty(dependencies)}) ${condition} = null;` : null}`;
})}
${this.branches.map(({ condition, snippet, block }) => condition
? b`
${snippet && b`if (${condition} == null) ${condition} = !!${snippet}`}
if (${condition}) return ${block.name};`
: b`return ${block.name};`
)}
}
`);
} else {
block.chunks.init.push(b`
function ${select_block_type}(#ctx, #dirty) {
${this.branches.map(({ condition, snippet, block }) => condition
? b`if (${snippet || condition}) return ${block.name};`
: b`return ${block.name};`)}
}
`);
}
if (need_select_block_ctx) {
// if all branches needs create a context
if (this.branches.every(branch => branch.get_ctx_name)) {
block.chunks.init.push(b`
function ${select_block_ctx}(#ctx, #type) {
${this.branches.map(({ condition, get_ctx_name, block }) => {
return condition
? b`if (#type === ${block.name}) return ${get_ctx_name}(#ctx);`
: b`return ${get_ctx_name}(#ctx);`;
}).filter(Boolean)}
}
`);
} else {
// when not all branches need to create a new context,
// this code is simpler
block.chunks.init.push(b`
function ${select_block_ctx}(#ctx, #type) {
${this.branches.map(({ get_ctx_name, block }) => {
return get_ctx_name
? b`if (#type === ${block.name}) return ${get_ctx_name}(#ctx);`
: null;
}).filter(Boolean)}
return #ctx;
}
`);
}
}
block.chunks.init.push(b`
let ${current_block_type} = ${select_block_type}(#ctx, ${this.renderer.get_initial_dirty()});
let ${name} = ${get_block};
`);
const initial_mount_node = parent_node || '#target';
const anchor_node = parent_node ? 'null' : '#anchor';
if (if_exists_condition) {
block.chunks.mount.push(
b`if (${if_exists_condition}) ${name}.m(${initial_mount_node}, ${anchor_node});`
);
} else {
block.chunks.mount.push(
b`${name}.m(${initial_mount_node}, ${anchor_node});`
);
}
if (this.needs_update) {
const update_mount_node = this.get_update_mount_node(anchor);
const change_block = b`
${if_exists_condition ? b`if (${if_exists_condition}) ${name}.d(1)` : b`${name}.d(1)`};
${name} = ${get_block};
if (${name}) {
${name}.c();
${has_transitions && b`@transition_in(${name}, 1);`}
${name}.m(${update_mount_node}, ${anchor});
}
`;
if (dynamic) {
block.chunks.update.push(b`
if (${current_block_type} === (${current_block_type} = ${select_block_type}(#ctx, #dirty)) && ${name}) {
${name}.p(${if_ctx}, #dirty);
} else {
${change_block}
}
`);
} else {
block.chunks.update.push(b`
if (${current_block_type} !== (${current_block_type} = ${select_block_type}(#ctx, #dirty))) {
${change_block}
}
`);
}
} else if (dynamic) {
if (if_exists_condition) {
block.chunks.update.push(b`if (${if_exists_condition}) ${name}.p(${if_ctx}, #dirty);`);
} else {
block.chunks.update.push(b`${name}.p(${if_ctx}, #dirty);`);
}
}
if (if_exists_condition) {
block.chunks.destroy.push(b`
if (${if_exists_condition}) {
${name}.d(${detaching});
}
`);
} else {
block.chunks.destroy.push(b`
${name}.d(${detaching});
`);
}
}
// if any of the siblings have outros, we need to keep references to the blocks
// (TODO does this only apply to bidi transitions?)
render_compound_with_outros(
block: Block,
parent_node: Identifier,
_parent_nodes: Identifier,
dynamic,
{ name, anchor, has_else, has_transitions, if_exists_condition },
detaching
) {
const select_block_type = this.renderer.component.get_unique_name('select_block_type');
const current_block_type_index = block.get_unique_name('current_block_type_index');
const previous_block_index = block.get_unique_name('previous_block_index');
const if_block_creators = block.get_unique_name('if_block_creators');
const if_blocks = block.get_unique_name('if_blocks');
const need_select_block_ctx = this.branches.some(branch => branch.get_ctx_name);
const select_block_ctx = need_select_block_ctx ? block.get_unique_name('select_block_ctx') : null;
const if_ctx = select_block_ctx ? x`${select_block_ctx}(#ctx, ${current_block_type_index})` : x`#ctx`;
const if_current_block_type_index = has_else
? nodes => nodes
: nodes => b`if (~${current_block_type_index}) { ${nodes} }`;
block.add_variable(current_block_type_index);
block.add_variable(name);
block.chunks.init.push(b`
const ${if_block_creators} = [
${this.branches.map(branch => branch.block.name)}
];
const ${if_blocks} = [];
${this.needs_update
? b`
function ${select_block_type}(#ctx, #dirty) {
${this.branches.map(({ dependencies, condition, snippet }) => {
return b`${snippet && dependencies.length > 0 ? b`if (${block.renderer.dirty(dependencies)}) ${condition} = null;` : null}`;
})}
${this.branches.map(({ condition, snippet }, i) => condition
? b`
${snippet && b`if (${condition} == null) ${condition} = !!${snippet}`}
if (${condition}) return ${i};`
: b`return ${i};`)}
${!has_else && b`return -1;`}
}
`
: b`
function ${select_block_type}(#ctx, #dirty) {
${this.branches.map(({ condition, snippet }, i) => condition
? b`if (${snippet || condition}) return ${i};`
: b`return ${i};`)}
${!has_else && b`return -1;`}
}
`}
`);
if (need_select_block_ctx) {
// if all branches needs create a context
if (this.branches.every(branch => branch.get_ctx_name)) {
block.chunks.init.push(b`
function ${select_block_ctx}(#ctx, #index) {
${this.branches.map(({ condition, get_ctx_name }, i) => {
return condition
? b`if (#index === ${i}) return ${get_ctx_name}(#ctx);`
: b`return ${get_ctx_name}(#ctx);`;
}).filter(Boolean)}
}
`);
} else {
// when not all branches need to create a new context,
// this code is simpler
block.chunks.init.push(b`
function ${select_block_ctx}(#ctx, #index) {
${this.branches.map(({ get_ctx_name }, i) => {
return get_ctx_name
? b`if (#index === ${i}) return ${get_ctx_name}(#ctx);`
: null;
}).filter(Boolean)}
return #ctx;
}
`);
}
}
if (has_else) {
block.chunks.init.push(b`
${current_block_type_index} = ${select_block_type}(#ctx, ${this.renderer.get_initial_dirty()});
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](${if_ctx});
`);
} else {
block.chunks.init.push(b`
if (~(${current_block_type_index} = ${select_block_type}(#ctx, ${this.renderer.get_initial_dirty()}))) {
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](${if_ctx});
}
`);
}
const initial_mount_node = parent_node || '#target';
const anchor_node = parent_node ? 'null' : '#anchor';
block.chunks.mount.push(
if_current_block_type_index(
b`${if_blocks}[${current_block_type_index}].m(${initial_mount_node}, ${anchor_node});`
)
);
if (this.needs_update) {
const update_mount_node = this.get_update_mount_node(anchor);
const destroy_old_block = b`
@group_outros();
@transition_out(${if_blocks}[${previous_block_index}], 1, 1, () => {
${if_blocks}[${previous_block_index}] = null;
});
@check_outros();
`;
const create_new_block = b`
${name} = ${if_blocks}[${current_block_type_index}];
if (!${name}) {
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](${if_ctx});
${name}.c();
} else {
${dynamic && b`${name}.p(${if_ctx}, #dirty);`}
}
${has_transitions && b`@transition_in(${name}, 1);`}
${name}.m(${update_mount_node}, ${anchor});
`;
const change_block = has_else
? b`
${destroy_old_block}
${create_new_block}
`
: b`
if (${name}) {
${destroy_old_block}
}
if (~${current_block_type_index}) {
${create_new_block}
} else {
${name} = null;
}
`;
block.chunks.update.push(b`
let ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(#ctx, #dirty);
`);
if (dynamic) {
block.chunks.update.push(b`
if (${current_block_type_index} === ${previous_block_index}) {
${if_current_block_type_index(b`${if_blocks}[${current_block_type_index}].p(${if_ctx}, #dirty);`)}
} else {
${change_block}
}
`);
} else {
block.chunks.update.push(b`
if (${current_block_type_index} !== ${previous_block_index}) {
${change_block}
}
`);
}
} else if (dynamic) {
if (if_exists_condition) {
block.chunks.update.push(b`if (${if_exists_condition}) ${name}.p(${if_ctx}, #dirty);`);
} else {
block.chunks.update.push(b`${name}.p(${if_ctx}, #dirty);`);
}
}
block.chunks.destroy.push(
if_current_block_type_index(b`${if_blocks}[${current_block_type_index}].d(${detaching});`)
);
}
render_simple(
block: Block,
parent_node: Identifier,
_parent_nodes: Identifier,
dynamic,
{ name, anchor, if_exists_condition, has_transitions },
detaching
) {
const branch = this.branches[0];
const if_ctx = branch.get_ctx_name ? x`${branch.get_ctx_name}(#ctx)` : x`#ctx`;
if (branch.snippet) block.add_variable(branch.condition, branch.snippet);
block.chunks.init.push(b`
let ${name} = ${branch.condition} && ${branch.block.name}(${if_ctx});
`);
const initial_mount_node = parent_node || '#target';
const anchor_node = parent_node ? 'null' : '#anchor';
block.chunks.mount.push(
b`if (${name}) ${name}.m(${initial_mount_node}, ${anchor_node});`
);
if (branch.dependencies.length > 0) {
const update_mount_node = this.get_update_mount_node(anchor);
const enter = b`
if (${name}) {
${dynamic && b`${name}.p(${if_ctx}, #dirty);`}
${has_transitions &&
b`if (${block.renderer.dirty(branch.dependencies)}) {
@transition_in(${name}, 1);
}`
}
} else {
${name} = ${branch.block.name}(${if_ctx});
${name}.c();
${has_transitions && b`@transition_in(${name}, 1);`}
${name}.m(${update_mount_node}, ${anchor});
}
`;
if (branch.snippet) {
block.chunks.update.push(b`if (${block.renderer.dirty(branch.dependencies)}) ${branch.condition} = ${branch.snippet}`);
}
// no `p()` here — we don't want to update outroing nodes,
// as that will typically result in glitching
if (branch.block.has_outro_method) {
block.chunks.update.push(b`
if (${branch.condition}) {
${enter}
} else if (${name}) {
@group_outros();
@transition_out(${name}, 1, 1, () => {
${name} = null;
});
@check_outros();
}
`);
} else {
block.chunks.update.push(b`
if (${branch.condition}) {
${enter}
} else if (${name}) {
${name}.d(1);
${name} = null;
}
`);
}
} else if (dynamic) {
block.chunks.update.push(b`
if (${branch.condition}) ${name}.p(${if_ctx}, #dirty);
`);
}
if (if_exists_condition) {
block.chunks.destroy.push(b`
if (${if_exists_condition}) ${name}.d(${detaching});
`);
} else {
block.chunks.destroy.push(b`
${name}.d(${detaching});
`);
}
}
}

@ -0,0 +1,87 @@
import Wrapper from './shared/Wrapper';
import Renderer from '../Renderer';
import Block from '../Block';
import { Identifier } from 'estree';
import { push_array } from '../../../utils/push_array';
import ConditionalBlockBranch from './shared/ConditionalBlockBranch';
import SwitchBlock from '../../nodes/SwitchBlock';
import CaseBlock from '../../nodes/CaseBlock';
import Expression from '../../nodes/shared/Expression';
import ConditionalBlockWrapper from './shared/ConditionalBlockWrapper';
export default class SwitchBlockWrapper extends ConditionalBlockWrapper {
node: SwitchBlock;
branches: ConditionalBlockBranch[];
discriminant: Expression;
needs_update = false;
var: Identifier = { type: 'Identifier', name: 'switch_block' };
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: SwitchBlock,
strip_whitespace: boolean,
next_sibling: Wrapper
) {
super(renderer, block, parent, node);
this.discriminant = node.discriminant;
const blocks: Block[] = [];
let is_dynamic = false;
let has_intros = false;
let has_outros = false;
const create_branches = (node: CaseBlock) => {
const branch = new ConditionalBlockBranch(
renderer,
block,
this,
node,
strip_whitespace,
next_sibling
);
this.branches.push(branch);
blocks.push(branch.block);
if (node.is_default) {
block.add_dependencies(this.node.discriminant.dependencies);
} else block.add_dependencies(node.test.dependencies);
if (branch.block.dependencies.size > 0) {
// the condition, or its contents, is dynamic
is_dynamic = true;
block.add_dependencies(branch.block.dependencies);
}
if (branch.dependencies && branch.dependencies.length > 0) {
// the condition itself is dynamic
this.needs_update = true;
}
if (branch.block.has_intros) has_intros = true;
if (branch.block.has_outros) has_outros = true;
};
this.node.cases.forEach((c) => create_branches(c));
// the default statement is first in the markup
// but logically we want it to be last
if ((this.branches[0].node as CaseBlock).is_default) {
const [default_case] = this.branches.splice(0, 1);
this.branches.push(default_case);
}
blocks.forEach((block) => {
block.has_update_method = is_dynamic;
block.has_intro_method = has_intros;
block.has_outro_method = has_outros;
});
push_array(renderer.blocks, blocks);
}
}

@ -0,0 +1,115 @@
import { walk } from 'estree-walker';
import CaseBlock from '../../../nodes/CaseBlock';
import ElseBlock from '../../../nodes/ElseBlock';
import IfBlock from '../../../nodes/IfBlock';
import Block from '../../Block';
import Renderer from '../../Renderer';
import FragmentWrapper from '../Fragment';
import { add_const_tags_context } from './add_const_tags';
import create_debugging_comment from './create_debugging_comment';
import Wrapper from './Wrapper';
import { Node } from 'estree';
import Expression from '../../../nodes/shared/Expression';
import ConditionalBlockWrapper from './ConditionalBlockWrapper';
function get_expression(node: IfBlock | ElseBlock | CaseBlock): Expression | void {
switch (node.type) {
case 'ElseBlock':
return;
case 'IfBlock':
return node.expression;
case 'CaseBlock':
if (!node.is_default) return node.test;
}
}
export default class ConditionalBlockBranch extends Wrapper {
block: Block;
fragment: FragmentWrapper;
dependencies?: string[];
condition?: any;
snippet?: Node;
is_dynamic: boolean;
node: IfBlock | ElseBlock | CaseBlock;
var = null;
get_ctx_name: Node | undefined;
constructor(
renderer: Renderer,
block: Block,
parent: ConditionalBlockWrapper,
node: IfBlock | ElseBlock | CaseBlock,
strip_whitespace: boolean,
next_sibling: Wrapper
) {
super(renderer, block, parent, node);
const expression = get_expression(node);
const type =
node.type === 'CaseBlock'
? 'case'
: node.type === 'ElseBlock'
? 'else'
: 'if';
if (expression) {
this.dependencies = expression.dynamic_dependencies();
// TODO is this the right rule? or should any non-reference count?
// const should_cache = !is_reference(expression.node, null) && dependencies.length > 0;
let should_cache = false;
walk(expression.node, {
enter(node) {
if (node.type === 'CallExpression' || node.type === 'NewExpression') {
should_cache = true;
}
}
});
if (should_cache) {
this.condition = block.get_unique_name('conditional_render');
this.snippet = expression.manipulate(block) as Node;
} else {
this.condition = expression.manipulate(block);
}
}
add_const_tags_context(renderer, this.node.const_tags);
this.block = block.child({
comment: create_debugging_comment(node, parent.renderer.component),
name: parent.renderer.component.get_unique_name(
type === 'case'
? 'create_case_block'
: type === 'else'
? 'create_else_block'
: 'create_if_block'
),
type
});
this.fragment = new FragmentWrapper(
renderer,
this.block,
node.children,
parent,
strip_whitespace,
next_sibling
);
this.is_dynamic = this.block.dependencies.size > 0;
if (node.const_tags.length > 0) {
this.get_ctx_name = parent.renderer.component.get_unique_name(
type === 'case'
? 'get_case_ctx'
: type === 'else'
? 'get_else_ctx'
: 'get_if_ctx'
);
}
}
}

@ -0,0 +1,530 @@
import { b, x } from 'code-red';
import { is_head } from './is_head';
import { Identifier } from 'estree';
import { add_const_tags } from './add_const_tags';
import ConditionalBlockBranch from './ConditionalBlockBranch';
import SwitchBlock from '../../../nodes/SwitchBlock';
import Wrapper from './Wrapper';
import IfBlock from '../../../nodes/IfBlock';
import Renderer from '../../Renderer';
import Block from '../../Block';
export default class ConditionalBlockWrapper extends Wrapper {
node: IfBlock | SwitchBlock;
branches: ConditionalBlockBranch[];
needs_update = false;
var: Identifier = { type: 'Identifier', name: 'if_block' };
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: IfBlock | SwitchBlock
) {
super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
this.branches = [];
}
render(
block: Block,
parent_node: Identifier,
parent_nodes: Identifier
) {
const name = this.var;
const needs_anchor = this.next ? !this.next.is_dom_node() : !parent_node || !this.parent.is_dom_node();
const anchor = needs_anchor
? block.get_unique_name(`${this.var.name}_anchor`)
: (this.next && this.next.var) || 'null';
const has_else = !(this.branches[this.branches.length - 1].condition);
const if_exists_condition = has_else ? null : name;
const dynamic = this.branches[0].block.has_update_method; // can use [0] as proxy for all, since they necessarily have the same value
const has_intros = this.branches[0].block.has_intro_method;
const has_outros = this.branches[0].block.has_outro_method;
const has_transitions = has_intros || has_outros;
this.branches.forEach(branch => {
if (branch.get_ctx_name) {
this.renderer.blocks.push(b`
function ${branch.get_ctx_name}(#ctx) {
const child_ctx = #ctx.slice();
${add_const_tags(block, branch.node.const_tags, 'child_ctx')}
return child_ctx;
}
`);
}
});
const vars = { name, anchor, if_exists_condition, has_else, has_transitions };
const detaching = parent_node && !is_head(parent_node) ? null : 'detaching';
if (this.node.type === 'IfBlock' && this.node.else) {
this.branches.forEach(branch => {
if (branch.snippet) block.add_variable(branch.condition);
});
if (has_outros) {
this.render_compound_with_outros(block, parent_node, parent_nodes, dynamic, vars, detaching);
block.chunks.outro.push(b`@transition_out(${name});`);
} else {
this.render_compound(block, parent_node, parent_nodes, dynamic, vars, detaching);
}
} else {
this.render_simple(block, parent_node, parent_nodes, dynamic, vars, detaching);
if (has_outros) {
block.chunks.outro.push(b`@transition_out(${name});`);
}
}
if (if_exists_condition) {
block.chunks.create.push(b`if (${if_exists_condition}) ${name}.c();`);
} else {
block.chunks.create.push(b`${name}.c();`);
}
if (parent_nodes && this.renderer.options.hydratable) {
if (if_exists_condition) {
block.chunks.claim.push(
b`if (${if_exists_condition}) ${name}.l(${parent_nodes});`
);
} else {
block.chunks.claim.push(
b`${name}.l(${parent_nodes});`
);
}
}
if (has_intros || has_outros) {
block.chunks.intro.push(b`@transition_in(${name});`);
}
if (needs_anchor) {
block.add_element(
anchor as Identifier,
x`@empty()`,
parent_nodes && x`@empty()`,
parent_node
);
}
this.branches.forEach(branch => {
branch.fragment.render(branch.block, null, x`#nodes` as unknown as Identifier);
});
}
render_compound(
block: Block,
parent_node: Identifier,
_parent_nodes: Identifier,
dynamic,
{ name, anchor, has_else, if_exists_condition, has_transitions },
detaching
) {
const select_block_type = this.renderer.component.get_unique_name('select_block_type');
const current_block_type = block.get_unique_name('current_block_type');
const need_select_block_ctx = this.branches.some(branch => branch.get_ctx_name);
const select_block_ctx = need_select_block_ctx ? block.get_unique_name('select_block_ctx') : null;
const if_ctx = select_block_ctx ? x`${select_block_ctx}(#ctx, ${current_block_type})` : x`#ctx`;
const get_block = has_else
? x`${current_block_type}(${if_ctx})`
: x`${current_block_type} && ${current_block_type}(${if_ctx})`;
if (this.needs_update) {
block.chunks.init.push(b`
function ${select_block_type}(#ctx, #dirty) {
${this.branches.map(({ dependencies, condition, snippet }) => {
return b`${snippet && dependencies.length > 0 ? b`if (${block.renderer.dirty(dependencies)}) ${condition} = null;` : null}`;
})}
${this.branches.map(({ condition, snippet, block }) => condition
? b`
${snippet && b`if (${condition} == null) ${condition} = !!${snippet}`}
if (${condition}) return ${block.name};`
: b`return ${block.name};`
)}
}
`);
} else {
block.chunks.init.push(b`
function ${select_block_type}(#ctx, #dirty) {
${this.branches.map(({ condition, snippet, block }) => condition
? b`if (${snippet || condition}) return ${block.name};`
: b`return ${block.name};`)}
}
`);
}
if (need_select_block_ctx) {
// if all branches needs create a context
if (this.branches.every(branch => branch.get_ctx_name)) {
block.chunks.init.push(b`
function ${select_block_ctx}(#ctx, #type) {
${this.branches.map(({ condition, get_ctx_name, block }) => {
return condition
? b`if (#type === ${block.name}) return ${get_ctx_name}(#ctx);`
: b`return ${get_ctx_name}(#ctx);`;
}).filter(Boolean)}
}
`);
} else {
// when not all branches need to create a new context,
// this code is simpler
block.chunks.init.push(b`
function ${select_block_ctx}(#ctx, #type) {
${this.branches.map(({ get_ctx_name, block }) => {
return get_ctx_name
? b`if (#type === ${block.name}) return ${get_ctx_name}(#ctx);`
: null;
}).filter(Boolean)}
return #ctx;
}
`);
}
}
block.chunks.init.push(b`
let ${current_block_type} = ${select_block_type}(#ctx, ${this.renderer.get_initial_dirty()});
let ${name} = ${get_block};
`);
const initial_mount_node = parent_node || '#target';
const anchor_node = parent_node ? 'null' : '#anchor';
if (if_exists_condition) {
block.chunks.mount.push(
b`if (${if_exists_condition}) ${name}.m(${initial_mount_node}, ${anchor_node});`
);
} else {
block.chunks.mount.push(
b`${name}.m(${initial_mount_node}, ${anchor_node});`
);
}
if (this.needs_update) {
const update_mount_node = this.get_update_mount_node(anchor);
const change_block = b`
${if_exists_condition ? b`if (${if_exists_condition}) ${name}.d(1)` : b`${name}.d(1)`};
${name} = ${get_block};
if (${name}) {
${name}.c();
${has_transitions && b`@transition_in(${name}, 1);`}
${name}.m(${update_mount_node}, ${anchor});
}
`;
if (dynamic) {
block.chunks.update.push(b`
if (${current_block_type} === (${current_block_type} = ${select_block_type}(#ctx, #dirty)) && ${name}) {
${name}.p(${if_ctx}, #dirty);
} else {
${change_block}
}
`);
} else {
block.chunks.update.push(b`
if (${current_block_type} !== (${current_block_type} = ${select_block_type}(#ctx, #dirty))) {
${change_block}
}
`);
}
} else if (dynamic) {
if (if_exists_condition) {
block.chunks.update.push(b`if (${if_exists_condition}) ${name}.p(${if_ctx}, #dirty);`);
} else {
block.chunks.update.push(b`${name}.p(${if_ctx}, #dirty);`);
}
}
if (if_exists_condition) {
block.chunks.destroy.push(b`
if (${if_exists_condition}) {
${name}.d(${detaching});
}
`);
} else {
block.chunks.destroy.push(b`
${name}.d(${detaching});
`);
}
}
// if any of the siblings have outros, we need to keep references to the blocks
// (TODO does this only apply to bidi transitions?)
render_compound_with_outros(
block: Block,
parent_node: Identifier,
_parent_nodes: Identifier,
dynamic,
{ name, anchor, has_else, has_transitions, if_exists_condition },
detaching
) {
const select_block_type = this.renderer.component.get_unique_name('select_block_type');
const current_block_type_index = block.get_unique_name('current_block_type_index');
const previous_block_index = block.get_unique_name('previous_block_index');
const if_block_creators = block.get_unique_name('if_block_creators');
const if_blocks = block.get_unique_name('if_blocks');
const need_select_block_ctx = this.branches.some(branch => branch.get_ctx_name);
const select_block_ctx = need_select_block_ctx ? block.get_unique_name('select_block_ctx') : null;
const if_ctx = select_block_ctx ? x`${select_block_ctx}(#ctx, ${current_block_type_index})` : x`#ctx`;
const if_current_block_type_index = has_else
? nodes => nodes
: nodes => b`if (~${current_block_type_index}) { ${nodes} }`;
block.add_variable(current_block_type_index);
block.add_variable(name);
block.chunks.init.push(b`
const ${if_block_creators} = [
${this.branches.map(branch => branch.block.name)}
];
const ${if_blocks} = [];
${this.needs_update
? b`
function ${select_block_type}(#ctx, #dirty) {
${this.branches.map(({ dependencies, condition, snippet }) => {
return b`${snippet && dependencies.length > 0 ? b`if (${block.renderer.dirty(dependencies)}) ${condition} = null;` : null}`;
})}
${this.branches.map(({ condition, snippet }, i) => condition
? b`
${snippet && b`if (${condition} == null) ${condition} = !!${snippet}`}
if (${condition}) return ${i};`
: b`return ${i};`)}
${!has_else && b`return -1;`}
}
`
: b`
function ${select_block_type}(#ctx, #dirty) {
${this.branches.map(({ condition, snippet }, i) => condition
? b`if (${snippet || condition}) return ${i};`
: b`return ${i};`)}
${!has_else && b`return -1;`}
}
`}
`);
if (need_select_block_ctx) {
// if all branches needs create a context
if (this.branches.every(branch => branch.get_ctx_name)) {
block.chunks.init.push(b`
function ${select_block_ctx}(#ctx, #index) {
${this.branches.map(({ condition, get_ctx_name }, i) => {
return condition
? b`if (#index === ${i}) return ${get_ctx_name}(#ctx);`
: b`return ${get_ctx_name}(#ctx);`;
}).filter(Boolean)}
}
`);
} else {
// when not all branches need to create a new context,
// this code is simpler
block.chunks.init.push(b`
function ${select_block_ctx}(#ctx, #index) {
${this.branches.map(({ get_ctx_name }, i) => {
return get_ctx_name
? b`if (#index === ${i}) return ${get_ctx_name}(#ctx);`
: null;
}).filter(Boolean)}
return #ctx;
}
`);
}
}
if (has_else) {
block.chunks.init.push(b`
${current_block_type_index} = ${select_block_type}(#ctx, ${this.renderer.get_initial_dirty()});
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](${if_ctx});
`);
} else {
block.chunks.init.push(b`
if (~(${current_block_type_index} = ${select_block_type}(#ctx, ${this.renderer.get_initial_dirty()}))) {
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](${if_ctx});
}
`);
}
const initial_mount_node = parent_node || '#target';
const anchor_node = parent_node ? 'null' : '#anchor';
block.chunks.mount.push(
if_current_block_type_index(
b`${if_blocks}[${current_block_type_index}].m(${initial_mount_node}, ${anchor_node});`
)
);
if (this.needs_update) {
const update_mount_node = this.get_update_mount_node(anchor);
const destroy_old_block = b`
@group_outros();
@transition_out(${if_blocks}[${previous_block_index}], 1, 1, () => {
${if_blocks}[${previous_block_index}] = null;
});
@check_outros();
`;
const create_new_block = b`
${name} = ${if_blocks}[${current_block_type_index}];
if (!${name}) {
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](${if_ctx});
${name}.c();
} else {
${dynamic && b`${name}.p(${if_ctx}, #dirty);`}
}
${has_transitions && b`@transition_in(${name}, 1);`}
${name}.m(${update_mount_node}, ${anchor});
`;
const change_block = has_else
? b`
${destroy_old_block}
${create_new_block}
`
: b`
if (${name}) {
${destroy_old_block}
}
if (~${current_block_type_index}) {
${create_new_block}
} else {
${name} = null;
}
`;
block.chunks.update.push(b`
let ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(#ctx, #dirty);
`);
if (dynamic) {
block.chunks.update.push(b`
if (${current_block_type_index} === ${previous_block_index}) {
${if_current_block_type_index(b`${if_blocks}[${current_block_type_index}].p(${if_ctx}, #dirty);`)}
} else {
${change_block}
}
`);
} else {
block.chunks.update.push(b`
if (${current_block_type_index} !== ${previous_block_index}) {
${change_block}
}
`);
}
} else if (dynamic) {
if (if_exists_condition) {
block.chunks.update.push(b`if (${if_exists_condition}) ${name}.p(${if_ctx}, #dirty);`);
} else {
block.chunks.update.push(b`${name}.p(${if_ctx}, #dirty);`);
}
}
block.chunks.destroy.push(
if_current_block_type_index(b`${if_blocks}[${current_block_type_index}].d(${detaching});`)
);
}
render_simple(
block: Block,
parent_node: Identifier,
_parent_nodes: Identifier,
dynamic,
{ name, anchor, if_exists_condition, has_transitions },
detaching
) {
const branch = this.branches[0];
const if_ctx = branch.get_ctx_name ? x`${branch.get_ctx_name}(#ctx)` : x`#ctx`;
if (branch.snippet) block.add_variable(branch.condition, branch.snippet);
block.chunks.init.push(b`
let ${name} = ${branch.condition} && ${branch.block.name}(${if_ctx});
`);
const initial_mount_node = parent_node || '#target';
const anchor_node = parent_node ? 'null' : '#anchor';
block.chunks.mount.push(
b`if (${name}) ${name}.m(${initial_mount_node}, ${anchor_node});`
);
if (branch.dependencies.length > 0) {
const update_mount_node = this.get_update_mount_node(anchor);
const enter = b`
if (${name}) {
${dynamic && b`${name}.p(${if_ctx}, #dirty);`}
${has_transitions &&
b`if (${block.renderer.dirty(branch.dependencies)}) {
@transition_in(${name}, 1);
}`
}
} else {
${name} = ${branch.block.name}(${if_ctx});
${name}.c();
${has_transitions && b`@transition_in(${name}, 1);`}
${name}.m(${update_mount_node}, ${anchor});
}
`;
if (branch.snippet) {
block.chunks.update.push(b`if (${block.renderer.dirty(branch.dependencies)}) ${branch.condition} = ${branch.snippet}`);
}
// no `p()` here — we don't want to update outroing nodes,
// as that will typically result in glitching
if (branch.block.has_outro_method) {
block.chunks.update.push(b`
if (${branch.condition}) {
${enter}
} else if (${name}) {
@group_outros();
@transition_out(${name}, 1, 1, () => {
${name} = null;
});
@check_outros();
}
`);
} else {
block.chunks.update.push(b`
if (${branch.condition}) {
${enter}
} else if (${name}) {
${name}.d(1);
${name} = null;
}
`);
}
} else if (dynamic) {
block.chunks.update.push(b`
if (${branch.condition}) ${name}.p(${if_ctx}, #dirty);
`);
}
if (if_exists_condition) {
block.chunks.destroy.push(b`
if (${if_exists_condition}) ${name}.d(${detaching});
`);
} else {
block.chunks.destroy.push(b`
${name}.d(${detaching});
`);
}
}
}

@ -36,7 +36,7 @@ export default {
},
expected_block_type: {
code: 'expected-block-type',
message: 'Expected if, each or await'
message: 'Expected if, each, await or case'
},
expected_name: {
code: 'expected-name',

@ -86,6 +86,14 @@ export class Parser {
}
}
add_to_end_of_stack(node: TemplateNode) {
this.stack.push(node);
}
remove_last_in_stack() {
this.stack.pop();
}
current() {
return this.stack[this.stack.length - 1];
}

@ -7,6 +7,7 @@ import { to_string } from '../utils/node';
import { Parser } from '../index';
import { TemplateNode } from '../../interfaces';
import parser_errors from '../errors';
import { Node } from 'estree';
function trim_whitespace(block: TemplateNode, trim_before: boolean, trim_after: boolean) {
if (!block.children || block.children.length === 0) return; // AwaitBlock
@ -28,36 +29,98 @@ function trim_whitespace(block: TemplateNode, trim_before: boolean, trim_after:
trim_whitespace(block.else, trim_before, trim_after);
}
if (block.case) {
trim_whitespace(block.case, trim_before, trim_after);
}
if (first_child.elseif) {
trim_whitespace(first_child, trim_before, trim_after);
}
}
function get_empty_block(type: string, start: number, expression: Node): TemplateNode {
if (type === 'AwaitBlock') {
return {
start,
end: null,
type,
expression,
value: null,
error: null,
pending: {
start: null,
end: null,
type: 'PendingBlock',
children: [],
skip: true
},
then: {
start: null,
end: null,
type: 'ThenBlock',
children: [],
skip: true
},
catch: {
start: null,
end: null,
type: 'CatchBlock',
children: [],
skip: true
}
};
}
if (type === 'SwitchBlock') {
return {
start,
end: null,
type,
discriminant: expression,
cases: []
};
}
return {
start,
end: null,
type,
expression,
children: []
};
}
export default function mustache(parser: Parser) {
const start = parser.index;
parser.index += 1;
parser.allow_whitespace();
// {/if}, {/each}, {/await} or {/key}
// {/if}, {/each}, {/await}, {/key} or {/switch}
if (parser.eat('/')) {
let block = parser.current();
let expected;
if (closing_tag_omitted(block.name)) {
block.end = start;
parser.stack.pop();
parser.remove_last_in_stack();
block = parser.current();
}
if (block.type === 'ElseBlock' || block.type === 'PendingBlock' || block.type === 'ThenBlock' || block.type === 'CatchBlock') {
block.end = start;
parser.stack.pop();
parser.remove_last_in_stack();
block = parser.current();
expected = 'await';
}
if (block.type === 'CaseBlock') {
block.end = start;
parser.remove_last_in_stack();
block = parser.current();
}
if (block.type === 'IfBlock') {
expected = 'if';
} else if (block.type === 'EachBlock') {
@ -66,6 +129,8 @@ export default function mustache(parser: Parser) {
expected = 'await';
} else if (block.type === 'KeyBlock') {
expected = 'key';
} else if (block.type === 'SwitchBlock') {
expected = 'switch';
} else {
parser.error(parser_errors.unexpected_block_close);
}
@ -76,7 +141,7 @@ export default function mustache(parser: Parser) {
while (block.elseif) {
block.end = parser.index;
parser.stack.pop();
parser.remove_last_in_stack();
block = parser.current();
if (block.else) {
@ -93,7 +158,7 @@ export default function mustache(parser: Parser) {
trim_whitespace(block, trim_before, trim_after);
block.end = parser.index;
parser.stack.pop();
parser.remove_last_in_stack();
} else if (parser.eat(':else')) {
if (parser.eat('if')) {
parser.error(parser_errors.invalid_elseif);
@ -135,7 +200,7 @@ export default function mustache(parser: Parser) {
]
};
parser.stack.push(block.else.children[0]);
parser.add_to_end_of_stack(block.else.children[0]);
} else {
// :else
const block = parser.current();
@ -157,8 +222,34 @@ export default function mustache(parser: Parser) {
children: []
};
parser.stack.push(block.else);
parser.add_to_end_of_stack(block.else);
}
} else if (parser.eat(':case')) {
parser.allow_whitespace();
const previous_case_block = parser.current();
previous_case_block.end = start;
parser.remove_last_in_stack();
// it's called test (not expression) in case blocks
const test = read_expression(parser);
const block = {
start,
end: null,
test,
type: 'CaseBlock',
children: []
};
parser.allow_whitespace();
parser.eat('}', true);
const switch_block = parser.current();
switch_block.cases.push(block);
parser.add_to_end_of_stack(block);
} else if (parser.match(':then') || parser.match(':catch')) {
const block = parser.current();
const is_then = parser.eat(':then') || !parser.eat(':catch');
@ -181,7 +272,7 @@ export default function mustache(parser: Parser) {
}
block.end = start;
parser.stack.pop();
parser.remove_last_in_stack();
const await_block = parser.current();
if (!parser.eat('}')) {
@ -200,9 +291,9 @@ export default function mustache(parser: Parser) {
};
await_block[is_then ? 'then' : 'catch'] = new_block;
parser.stack.push(new_block);
parser.add_to_end_of_stack(new_block);
} else if (parser.eat('#')) {
// {#if foo}, {#each foo} or {#await foo}
// {#if foo}, {#each foo}, {#await foo} or {#case foo}
let type;
if (parser.eat('if')) {
@ -213,6 +304,8 @@ export default function mustache(parser: Parser) {
type = 'AwaitBlock';
} else if (parser.eat('key')) {
type = 'KeyBlock';
} else if (parser.eat('switch')) {
type = 'SwitchBlock';
} else {
parser.error(parser_errors.expected_block_type);
}
@ -221,44 +314,7 @@ export default function mustache(parser: Parser) {
const expression = read_expression(parser);
const block: TemplateNode = type === 'AwaitBlock' ?
{
start,
end: null,
type,
expression,
value: null,
error: null,
pending: {
start: null,
end: null,
type: 'PendingBlock',
children: [],
skip: true
},
then: {
start: null,
end: null,
type: 'ThenBlock',
children: [],
skip: true
},
catch: {
start: null,
end: null,
type: 'CatchBlock',
children: [],
skip: true
}
} :
{
start,
end: null,
type,
expression,
children: []
};
const block: TemplateNode = get_empty_block(type, start, expression);
parser.allow_whitespace();
// {#each} blocks must declare a context {#each list as item}
@ -310,10 +366,40 @@ export default function mustache(parser: Parser) {
}
}
if (type === 'SwitchBlock') {
const no_default_case = parser.eat('case');
if (no_default_case) parser.require_whitespace();
const test = read_expression(parser);
const first_case_block: any = {
type: 'CaseBlock',
start: block.start,
end: null,
children: []
};
if (no_default_case) {
first_case_block.test = test;
} else {
first_case_block.isdefault = true;
}
parser.current().children.push(block);
block.cases.push(first_case_block);
parser.add_to_end_of_stack(block);
parser.add_to_end_of_stack(first_case_block);
}
parser.eat('}', true);
// SwitchBlock doesn't have children, it has cases with children
// so this logic is performed above if type is SwitchBlock
if (type !== 'SwitchBlock') {
parser.current().children.push(block);
parser.stack.push(block);
parser.add_to_end_of_stack(block);
}
if (type === 'AwaitBlock') {
let child_block;
@ -329,8 +415,10 @@ export default function mustache(parser: Parser) {
}
child_block.start = parser.index;
parser.stack.push(child_block);
parser.add_to_end_of_stack(child_block);
}
} else if (parser.eat('@html')) {
// {@html content} tag
parser.require_whitespace();
@ -349,7 +437,7 @@ export default function mustache(parser: Parser) {
} else if (parser.eat('@debug')) {
let identifiers;
// Implies {@debug} which indicates "debug all"
// Implies {@debug} which indicates 'debug all'
if (parser.read(/\s*}/)) {
identifiers = [];
} else {

@ -137,13 +137,13 @@ export default function tag(parser: Parser) {
}
parent.end = start;
parser.stack.pop();
parser.remove_last_in_stack();
parent = parser.current();
}
parent.end = parser.index;
parser.stack.pop();
parser.remove_last_in_stack();
if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) {
parser.last_auto_closed_tag = null;
@ -152,7 +152,7 @@ export default function tag(parser: Parser) {
return;
} else if (closing_tag_omitted(parent.name, name)) {
parent.end = start;
parser.stack.pop();
parser.remove_last_in_stack();
parser.last_auto_closed_tag = {
tag: parent.name,
reason: name,
@ -232,7 +232,7 @@ export default function tag(parser: Parser) {
parser.eat(`</${name}>`, true);
element.end = parser.index;
} else {
parser.stack.push(element);
parser.add_to_end_of_stack(element);
}
}

@ -4,6 +4,8 @@ export function to_string(node: TemplateNode) {
switch (node.type) {
case 'IfBlock':
return '{#if} block';
case 'CaseBlock':
return '{:case} block';
case 'ThenBlock':
return '{:then} block';
case 'ElseBlock':
@ -17,6 +19,8 @@ export function to_string(node: TemplateNode) {
return '{#each} block';
case 'RawMustacheTag':
return '{@html} block';
case 'SwitchBlock':
return '{#switch} block';
case 'DebugTag':
return '{@debug} block';
case 'ConstTag':

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

@ -0,0 +1,109 @@
{
"html": {
"start": 0,
"end": 62,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 62,
"type": "SwitchBlock",
"discriminant": {
"type": "Identifier",
"start": 9,
"end": 12,
"loc": {
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 12
}
},
"name": "foo"
},
"cases": [
{
"start": 0,
"end": 32,
"type": "CaseBlock",
"test": {
"type": "Identifier",
"start": 18,
"end": 21,
"loc": {
"start": {
"line": 1,
"column": 18
},
"end": {
"line": 1,
"column": 21
}
},
"name": "bar"
},
"children": [
{
"start": 22,
"end": 32,
"type": "Element",
"name": "p",
"attributes": [],
"children": [
{
"start": 25,
"end": 28,
"type": "Text",
"raw": "bar",
"data": "bar"
}
]
}
]
},
{
"start": 32,
"end": 53,
"type": "CaseBlock",
"test": {
"type": "Identifier",
"start": 39,
"end": 42,
"loc": {
"start": {
"line": 1,
"column": 39
},
"end": {
"line": 1,
"column": 42
}
},
"name": "foo"
},
"children": [
{
"start": 43,
"end": 53,
"type": "Element",
"name": "p",
"attributes": [],
"children": [
{
"start": 46,
"end": 49,
"type": "Text",
"raw": "foo",
"data": "foo"
}
]
}
]
}]
}
]
}
}

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

@ -0,0 +1,94 @@
{
"html": {
"start": 0,
"end": 57,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 57,
"type": "SwitchBlock",
"discriminant": {
"type": "Identifier",
"start": 9,
"end": 12,
"loc": {
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 12
}
},
"name": "foo"
},
"cases": [
{
"start": 0,
"end": 27,
"type": "CaseBlock",
"isdefault": true,
"children": [
{
"start": 13,
"end": 27,
"type": "Element",
"name": "p",
"attributes": [],
"children": [
{
"start": 16,
"end": 23,
"type": "Text",
"raw": "default",
"data": "default"
}
]
}
]
},
{
"start": 27,
"end": 48,
"type": "CaseBlock",
"test": {
"type": "Identifier",
"start": 34,
"end": 37,
"loc": {
"start": {
"line": 1,
"column": 34
},
"end": {
"line": 1,
"column": 37
}
},
"name": "bar"
},
"children": [
{
"start": 38,
"end": 48,
"type": "Element",
"name": "p",
"attributes": [],
"children": [
{
"start": 41,
"end": 44,
"type": "Text",
"raw": "bar",
"data": "bar"
}
]
}
]
}]
}
]
}
}

@ -0,0 +1,15 @@
export default {
props: {
thing: 'foo'
},
html: '<p>i am foo</p>',
test({ assert, component, target }) {
assert.htmlEqual( target.innerHTML, '<p>i am foo</p>' );
component.thing = 'bar';
assert.htmlEqual( target.innerHTML, '<p>i am bar</p>' );
component.thing = 'no-match';
assert.htmlEqual( target.innerHTML, '<p>i am default</p>' );
}
};

@ -0,0 +1,12 @@
<script>
export let thing;
let a = "foo"
let b = "bar"
</script>
{#switch thing case a}
<p>i am foo</p>
{:case b}
<p>i am bar</p>
{/switch}

@ -0,0 +1,15 @@
export default {
props: {
thing: 'foo'
},
html: '<p>i am foo</p>',
test({ assert, component, target }) {
assert.htmlEqual( target.innerHTML, '<p>i am foo</p>' );
component.thing = 'bar';
assert.htmlEqual( target.innerHTML, '<p>i am bar</p>' );
component.thing = 'no-match';
assert.htmlEqual( target.innerHTML, '<p>i am default</p>' );
}
};

@ -0,0 +1,14 @@
<script>
export let thing;
let a = "foo"
let b = "bar"
</script>
{#switch thing}
<p>i am default</p>
{:case a}
<p>i am foo</p>
{:case b}
<p>i am bar</p>
{/switch}
Loading…
Cancel
Save