]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test(compiler): transformIf
authorEvan You <yyx990803@gmail.com>
Thu, 19 Sep 2019 19:41:17 +0000 (15:41 -0400)
committerEvan You <yyx990803@gmail.com>
Thu, 19 Sep 2019 19:41:17 +0000 (15:41 -0400)
packages/compiler-core/__tests__/directives/vIf.spec.ts [new file with mode: 0644]
packages/compiler-core/__tests__/transform.spec.ts
packages/compiler-core/src/directives/vIf.ts
packages/compiler-core/src/parse.ts
packages/compiler-core/src/transform.ts

diff --git a/packages/compiler-core/__tests__/directives/vIf.spec.ts b/packages/compiler-core/__tests__/directives/vIf.spec.ts
new file mode 100644 (file)
index 0000000..5039010
--- /dev/null
@@ -0,0 +1,257 @@
+import { parse } from '../../src/parse'
+import { transform } from '../../src/transform'
+import { transformIf } from '../../src/directives/vIf'
+import {
+  IfNode,
+  NodeTypes,
+  ElementNode,
+  TextNode,
+  CommentNode
+} from '../../src/ast'
+import { ErrorCodes } from '../../src/errors'
+
+describe('compiler: v-if', () => {
+  describe('transform', () => {
+    test('basic v-if', () => {
+      const ast = parse(`<div v-if="ok"/>`)
+      transform(ast, {
+        transforms: [transformIf]
+      })
+      const node = ast.children[0] as IfNode
+      expect(node.type).toBe(NodeTypes.IF)
+      expect(node.branches.length).toBe(1)
+      expect(node.branches[0].condition!.content).toBe(`ok`)
+      expect(node.branches[0].children.length).toBe(1)
+      expect(node.branches[0].children[0].type).toBe(NodeTypes.ELEMENT)
+      expect((node.branches[0].children[0] as ElementNode).tag).toBe(`div`)
+    })
+
+    test('template v-if', () => {
+      const ast = parse(`<template v-if="ok"><div/>hello<p/></template>`)
+      transform(ast, {
+        transforms: [transformIf]
+      })
+      const node = ast.children[0] as IfNode
+      expect(node.type).toBe(NodeTypes.IF)
+      expect(node.branches.length).toBe(1)
+      expect(node.branches[0].condition!.content).toBe(`ok`)
+      expect(node.branches[0].children.length).toBe(3)
+      expect(node.branches[0].children[0].type).toBe(NodeTypes.ELEMENT)
+      expect((node.branches[0].children[0] as ElementNode).tag).toBe(`div`)
+      expect(node.branches[0].children[1].type).toBe(NodeTypes.TEXT)
+      expect((node.branches[0].children[1] as TextNode).content).toBe(`hello`)
+      expect(node.branches[0].children[2].type).toBe(NodeTypes.ELEMENT)
+      expect((node.branches[0].children[2] as ElementNode).tag).toBe(`p`)
+    })
+
+    test('v-if + v-else', () => {
+      const ast = parse(`<div v-if="ok"/><p v-else/>`)
+      transform(ast, {
+        transforms: [transformIf]
+      })
+      // should fold branches
+      expect(ast.children.length).toBe(1)
+
+      const node = ast.children[0] as IfNode
+      expect(node.type).toBe(NodeTypes.IF)
+      expect(node.branches.length).toBe(2)
+
+      const b1 = node.branches[0]
+      expect(b1.condition!.content).toBe(`ok`)
+      expect(b1.children.length).toBe(1)
+      expect(b1.children[0].type).toBe(NodeTypes.ELEMENT)
+      expect((b1.children[0] as ElementNode).tag).toBe(`div`)
+
+      const b2 = node.branches[1]
+      expect(b2.condition).toBeUndefined()
+      expect(b2.children.length).toBe(1)
+      expect(b2.children[0].type).toBe(NodeTypes.ELEMENT)
+      expect((b2.children[0] as ElementNode).tag).toBe(`p`)
+    })
+
+    test('v-if + v-else-if', () => {
+      const ast = parse(`<div v-if="ok"/><p v-else-if="orNot"/>`)
+      transform(ast, {
+        transforms: [transformIf]
+      })
+      // should fold branches
+      expect(ast.children.length).toBe(1)
+
+      const node = ast.children[0] as IfNode
+      expect(node.type).toBe(NodeTypes.IF)
+      expect(node.branches.length).toBe(2)
+
+      const b1 = node.branches[0]
+      expect(b1.condition!.content).toBe(`ok`)
+      expect(b1.children.length).toBe(1)
+      expect(b1.children[0].type).toBe(NodeTypes.ELEMENT)
+      expect((b1.children[0] as ElementNode).tag).toBe(`div`)
+
+      const b2 = node.branches[1]
+      expect(b2.condition!.content).toBe(`orNot`)
+      expect(b2.children.length).toBe(1)
+      expect(b2.children[0].type).toBe(NodeTypes.ELEMENT)
+      expect((b2.children[0] as ElementNode).tag).toBe(`p`)
+    })
+
+    test('v-if + v-else-if + v-else', () => {
+      const ast = parse(
+        `<div v-if="ok"/><p v-else-if="orNot"/><template v-else>fine</template>`
+      )
+      transform(ast, {
+        transforms: [transformIf]
+      })
+      // should fold branches
+      expect(ast.children.length).toBe(1)
+
+      const node = ast.children[0] as IfNode
+      expect(node.type).toBe(NodeTypes.IF)
+      expect(node.branches.length).toBe(3)
+
+      const b1 = node.branches[0]
+      expect(b1.condition!.content).toBe(`ok`)
+      expect(b1.children.length).toBe(1)
+      expect(b1.children[0].type).toBe(NodeTypes.ELEMENT)
+      expect((b1.children[0] as ElementNode).tag).toBe(`div`)
+
+      const b2 = node.branches[1]
+      expect(b2.condition!.content).toBe(`orNot`)
+      expect(b2.children.length).toBe(1)
+      expect(b2.children[0].type).toBe(NodeTypes.ELEMENT)
+      expect((b2.children[0] as ElementNode).tag).toBe(`p`)
+
+      const b3 = node.branches[2]
+      expect(b3.condition).toBeUndefined()
+      expect(b3.children.length).toBe(1)
+      expect(b3.children[0].type).toBe(NodeTypes.TEXT)
+      expect((b3.children[0] as TextNode).content).toBe(`fine`)
+    })
+
+    test('comment between branches', () => {
+      const ast = parse(`
+        <div v-if="ok"/>
+        <!--foo-->
+        <p v-else-if="orNot"/>
+        <!--bar-->
+        <template v-else>fine</template>
+      `)
+      transform(ast, {
+        transforms: [transformIf]
+      })
+      // should fold branches
+      expect(ast.children.length).toBe(1)
+
+      const node = ast.children[0] as IfNode
+      expect(node.type).toBe(NodeTypes.IF)
+      expect(node.branches.length).toBe(3)
+
+      const b1 = node.branches[0]
+      expect(b1.condition!.content).toBe(`ok`)
+      expect(b1.children.length).toBe(1)
+      expect(b1.children[0].type).toBe(NodeTypes.ELEMENT)
+      expect((b1.children[0] as ElementNode).tag).toBe(`div`)
+
+      const b2 = node.branches[1]
+      expect(b2.condition!.content).toBe(`orNot`)
+      expect(b2.children.length).toBe(2)
+      expect(b2.children[0].type).toBe(NodeTypes.COMMENT)
+      expect((b2.children[0] as CommentNode).content).toBe(`foo`)
+      expect(b2.children[1].type).toBe(NodeTypes.ELEMENT)
+      expect((b2.children[1] as ElementNode).tag).toBe(`p`)
+
+      const b3 = node.branches[2]
+      expect(b3.condition).toBeUndefined()
+      expect(b3.children.length).toBe(2)
+      expect(b3.children[0].type).toBe(NodeTypes.COMMENT)
+      expect((b3.children[0] as CommentNode).content).toBe(`bar`)
+      expect(b3.children[1].type).toBe(NodeTypes.TEXT)
+      expect((b3.children[1] as TextNode).content).toBe(`fine`)
+    })
+
+    test('error on v-else missing adjacent v-if', () => {
+      const ast = parse(`<div v-else/>`)
+      const spy = jest.fn()
+      transform(ast, {
+        transforms: [transformIf],
+        onError: spy
+      })
+      expect(spy.mock.calls[0]).toMatchObject([
+        {
+          code: ErrorCodes.X_ELSE_NO_ADJACENT_IF,
+          loc: ast.children[0].loc.start
+        }
+      ])
+
+      const ast2 = parse(`<div/><div v-else/>`)
+      const spy2 = jest.fn()
+      transform(ast2, {
+        transforms: [transformIf],
+        onError: spy2
+      })
+      expect(spy2.mock.calls[0]).toMatchObject([
+        {
+          code: ErrorCodes.X_ELSE_NO_ADJACENT_IF,
+          loc: ast2.children[1].loc.start
+        }
+      ])
+
+      const ast3 = parse(`<div/>foo<div v-else/>`)
+      const spy3 = jest.fn()
+      transform(ast3, {
+        transforms: [transformIf],
+        onError: spy3
+      })
+      expect(spy3.mock.calls[0]).toMatchObject([
+        {
+          code: ErrorCodes.X_ELSE_NO_ADJACENT_IF,
+          loc: ast3.children[2].loc.start
+        }
+      ])
+    })
+
+    test('error on v-else-if missing adjacent v-if', () => {
+      const ast = parse(`<div v-else-if="foo"/>`)
+      const spy = jest.fn()
+      transform(ast, {
+        transforms: [transformIf],
+        onError: spy
+      })
+      expect(spy.mock.calls[0]).toMatchObject([
+        {
+          code: ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF,
+          loc: ast.children[0].loc.start
+        }
+      ])
+
+      const ast2 = parse(`<div/><div v-else-if="foo"/>`)
+      const spy2 = jest.fn()
+      transform(ast2, {
+        transforms: [transformIf],
+        onError: spy2
+      })
+      expect(spy2.mock.calls[0]).toMatchObject([
+        {
+          code: ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF,
+          loc: ast2.children[1].loc.start
+        }
+      ])
+
+      const ast3 = parse(`<div/>foo<div v-else-if="foo"/>`)
+      const spy3 = jest.fn()
+      transform(ast3, {
+        transforms: [transformIf],
+        onError: spy3
+      })
+      expect(spy3.mock.calls[0]).toMatchObject([
+        {
+          code: ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF,
+          loc: ast3.children[2].loc.start
+        }
+      ])
+    })
+  })
+
+  describe('codegen', () => {
+    // TODO
+  })
+})
index 2875e095618e0b71ed0bc16112429b4169f68fcc..87cb9326a3d09cbfaa8a2ae2eac256245251d2b1 100644 (file)
@@ -25,7 +25,7 @@ describe('compiler: transform', () => {
       {
         parent: ast,
         ancestors: [ast],
-        childIndex: 0
+        currentNode: div
       }
     ])
     expect(calls[1]).toMatchObject([
@@ -33,7 +33,7 @@ describe('compiler: transform', () => {
       {
         parent: div,
         ancestors: [ast, div],
-        childIndex: 0
+        currentNode: div.children[0]
       }
     ])
     expect(calls[2]).toMatchObject([
@@ -41,7 +41,7 @@ describe('compiler: transform', () => {
       {
         parent: div,
         ancestors: [ast, div],
-        childIndex: 1
+        currentNode: div.children[1]
       }
     ])
   })
@@ -81,7 +81,7 @@ describe('compiler: transform', () => {
   })
 
   test('context.removeNode', () => {
-    const ast = parse(`<span/><div/><span/>`)
+    const ast = parse(`<span/><div>hello</div><span/>`)
     const c1 = ast.children[0]
     const c2 = ast.children[2]
 
@@ -99,12 +99,67 @@ describe('compiler: transform', () => {
     expect(ast.children[0]).toBe(c1)
     expect(ast.children[1]).toBe(c2)
 
+    // should not traverse children of remove node
     expect(spy).toHaveBeenCalledTimes(3)
     // should traverse nodes around removed
     expect(spy.mock.calls[0][0]).toBe(c1)
     expect(spy.mock.calls[2][0]).toBe(c2)
   })
 
+  test('context.removeNode (prev sibling)', () => {
+    const ast = parse(`<span/><div/><span/>`)
+    const c1 = ast.children[0]
+    const c2 = ast.children[2]
+
+    const plugin: Transform = (node, context) => {
+      if (node.type === NodeTypes.ELEMENT && node.tag === 'div') {
+        context.removeNode()
+        // remove previous sibling
+        context.removeNode(context.parent.children[0])
+      }
+    }
+    const spy = jest.fn(plugin)
+    transform(ast, {
+      transforms: [spy]
+    })
+
+    expect(ast.children.length).toBe(1)
+    expect(ast.children[0]).toBe(c2)
+
+    expect(spy).toHaveBeenCalledTimes(3)
+    // should still traverse first span before removal
+    expect(spy.mock.calls[0][0]).toBe(c1)
+    // should still traverse last span
+    expect(spy.mock.calls[2][0]).toBe(c2)
+  })
+
+  test('context.removeNode (next sibling)', () => {
+    const ast = parse(`<span/><div/><span/>`)
+    const c1 = ast.children[0]
+    const d1 = ast.children[1]
+
+    const plugin: Transform = (node, context) => {
+      if (node.type === NodeTypes.ELEMENT && node.tag === 'div') {
+        context.removeNode()
+        // remove next sibling
+        context.removeNode(context.parent.children[1])
+      }
+    }
+    const spy = jest.fn(plugin)
+    transform(ast, {
+      transforms: [spy]
+    })
+
+    expect(ast.children.length).toBe(1)
+    expect(ast.children[0]).toBe(c1)
+
+    expect(spy).toHaveBeenCalledTimes(2)
+    // should still traverse first span before removal
+    expect(spy.mock.calls[0][0]).toBe(c1)
+    // should not traverse last span
+    expect(spy.mock.calls[1][0]).toBe(d1)
+  })
+
   test('onError option', () => {
     const ast = parse(`<div/>`)
     const loc = ast.children[0].loc.start
index 61fae264d1808b3e4b48c02521d51a9991a3a264..09898997bc80cf7739286465f05c207cd0d2790f 100644 (file)
@@ -20,16 +20,23 @@ export const transformIf = createDirectiveTransform(
     } else {
       // locate the adjacent v-if
       const siblings = context.parent.children
-      let i = context.childIndex
-      while (i--) {
+      const comments = []
+      let i = siblings.indexOf(node)
+      while (i-- >= -1) {
         const sibling = siblings[i]
-        if (sibling.type === NodeTypes.COMMENT) {
+        if (__DEV__ && sibling && sibling.type === NodeTypes.COMMENT) {
+          context.removeNode(sibling)
+          comments.unshift(sibling)
           continue
         }
-        if (sibling.type === NodeTypes.IF) {
+        if (sibling && sibling.type === NodeTypes.IF) {
           // move the node to the if node's branches
           context.removeNode()
-          sibling.branches.push(createIfBranch(node, dir))
+          const branch = createIfBranch(node, dir)
+          if (__DEV__ && comments.length) {
+            branch.children = [...comments, ...branch.children]
+          }
+          sibling.branches.push(branch)
         } else {
           context.onError(
             createCompilerError(
index 2fd5f2a00e70706f7157c0b9a44e04bb854305d9..54959c4009ecbd116e9399d1be523342fcd93a3e 100644 (file)
@@ -186,6 +186,10 @@ function pushNode(
   nodes: ChildNode[],
   node: ChildNode
 ): void {
+  // ignore comments in production
+  if (!__DEV__ && node.type === NodeTypes.COMMENT) {
+    return
+  }
   if (context.ignoreSpaces && node.type === NodeTypes.TEXT && node.isEmpty) {
     return
   }
index c7615e391e0fcc01f620acad5497b5b1706705a7..7db56d3b12c215e6e3ab47683351a9275aa60145 100644 (file)
@@ -26,9 +26,10 @@ export interface TransformContext extends Required<TransformOptions> {
   parent: ParentNode
   ancestors: ParentNode[]
   childIndex: number
+  currentNode: ChildNode | null
   replaceNode(node: ChildNode): void
-  removeNode(): void
-  nodeRemoved: boolean
+  removeNode(node?: ChildNode): void
+  onNodeRemoved: () => void
 }
 
 export function transform(root: RootNode, options: TransformOptions) {
@@ -48,17 +49,37 @@ function createTransformContext(
     parent: root,
     ancestors: [],
     childIndex: 0,
+    currentNode: null,
     replaceNode(node) {
-      if (__DEV__ && context.nodeRemoved) {
-        throw new Error(`node being replaced is already removed`)
+      if (__DEV__ && !context.currentNode) {
+        throw new Error(`node being replaced is already removed.`)
       }
-      context.parent.children[context.childIndex] = node
+      context.parent.children[context.childIndex] = context.currentNode = node
     },
-    removeNode() {
-      context.parent.children.splice(context.childIndex, 1)
-      context.nodeRemoved = true
+    removeNode(node) {
+      const list = context.parent.children
+      const removalIndex = node
+        ? list.indexOf(node)
+        : context.currentNode
+          ? context.childIndex
+          : -1
+      if (__DEV__ && removalIndex < 0) {
+        throw new Error(`node being removed is not a child of current parent`)
+      }
+      if (!node || node === context.currentNode) {
+        // current node removed
+        context.currentNode = null
+        context.onNodeRemoved()
+      } else {
+        // sibling node removed
+        if (context.childIndex > removalIndex) {
+          context.childIndex--
+          context.onNodeRemoved()
+        }
+      }
+      context.parent.children.splice(removalIndex, 1)
     },
-    nodeRemoved: false
+    onNodeRemoved: () => {}
   }
   return context
 }
@@ -69,14 +90,16 @@ function traverseChildren(
   ancestors: ParentNode[]
 ) {
   ancestors = ancestors.concat(parent)
-  for (let i = 0; i < parent.children.length; i++) {
+  let i = 0
+  const nodeRemoved = () => {
+    i--
+  }
+  for (; i < parent.children.length; i++) {
     context.parent = parent
     context.ancestors = ancestors
     context.childIndex = i
-    traverseNode(parent.children[i], context, ancestors)
-    if (context.nodeRemoved) {
-      i--
-    }
+    context.onNodeRemoved = nodeRemoved
+    traverseNode((context.currentNode = parent.children[i]), context, ancestors)
   }
 }
 
@@ -89,13 +112,12 @@ function traverseNode(
   const transforms = context.transforms
   for (let i = 0; i < transforms.length; i++) {
     const plugin = transforms[i]
-    context.nodeRemoved = false
     plugin(node, context)
-    if (context.nodeRemoved) {
+    if (!context.currentNode) {
       return
     } else {
       // node may have been replaced
-      node = context.parent.children[context.childIndex]
+      node = context.currentNode
     }
   }