From 2cc56af2c66cde4e423cbfebae8daa689e11ab92 Mon Sep 17 00:00:00 2001 From: Mathias Picker Date: Sun, 3 Jul 2022 15:23:06 +0200 Subject: [PATCH 1/3] First steps for adding switch-case syntax --- src/compiler/compile/nodes/CaseBlock.ts | 32 + src/compiler/compile/nodes/SwitchBlock.ts | 27 + src/compiler/compile/nodes/interfaces.ts | 8 +- .../compile/nodes/shared/Expression.ts | 1 + .../compile/nodes/shared/map_children.ts | 4 + src/compiler/compile/render_dom/Renderer.ts | 4 + .../compile/render_dom/wrappers/Fragment.ts | 2 + .../compile/render_dom/wrappers/IfBlock.ts | 591 +----------------- .../render_dom/wrappers/SwitchBlock.ts | 87 +++ .../wrappers/shared/ConditionalBlockBranch.ts | 115 ++++ .../shared/ConditionalBlockWrapper.ts | 530 ++++++++++++++++ src/compiler/parse/errors.ts | 2 +- src/compiler/parse/index.ts | 8 + src/compiler/parse/state/mustache.ts | 192 ++++-- src/compiler/parse/state/tag.ts | 8 +- src/compiler/parse/utils/node.ts | 4 + .../switch-case-block-no-default/input.svelte | 1 + .../switch-case-block-no-default/output.json | 109 ++++ .../samples/switch-case-block/input.svelte | 1 + .../samples/switch-case-block/output.json | 94 +++ .../switch-block-no-default/_config.js | 15 + .../switch-block-no-default/main.svelte | 12 + test/runtime/samples/switch-block/_config.js | 15 + test/runtime/samples/switch-block/main.svelte | 14 + 24 files changed, 1235 insertions(+), 641 deletions(-) create mode 100644 src/compiler/compile/nodes/CaseBlock.ts create mode 100644 src/compiler/compile/nodes/SwitchBlock.ts create mode 100644 src/compiler/compile/render_dom/wrappers/SwitchBlock.ts create mode 100644 src/compiler/compile/render_dom/wrappers/shared/ConditionalBlockBranch.ts create mode 100644 src/compiler/compile/render_dom/wrappers/shared/ConditionalBlockWrapper.ts create mode 100644 test/parser/samples/switch-case-block-no-default/input.svelte create mode 100644 test/parser/samples/switch-case-block-no-default/output.json create mode 100644 test/parser/samples/switch-case-block/input.svelte create mode 100644 test/parser/samples/switch-case-block/output.json create mode 100644 test/runtime/samples/switch-block-no-default/_config.js create mode 100644 test/runtime/samples/switch-block-no-default/main.svelte create mode 100644 test/runtime/samples/switch-block/_config.js create mode 100644 test/runtime/samples/switch-block/main.svelte diff --git a/src/compiler/compile/nodes/CaseBlock.ts b/src/compiler/compile/nodes/CaseBlock.ts new file mode 100644 index 000000000000..e38fb7ad438d --- /dev/null +++ b/src/compiler/compile/nodes/CaseBlock.ts @@ -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(); + } +} diff --git a/src/compiler/compile/nodes/SwitchBlock.ts b/src/compiler/compile/nodes/SwitchBlock.ts new file mode 100644 index 000000000000..0ac879f8c981 --- /dev/null +++ b/src/compiler/compile/nodes/SwitchBlock.ts @@ -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(); + } +} diff --git a/src/compiler/compile/nodes/interfaces.ts b/src/compiler/compile/nodes/interfaces.ts index f023cad25c9f..4385f6a0d506 100644 --- a/src/compiler/compile/nodes/interfaces.ts +++ b/src/compiler/compile/nodes/interfaces.ts @@ -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; diff --git a/src/compiler/compile/nodes/shared/Expression.ts b/src/compiler/compile/nodes/shared/Expression.ts index a773355e3117..ada1432781b0 100644 --- a/src/compiler/compile/nodes/shared/Expression.ts +++ b/src/compiler/compile/nodes/shared/Expression.ts @@ -188,6 +188,7 @@ export default class Expression { template_scope, owner } = this; + let scope = this.scope; let function_expression; diff --git a/src/compiler/compile/nodes/shared/map_children.ts b/src/compiler/compile/nodes/shared/map_children.ts index e6ad1d7f6eec..267c7999cf8d 100644 --- a/src/compiler/compile/nodes/shared/map_children.ts +++ b/src/compiler/compile/nodes/shared/map_children.ts @@ -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; @@ -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; diff --git a/src/compiler/compile/render_dom/Renderer.ts b/src/compiler/compile/render_dom/Renderer.ts index 179fc0c95600..df6d87113c83 100644 --- a/src/compiler/compile/render_dom/Renderer.ts +++ b/src/compiler/compile/render_dom/Renderer.ts @@ -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); } diff --git a/src/compiler/compile/render_dom/wrappers/Fragment.ts b/src/compiler/compile/render_dom/wrappers/Fragment.ts index 87ce775ca93a..1601d88f26c5 100644 --- a/src/compiler/compile/render_dom/wrappers/Fragment.ts +++ b/src/compiler/compile/render_dom/wrappers/Fragment.ts @@ -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, diff --git a/src/compiler/compile/render_dom/wrappers/IfBlock.ts b/src/compiler/compile/render_dom/wrappers/IfBlock.ts index d74ce270bfe0..4d8fac16cfa7 100644 --- a/src/compiler/compile/render_dom/wrappers/IfBlock.ts +++ b/src/compiler/compile/render_dom/wrappers/IfBlock.ts @@ -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}); - `); - } - } } diff --git a/src/compiler/compile/render_dom/wrappers/SwitchBlock.ts b/src/compiler/compile/render_dom/wrappers/SwitchBlock.ts new file mode 100644 index 000000000000..99dbded24f7b --- /dev/null +++ b/src/compiler/compile/render_dom/wrappers/SwitchBlock.ts @@ -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); + } +} diff --git a/src/compiler/compile/render_dom/wrappers/shared/ConditionalBlockBranch.ts b/src/compiler/compile/render_dom/wrappers/shared/ConditionalBlockBranch.ts new file mode 100644 index 000000000000..6826db35f1d8 --- /dev/null +++ b/src/compiler/compile/render_dom/wrappers/shared/ConditionalBlockBranch.ts @@ -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' + ); + } + } +} diff --git a/src/compiler/compile/render_dom/wrappers/shared/ConditionalBlockWrapper.ts b/src/compiler/compile/render_dom/wrappers/shared/ConditionalBlockWrapper.ts new file mode 100644 index 000000000000..8cfaa5e1724a --- /dev/null +++ b/src/compiler/compile/render_dom/wrappers/shared/ConditionalBlockWrapper.ts @@ -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}); + `); + } + } +} diff --git a/src/compiler/parse/errors.ts b/src/compiler/parse/errors.ts index 63bd5b09199f..585858e1a46c 100644 --- a/src/compiler/parse/errors.ts +++ b/src/compiler/parse/errors.ts @@ -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', diff --git a/src/compiler/parse/index.ts b/src/compiler/parse/index.ts index 836fb670cfd5..c0b0337eb18e 100644 --- a/src/compiler/parse/index.ts +++ b/src/compiler/parse/index.ts @@ -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]; } diff --git a/src/compiler/parse/state/mustache.ts b/src/compiler/parse/state/mustache.ts index da9016a9df47..925e0819bd66 100644 --- a/src/compiler/parse/state/mustache.ts +++ b/src/compiler/parse/state/mustache.ts @@ -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); - parser.current().children.push(block); - parser.stack.push(block); + // 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.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 { diff --git a/src/compiler/parse/state/tag.ts b/src/compiler/parse/state/tag.ts index efce375b7e7f..e9a7699fa192 100644 --- a/src/compiler/parse/state/tag.ts +++ b/src/compiler/parse/state/tag.ts @@ -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(``, true); element.end = parser.index; } else { - parser.stack.push(element); + parser.add_to_end_of_stack(element); } } diff --git a/src/compiler/parse/utils/node.ts b/src/compiler/parse/utils/node.ts index 944bdb4c58a9..59ae275ddda8 100644 --- a/src/compiler/parse/utils/node.ts +++ b/src/compiler/parse/utils/node.ts @@ -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': diff --git a/test/parser/samples/switch-case-block-no-default/input.svelte b/test/parser/samples/switch-case-block-no-default/input.svelte new file mode 100644 index 000000000000..c41fbc78aeb0 --- /dev/null +++ b/test/parser/samples/switch-case-block-no-default/input.svelte @@ -0,0 +1 @@ +{#switch foo case bar}

bar

{:case foo}

foo

{/switch} \ No newline at end of file diff --git a/test/parser/samples/switch-case-block-no-default/output.json b/test/parser/samples/switch-case-block-no-default/output.json new file mode 100644 index 000000000000..70fe2c16ee41 --- /dev/null +++ b/test/parser/samples/switch-case-block-no-default/output.json @@ -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" + } + ] + } + ] + }] + } + ] + } +} \ No newline at end of file diff --git a/test/parser/samples/switch-case-block/input.svelte b/test/parser/samples/switch-case-block/input.svelte new file mode 100644 index 000000000000..59806ecfd321 --- /dev/null +++ b/test/parser/samples/switch-case-block/input.svelte @@ -0,0 +1 @@ +{#switch foo}

default

{:case bar}

bar

{/switch} \ No newline at end of file diff --git a/test/parser/samples/switch-case-block/output.json b/test/parser/samples/switch-case-block/output.json new file mode 100644 index 000000000000..8ca6b77dc524 --- /dev/null +++ b/test/parser/samples/switch-case-block/output.json @@ -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" + } + ] + } + ] + }] + } + ] + } +} \ No newline at end of file diff --git a/test/runtime/samples/switch-block-no-default/_config.js b/test/runtime/samples/switch-block-no-default/_config.js new file mode 100644 index 000000000000..3ae11074d4ac --- /dev/null +++ b/test/runtime/samples/switch-block-no-default/_config.js @@ -0,0 +1,15 @@ +export default { + props: { + thing: 'foo' + }, + + html: '

i am foo

', + + test({ assert, component, target }) { + assert.htmlEqual( target.innerHTML, '

i am foo

' ); + component.thing = 'bar'; + assert.htmlEqual( target.innerHTML, '

i am bar

' ); + component.thing = 'no-match'; + assert.htmlEqual( target.innerHTML, '

i am default

' ); + } +}; diff --git a/test/runtime/samples/switch-block-no-default/main.svelte b/test/runtime/samples/switch-block-no-default/main.svelte new file mode 100644 index 000000000000..5b3179107e12 --- /dev/null +++ b/test/runtime/samples/switch-block-no-default/main.svelte @@ -0,0 +1,12 @@ + + +{#switch thing case a} +

i am foo

+{:case b} +

i am bar

+{/switch} diff --git a/test/runtime/samples/switch-block/_config.js b/test/runtime/samples/switch-block/_config.js new file mode 100644 index 000000000000..3ae11074d4ac --- /dev/null +++ b/test/runtime/samples/switch-block/_config.js @@ -0,0 +1,15 @@ +export default { + props: { + thing: 'foo' + }, + + html: '

i am foo

', + + test({ assert, component, target }) { + assert.htmlEqual( target.innerHTML, '

i am foo

' ); + component.thing = 'bar'; + assert.htmlEqual( target.innerHTML, '

i am bar

' ); + component.thing = 'no-match'; + assert.htmlEqual( target.innerHTML, '

i am default

' ); + } +}; diff --git a/test/runtime/samples/switch-block/main.svelte b/test/runtime/samples/switch-block/main.svelte new file mode 100644 index 000000000000..8ae6ebda3800 --- /dev/null +++ b/test/runtime/samples/switch-block/main.svelte @@ -0,0 +1,14 @@ + + +{#switch thing} +

i am default

+{:case a} +

i am foo

+{:case b} +

i am bar

+{/switch} From a3e88706c4d5e79d60fb12c7a2ec75e35587286e Mon Sep 17 00:00:00 2001 From: Mathias Picker Date: Sun, 3 Jul 2022 15:46:18 +0200 Subject: [PATCH 2/3] Renamed ConditionalBlockWrapper file to ConditionalBlock --- src/compiler/compile/render_dom/wrappers/IfBlock.ts | 2 +- src/compiler/compile/render_dom/wrappers/SwitchBlock.ts | 2 +- .../shared/{ConditionalBlockWrapper.ts => ConditionalBlock.ts} | 0 .../render_dom/wrappers/shared/ConditionalBlockBranch.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/compiler/compile/render_dom/wrappers/shared/{ConditionalBlockWrapper.ts => ConditionalBlock.ts} (100%) diff --git a/src/compiler/compile/render_dom/wrappers/IfBlock.ts b/src/compiler/compile/render_dom/wrappers/IfBlock.ts index 4d8fac16cfa7..ae95e56d3fe0 100644 --- a/src/compiler/compile/render_dom/wrappers/IfBlock.ts +++ b/src/compiler/compile/render_dom/wrappers/IfBlock.ts @@ -6,7 +6,7 @@ import ElseBlock from '../../nodes/ElseBlock'; import { Identifier } from 'estree'; import { push_array } from '../../../utils/push_array'; import ConditionalBlockBranch from './shared/ConditionalBlockBranch'; -import ConditionalBlockWrapper from './shared/ConditionalBlockWrapper'; +import ConditionalBlockWrapper from './shared/ConditionalBlock'; function is_else_if(node: ElseBlock) { return ( diff --git a/src/compiler/compile/render_dom/wrappers/SwitchBlock.ts b/src/compiler/compile/render_dom/wrappers/SwitchBlock.ts index 99dbded24f7b..7a9bd7790d62 100644 --- a/src/compiler/compile/render_dom/wrappers/SwitchBlock.ts +++ b/src/compiler/compile/render_dom/wrappers/SwitchBlock.ts @@ -7,7 +7,7 @@ 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'; +import ConditionalBlockWrapper from './shared/ConditionalBlock'; export default class SwitchBlockWrapper extends ConditionalBlockWrapper { node: SwitchBlock; diff --git a/src/compiler/compile/render_dom/wrappers/shared/ConditionalBlockWrapper.ts b/src/compiler/compile/render_dom/wrappers/shared/ConditionalBlock.ts similarity index 100% rename from src/compiler/compile/render_dom/wrappers/shared/ConditionalBlockWrapper.ts rename to src/compiler/compile/render_dom/wrappers/shared/ConditionalBlock.ts diff --git a/src/compiler/compile/render_dom/wrappers/shared/ConditionalBlockBranch.ts b/src/compiler/compile/render_dom/wrappers/shared/ConditionalBlockBranch.ts index 6826db35f1d8..7df88e2f1501 100644 --- a/src/compiler/compile/render_dom/wrappers/shared/ConditionalBlockBranch.ts +++ b/src/compiler/compile/render_dom/wrappers/shared/ConditionalBlockBranch.ts @@ -10,7 +10,7 @@ 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'; +import ConditionalBlockWrapper from './ConditionalBlock'; function get_expression(node: IfBlock | ElseBlock | CaseBlock): Expression | void { switch (node.type) { From 0e0de9c320df03cf0f1797a1377170393e3ca58c Mon Sep 17 00:00:00 2001 From: Mathias Picker Date: Sun, 3 Jul 2022 16:37:42 +0200 Subject: [PATCH 3/3] Removed temporary method --- src/compiler/compile/render_dom/Renderer.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/compiler/compile/render_dom/Renderer.ts b/src/compiler/compile/render_dom/Renderer.ts index df6d87113c83..179fc0c95600 100644 --- a/src/compiler/compile/render_dom/Renderer.ts +++ b/src/compiler/compile/render_dom/Renderer.ts @@ -279,10 +279,6 @@ 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); }