]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler-vapor): `v-else` / `v-else-if` (#98)
authorRizumu Ayaka <rizumu@ayaka.moe>
Sun, 28 Jan 2024 19:42:56 +0000 (03:42 +0800)
committerGitHub <noreply@github.com>
Sun, 28 Jan 2024 19:42:56 +0000 (03:42 +0800)
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
packages/compiler-vapor/__tests__/transforms/__snapshots__/vIf.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/vIf.spec.ts
packages/compiler-vapor/src/generators/if.ts
packages/compiler-vapor/src/ir.ts
packages/compiler-vapor/src/transform.ts
packages/compiler-vapor/src/transforms/vIf.ts

index 441f734706713a8499495f6dbeced5f740933cfe..a251ec008d74c541172a05112ac7b05b6d34ec63 100644 (file)
@@ -20,6 +20,30 @@ export function render(_ctx) {
 }"
 `;
 
+exports[`compiler: v-if > comment between branches 1`] = `
+"import { template as _template, fragment as _fragment, createIf as _createIf, prepend as _prepend } from 'vue/vapor';
+
+export function render(_ctx) {
+  const t0 = _template("<div></div>")
+  const t1 = _template("<!--foo--><p></p>")
+  const t2 = _template("<!--bar-->fine")
+  const t3 = _fragment()
+  const n0 = t3()
+  const n1 = _createIf(() => (_ctx.ok), () => {
+    const n2 = t0()
+    return n2
+  }, () => _createIf(() => (_ctx.orNot), () => {
+    const n3 = t1()
+    return n3
+  }, () => {
+    const n4 = t2()
+    return n4
+  }))
+  _prepend(n0, n1)
+  return n0
+}"
+`;
+
 exports[`compiler: v-if > dedupe same template 1`] = `
 "import { template as _template, fragment as _fragment, createIf as _createIf, append as _append } from 'vue/vapor';
 
@@ -59,3 +83,67 @@ export function render(_ctx) {
   return n0
 }"
 `;
+
+exports[`compiler: v-if > v-if + v-else 1`] = `
+"import { template as _template, fragment as _fragment, createIf as _createIf, prepend as _prepend } from 'vue/vapor';
+
+export function render(_ctx) {
+  const t0 = _template("<div></div>")
+  const t1 = _template("<p></p>")
+  const t2 = _fragment()
+  const n0 = t2()
+  const n1 = _createIf(() => (_ctx.ok), () => {
+    const n2 = t0()
+    return n2
+  }, () => {
+    const n3 = t1()
+    return n3
+  })
+  _prepend(n0, n1)
+  return n0
+}"
+`;
+
+exports[`compiler: v-if > v-if + v-else-if + v-else 1`] = `
+"import { template as _template, fragment as _fragment, createIf as _createIf, prepend as _prepend } from 'vue/vapor';
+
+export function render(_ctx) {
+  const t0 = _template("<div></div>")
+  const t1 = _template("<p></p>")
+  const t2 = _template("fine")
+  const t3 = _fragment()
+  const n0 = t3()
+  const n1 = _createIf(() => (_ctx.ok), () => {
+    const n2 = t0()
+    return n2
+  }, () => _createIf(() => (_ctx.orNot), () => {
+    const n3 = t1()
+    return n3
+  }, () => {
+    const n4 = t2()
+    return n4
+  }))
+  _prepend(n0, n1)
+  return n0
+}"
+`;
+
+exports[`compiler: v-if > v-if + v-else-if 1`] = `
+"import { template as _template, fragment as _fragment, createIf as _createIf, prepend as _prepend } from 'vue/vapor';
+
+export function render(_ctx) {
+  const t0 = _template("<div></div>")
+  const t1 = _template("<p></p>")
+  const t2 = _fragment()
+  const n0 = t2()
+  const n1 = _createIf(() => (_ctx.ok), () => {
+    const n2 = t0()
+    return n2
+  }, () => _createIf(() => (_ctx.orNot), () => {
+    const n3 = t1()
+    return n3
+  }))
+  _prepend(n0, n1)
+  return n0
+}"
+`;
index b7f7b188642472b61d9594577e461a0e3085cda7..7d6cd38a1d9ea6cd90b50765794a2e498c9580ea 100644 (file)
@@ -1,11 +1,14 @@
 import { makeCompile } from './_utils'
 import {
+  IRNodeTypes,
+  type IfIRNode,
   transformElement,
   transformInterpolation,
   transformOnce,
   transformVIf,
   transformVText,
 } from '../../src'
+import { NodeTypes } from '@vue/compiler-core'
 
 const compileWithVIf = makeCompile({
   nodeTransforms: [
@@ -21,15 +24,87 @@ const compileWithVIf = makeCompile({
 
 describe('compiler: v-if', () => {
   test('basic v-if', () => {
-    const { code } = compileWithVIf(`<div v-if="ok">{{msg}}</div>`)
+    const { code, vaporHelpers, ir, helpers } = compileWithVIf(
+      `<div v-if="ok">{{msg}}</div>`,
+    )
+
+    expect(vaporHelpers).contains('createIf')
+    expect(helpers.size).toBe(0)
+
+    expect(ir.template).lengthOf(2)
+    expect(ir.template).toMatchObject([
+      {
+        template: '<div></div>',
+        type: IRNodeTypes.TEMPLATE_FACTORY,
+      },
+      {
+        type: IRNodeTypes.FRAGMENT_FACTORY,
+      },
+    ])
+    expect(ir.operation).toMatchObject([
+      {
+        type: IRNodeTypes.IF,
+        id: 1,
+        condition: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'ok',
+          isStatic: false,
+        },
+        positive: {
+          type: IRNodeTypes.BLOCK_FUNCTION,
+          templateIndex: 0,
+        },
+      },
+      {
+        type: IRNodeTypes.APPEND_NODE,
+        elements: [1],
+        parent: 0,
+      },
+    ])
+
+    expect(ir.dynamic).toMatchObject({
+      id: 0,
+      children: { 0: { id: 1 } },
+    })
+
+    expect(ir.effect).toEqual([])
+    expect((ir.operation[0] as IfIRNode).positive.effect).lengthOf(1)
+
     expect(code).matchSnapshot()
   })
 
   test('template v-if', () => {
-    const { code } = compileWithVIf(
+    const { code, ir } = compileWithVIf(
       `<template v-if="ok"><div/>hello<p v-text="msg"/></template>`,
     )
     expect(code).matchSnapshot()
+
+    expect(ir.template).lengthOf(2)
+    expect(ir.template[0]).toMatchObject({
+      template: '<div></div>hello<p></p>',
+      type: IRNodeTypes.TEMPLATE_FACTORY,
+    })
+
+    expect(ir.effect).toEqual([])
+    expect((ir.operation[0] as IfIRNode).positive.effect).toMatchObject([
+      {
+        operations: [
+          {
+            type: IRNodeTypes.SET_TEXT,
+            element: 3,
+            value: {
+              content: 'msg',
+              type: NodeTypes.SIMPLE_EXPRESSION,
+              isStatic: false,
+            },
+          },
+        ],
+      },
+    ])
+    expect((ir.operation[0] as IfIRNode).positive.dynamic).toMatchObject({
+      id: 2,
+      children: { 2: { id: 3 } },
+    })
   })
 
   test('dedupe same template', () => {
@@ -42,10 +117,185 @@ describe('compiler: v-if', () => {
 
   test.todo('v-if with v-once')
   test.todo('component v-if')
-  test.todo('v-if + v-else')
-  test.todo('v-if + v-else-if')
-  test.todo('v-if + v-else-if + v-else')
-  test.todo('comment between branches')
+
+  test('v-if + v-else', () => {
+    const { code, ir, vaporHelpers, helpers } = compileWithVIf(
+      `<div v-if="ok"/><p v-else/>`,
+    )
+    expect(code).matchSnapshot()
+    expect(ir.template).lengthOf(3)
+    expect(ir.template).toMatchObject([
+      {
+        template: '<div></div>',
+        type: IRNodeTypes.TEMPLATE_FACTORY,
+      },
+      {
+        template: '<p></p>',
+        type: IRNodeTypes.TEMPLATE_FACTORY,
+      },
+      {
+        type: IRNodeTypes.FRAGMENT_FACTORY,
+      },
+    ])
+
+    expect(vaporHelpers).contains('createIf')
+    expect(ir.effect).lengthOf(0)
+    expect(helpers).lengthOf(0)
+    expect(ir.operation).toMatchObject([
+      {
+        type: IRNodeTypes.IF,
+        id: 1,
+        condition: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'ok',
+          isStatic: false,
+        },
+        positive: {
+          type: IRNodeTypes.BLOCK_FUNCTION,
+          templateIndex: 0,
+        },
+        negative: {
+          type: IRNodeTypes.BLOCK_FUNCTION,
+          templateIndex: 1,
+        },
+      },
+      {
+        type: IRNodeTypes.PREPEND_NODE,
+        elements: [1],
+        parent: 0,
+      },
+    ])
+  })
+
+  test('v-if + v-else-if', () => {
+    const { code, ir } = compileWithVIf(
+      `<div v-if="ok"/><p v-else-if="orNot"/>`,
+    )
+    expect(code).matchSnapshot()
+    expect(ir.template).lengthOf(3)
+    expect(ir.template).toMatchObject([
+      {
+        template: '<div></div>',
+        type: IRNodeTypes.TEMPLATE_FACTORY,
+      },
+      {
+        template: '<p></p>',
+        type: IRNodeTypes.TEMPLATE_FACTORY,
+      },
+      { type: IRNodeTypes.FRAGMENT_FACTORY },
+    ])
+
+    expect(ir.operation).toMatchObject([
+      {
+        type: IRNodeTypes.IF,
+        id: 1,
+        condition: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: 'ok',
+          isStatic: false,
+        },
+        positive: {
+          type: IRNodeTypes.BLOCK_FUNCTION,
+          templateIndex: 0,
+        },
+        negative: {
+          type: IRNodeTypes.IF,
+          condition: {
+            type: NodeTypes.SIMPLE_EXPRESSION,
+            content: 'orNot',
+            isStatic: false,
+          },
+          positive: {
+            type: IRNodeTypes.BLOCK_FUNCTION,
+            templateIndex: 1,
+          },
+        },
+      },
+      {
+        type: IRNodeTypes.PREPEND_NODE,
+        elements: [1],
+        parent: 0,
+      },
+    ])
+  })
+
+  test('v-if + v-else-if + v-else', () => {
+    const { code, ir } = compileWithVIf(
+      `<div v-if="ok"/><p v-else-if="orNot"/><template v-else>fine</template>`,
+    )
+    expect(code).matchSnapshot()
+    expect(ir.template).lengthOf(4)
+    expect(ir.template).toMatchObject([
+      {
+        template: '<div></div>',
+        type: IRNodeTypes.TEMPLATE_FACTORY,
+      },
+      {
+        template: '<p></p>',
+        type: IRNodeTypes.TEMPLATE_FACTORY,
+      },
+      {
+        template: 'fine',
+        type: IRNodeTypes.TEMPLATE_FACTORY,
+      },
+      { type: IRNodeTypes.FRAGMENT_FACTORY },
+    ])
+
+    expect(ir.operation).toMatchObject([
+      {
+        type: IRNodeTypes.IF,
+        id: 1,
+        positive: {
+          type: IRNodeTypes.BLOCK_FUNCTION,
+          templateIndex: 0,
+        },
+        negative: {
+          type: IRNodeTypes.IF,
+          positive: {
+            type: IRNodeTypes.BLOCK_FUNCTION,
+            templateIndex: 1,
+          },
+          negative: {
+            type: IRNodeTypes.BLOCK_FUNCTION,
+            templateIndex: 2,
+          },
+        },
+      },
+      {
+        type: IRNodeTypes.PREPEND_NODE,
+        elements: [1],
+        parent: 0,
+      },
+    ])
+  })
+
+  test('comment between branches', () => {
+    const { code, ir } = compileWithVIf(`
+      <div v-if="ok"/>
+      <!--foo-->
+      <p v-else-if="orNot"/>
+      <!--bar-->
+      <template v-else>fine</template>
+    `)
+    expect(code).matchSnapshot()
+    expect(ir.template).lengthOf(4)
+    expect(ir.template).toMatchObject([
+      {
+        template: '<div></div>',
+        type: IRNodeTypes.TEMPLATE_FACTORY,
+      },
+      {
+        template: '<!--foo--><p></p>',
+        type: IRNodeTypes.TEMPLATE_FACTORY,
+      },
+      {
+        template: '<!--bar-->fine',
+        type: IRNodeTypes.TEMPLATE_FACTORY,
+      },
+      { type: IRNodeTypes.FRAGMENT_FACTORY },
+    ])
+  })
+
   describe.todo('errors')
   describe.todo('codegen')
   test.todo('v-on with v-if')
index 69ef737d93aad6a0e19a7b7a9c7aa68c53bd6d22..0019273f8277fc6dcfdfcad195aa4d92e9ea722c 100644 (file)
@@ -1,12 +1,30 @@
 import { type CodegenContext, genBlockFunctionContent } from '../generate'
-import type { BlockFunctionIRNode, IfIRNode } from '../ir'
+import { type BlockFunctionIRNode, IRNodeTypes, type IfIRNode } from '../ir'
 import { genExpression } from './expression'
 
-export function genIf(oper: IfIRNode, context: CodegenContext) {
-  const { pushFnCall, vaporHelper, pushNewline, push, withIndent } = context
+export function genIf(
+  oper: IfIRNode,
+  context: CodegenContext,
+  isNested = false,
+) {
+  const { pushFnCall, vaporHelper, pushNewline, push } = context
   const { condition, positive, negative } = oper
 
-  pushNewline(`const n${oper.id} = `)
+  let positiveArg = () => genBlockFunction(positive, context)
+  let negativeArg: false | (() => void) = false
+
+  if (negative) {
+    if (negative.type === IRNodeTypes.BLOCK_FUNCTION) {
+      negativeArg = () => genBlockFunction(negative, context)
+    } else {
+      negativeArg = () => {
+        push('() => ')
+        genIf(negative!, context, true)
+      }
+    }
+  }
+
+  if (!isNested) pushNewline(`const n${oper.id} = `)
   pushFnCall(
     vaporHelper('createIf'),
     () => {
@@ -14,15 +32,17 @@ export function genIf(oper: IfIRNode, context: CodegenContext) {
       genExpression(condition, context)
       push(')')
     },
-    () => genBlockFunction(positive),
-    !!negative && (() => genBlockFunction(negative!)),
+    positiveArg,
+    negativeArg,
   )
+}
 
-  function genBlockFunction(oper: BlockFunctionIRNode) {
-    push('() => {')
-    withIndent(() => {
-      genBlockFunctionContent(oper, context)
-    })
-    pushNewline('}')
-  }
+function genBlockFunction(oper: BlockFunctionIRNode, context: CodegenContext) {
+  const { pushNewline, push, withIndent } = context
+
+  push('() => {')
+  withIndent(() => {
+    genBlockFunctionContent(oper, context)
+  })
+  pushNewline('}')
 }
index 09ca59fff6b95db1bba6da1c889221c78acbd882..7c0872a1bd68c7de6cc7eb1aac0f5c21de093fac 100644 (file)
@@ -62,7 +62,7 @@ export interface IfIRNode extends BaseIRNode {
   id: number
   condition: IRExpression
   positive: BlockFunctionIRNode
-  negative?: BlockFunctionIRNode
+  negative?: BlockFunctionIRNode | IfIRNode
 }
 
 export interface TemplateFactoryIRNode extends BaseIRNode {
index b5f66c1ca60c7014a9a24abefe740bb2ac75440a..b8b3adf254c4017b414edb361547d63bac8337aa 100644 (file)
@@ -42,9 +42,9 @@ export type DirectiveTransform = (
 // A structural directive transform is technically also a NodeTransform;
 // Only v-if and v-for fall into this category.
 export type StructuralDirectiveTransform = (
-  node: RootNode | TemplateChildNode,
+  node: ElementNode,
   dir: VaporDirectiveNode,
-  context: TransformContext<RootNode | TemplateChildNode>,
+  context: TransformContext<ElementNode>,
 ) => void | (() => void)
 
 export type TransformOptions = HackOptions<BaseTransformOptions>
@@ -60,7 +60,7 @@ export interface TransformContext<T extends AllNode = AllNode> {
   >
 
   template: string
-  childrenTemplate: string[]
+  childrenTemplate: (string | null)[]
   dynamic: IRDynamicInfo
 
   inVOnce: boolean
@@ -311,15 +311,12 @@ function transformNode(
   }
 
   if (context.node.type === NodeTypes.ROOT)
-    context.template += context.childrenTemplate.join('')
+    context.template += context.childrenTemplate.filter(Boolean).join('')
 }
 
 function transformChildren(ctx: TransformContext<RootNode | ElementNode>) {
   const { children } = ctx.node
   let i = 0
-  // const nodeRemoved = () => {
-  //   i--
-  // }
   for (; i < children.length; i++) {
     const child = children[i]
     const childContext = createContext(child, ctx, i)
@@ -405,7 +402,11 @@ export function createStructuralDirectiveTransform(
       const exitFns = []
       for (const prop of props) {
         if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
-          const onExit = fn(node, prop as VaporDirectiveNode, context)
+          const onExit = fn(
+            node,
+            prop as VaporDirectiveNode,
+            context as TransformContext<ElementNode>,
+          )
           if (onExit) exitFns.push(onExit)
         }
       }
index 90d4a8c93c46a4377fe0c5f93779ee02f66180da..25e1c4de8b802559aebb12f815d04f7621644824 100644 (file)
@@ -1,4 +1,5 @@
 import {
+  type ElementNode,
   ElementTypes,
   ErrorCodes,
   NodeTypes,
@@ -15,6 +16,7 @@ import {
 import {
   type BlockFunctionIRNode,
   IRNodeTypes,
+  type OperationNode,
   type VaporDirectiveNode,
 } from '../ir'
 import { extend } from '@vue/shared'
@@ -25,7 +27,7 @@ export const transformVIf = createStructuralDirectiveTransform(
 )
 
 export function processIf(
-  node: RootNode | TemplateChildNode,
+  node: ElementNode,
   dir: VaporDirectiveNode,
   context: TransformContext<RootNode | TemplateChildNode>,
 ) {
@@ -40,7 +42,7 @@ export function processIf(
   if (dir.name === 'if') {
     const id = context.reference()
     context.dynamic.ghost = true
-    const [branch, onExit] = createIfBranch(node, dir, context)
+    const [branch, onExit] = createIfBranch(node, context)
 
     return () => {
       onExit()
@@ -52,37 +54,100 @@ export function processIf(
         positive: branch,
       })
     }
+  } else {
+    // check the adjacent v-if
+    const parent = context.parent!
+    const siblings = parent.node.children
+    const templates = parent.childrenTemplate
+
+    const comments = []
+    let sibling: TemplateChildNode | undefined
+    let i = siblings.indexOf(node)
+    while (i-- >= -1) {
+      sibling = siblings[i]
+
+      if (sibling) {
+        if (sibling.type === NodeTypes.COMMENT) {
+          __DEV__ && comments.unshift(sibling)
+          templates[i] = null
+          continue
+        } else if (
+          sibling.type === NodeTypes.TEXT &&
+          !sibling.content.trim().length
+        ) {
+          templates[i] = null
+          continue
+        }
+      }
+      break
+    }
+
+    const { operation } = context.block
+    let lastIfNode: OperationNode
+    if (
+      // check if v-if is the sibling node
+      !sibling ||
+      sibling.type !== NodeTypes.ELEMENT ||
+      !sibling.props.some(
+        ({ type, name }) =>
+          type === NodeTypes.DIRECTIVE && ['if', 'else-if'].includes(name),
+      ) ||
+      // check if IFNode is the last operation and get the root IFNode
+      !(lastIfNode = operation[operation.length - 1]) ||
+      lastIfNode.type !== IRNodeTypes.IF
+    ) {
+      context.options.onError(
+        createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc),
+      )
+      return
+    }
+
+    while (lastIfNode.negative && lastIfNode.negative.type === IRNodeTypes.IF) {
+      lastIfNode = lastIfNode.negative
+    }
+
+    // Check if v-else was followed by v-else-if
+    if (dir.name === 'else-if' && lastIfNode.negative) {
+      context.options.onError(
+        createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc),
+      )
+    }
+
+    // TODO ignore comments if the v-if is direct child of <transition> (PR #3622)
+    if (__DEV__ && comments.length) {
+      node = wrapTemplate(node)
+      context.node = node = extend({}, node, {
+        children: [...comments, ...node.children],
+      })
+    }
+
+    const [branch, onExit] = createIfBranch(node, context)
+
+    if (dir.name === 'else') {
+      lastIfNode.negative = branch
+    } else {
+      lastIfNode.negative = {
+        type: IRNodeTypes.IF,
+        id: -1,
+        loc: dir.loc,
+        condition: dir.exp!,
+        positive: branch,
+      }
+    }
+
+    return () => onExit()
   }
 }
 
 export function createIfBranch(
-  node: RootNode | TemplateChildNode,
-  dir: VaporDirectiveNode,
+  node: ElementNode,
   context: TransformContext<RootNode | TemplateChildNode>,
 ): [BlockFunctionIRNode, () => void] {
-  if (
-    node.type === NodeTypes.ELEMENT &&
-    node.tagType !== ElementTypes.TEMPLATE
-  ) {
-    node = extend({}, node, {
-      type: NodeTypes.ELEMENT,
-      tag: 'template',
-      props: [],
-      tagType: ElementTypes.TEMPLATE,
-      children: [
-        extend({}, node, {
-          props: node.props.filter(
-            p => p.type !== NodeTypes.DIRECTIVE && p.name !== 'if',
-          ),
-        } as TemplateChildNode),
-      ],
-    } as Partial<TemplateNode>)
-    context.node = node
-  }
+  context.node = node = wrapTemplate(node)
 
   const branch: BlockFunctionIRNode = {
     type: IRNodeTypes.BLOCK_FUNCTION,
-    loc: dir.loc,
+    loc: node.loc,
     node,
     templateIndex: -1,
     dynamic: {
@@ -105,3 +170,22 @@ export function createIfBranch(
   }
   return [branch, onExit]
 }
+
+function wrapTemplate(node: ElementNode): TemplateNode {
+  if (node.tagType === ElementTypes.TEMPLATE) {
+    return node
+  }
+  return extend({}, node, {
+    type: NodeTypes.ELEMENT,
+    tag: 'template',
+    props: [],
+    tagType: ElementTypes.TEMPLATE,
+    children: [
+      extend({}, node, {
+        props: node.props.filter(
+          p => p.type !== NodeTypes.DIRECTIVE && p.name !== 'if',
+        ),
+      } as TemplateChildNode),
+    ],
+  } as Partial<TemplateNode>)
+}