Explorar el Código

feat(compiler-dom): transform for v-html

Evan You hace 6 años
padre
commit
eadcaead37

+ 4 - 0
packages/compiler-core/src/errors.ts

@@ -68,6 +68,8 @@ export const enum ErrorCodes {
   X_FOR_MALFORMED_EXPRESSION,
   X_V_BIND_NO_EXPRESSION,
   X_V_ON_NO_EXPRESSION,
+  X_V_HTML_NO_EXPRESSION,
+  X_V_HTML_WITH_CHILDREN,
   X_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
   X_NAMED_SLOT_ON_COMPONENT,
   X_MIXED_SLOT_USAGE,
@@ -144,6 +146,8 @@ export const errorMessages: { [code: number]: string } = {
   [ErrorCodes.X_FOR_MALFORMED_EXPRESSION]: `v-for has invalid expression.`,
   [ErrorCodes.X_V_BIND_NO_EXPRESSION]: `v-bind is missing expression.`,
   [ErrorCodes.X_V_ON_NO_EXPRESSION]: `v-on is missing expression.`,
+  [ErrorCodes.X_V_HTML_NO_EXPRESSION]: `v-html is missing epxression.`,
+  [ErrorCodes.X_V_HTML_WITH_CHILDREN]: `v-html will override element children.`,
   [ErrorCodes.X_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET]: `Unexpected custom directive on <slot> outlet.`,
   [ErrorCodes.X_NAMED_SLOT_ON_COMPONENT]:
     `Named v-slot on component. ` +

+ 2 - 1
packages/compiler-core/src/index.ts

@@ -78,7 +78,8 @@ export {
   TransformOptions,
   TransformContext,
   NodeTransform,
-  StructuralDirectiveTransform
+  StructuralDirectiveTransform,
+  DirectiveTransform
 } from './transform'
 export {
   generate,

+ 1 - 0
packages/compiler-core/src/transform.ts

@@ -45,6 +45,7 @@ export type NodeTransform = (
 //   It translates the raw directive into actual props for the VNode.
 export type DirectiveTransform = (
   dir: DirectiveNode,
+  node: ElementNode,
   context: TransformContext
 ) => {
   props: Property | Property[]

+ 8 - 13
packages/compiler-core/src/transforms/transformElement.ts

@@ -13,8 +13,7 @@ import {
   createObjectProperty,
   createSimpleExpression,
   createObjectExpression,
-  Property,
-  SourceLocation
+  Property
 } from '../ast'
 import { isArray, PatchFlags, PatchFlagNames } from '@vue/shared'
 import { createCompilerError, ErrorCodes } from '../errors'
@@ -44,7 +43,6 @@ export const transformElement: NodeTransform = (node, context) => {
       return () => {
         const isComponent = node.tagType === ElementTypes.COMPONENT
         let hasProps = node.props.length > 0
-        const hasChildren = node.children.length > 0
         let patchFlag: number = 0
         let runtimeDirectives: DirectiveNode[] | undefined
         let dynamicPropNames: string[] | undefined
@@ -59,12 +57,7 @@ export const transformElement: NodeTransform = (node, context) => {
         ]
         // props
         if (hasProps) {
-          const propsBuildResult = buildProps(
-            node.props,
-            node.loc,
-            context,
-            isComponent
-          )
+          const propsBuildResult = buildProps(node, context)
           patchFlag = propsBuildResult.patchFlag
           dynamicPropNames = propsBuildResult.dynamicPropNames
           runtimeDirectives = propsBuildResult.directives
@@ -75,6 +68,7 @@ export const transformElement: NodeTransform = (node, context) => {
           }
         }
         // children
+        const hasChildren = node.children.length > 0
         if (hasChildren) {
           if (!hasProps) {
             args.push(`null`)
@@ -162,16 +156,17 @@ export const transformElement: NodeTransform = (node, context) => {
 export type PropsExpression = ObjectExpression | CallExpression | ExpressionNode
 
 export function buildProps(
-  props: ElementNode['props'],
-  elementLoc: SourceLocation,
+  node: ElementNode,
   context: TransformContext,
-  isComponent: boolean = false
+  props: ElementNode['props'] = node.props
 ): {
   props: PropsExpression | undefined
   directives: DirectiveNode[]
   patchFlag: number
   dynamicPropNames: string[]
 } {
+  const elementLoc = node.loc
+  const isComponent = node.tagType === ElementTypes.COMPONENT
   let properties: ObjectExpression['properties'] = []
   const mergeArgs: PropsExpression[] = []
   const runtimeDirectives: DirectiveNode[] = []
@@ -278,7 +273,7 @@ export function buildProps(
       const directiveTransform = context.directiveTransforms[name]
       if (directiveTransform) {
         // has built-in directive transform.
-        const { props, needRuntime } = directiveTransform(prop, context)
+        const { props, needRuntime } = directiveTransform(prop, node, context)
         if (isArray(props)) {
           properties.push(...props)
           properties.forEach(analyzePatchFlag)

+ 3 - 3
packages/compiler-core/src/transforms/transformSlotOutlet.ts

@@ -52,9 +52,9 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
     let hasProps = propsWithoutName.length > 0
     if (hasProps) {
       const { props: propsExpression, directives } = buildProps(
-        propsWithoutName,
-        loc,
-        context
+        node,
+        context,
+        propsWithoutName
       )
       if (directives.length) {
         context.onError(

+ 1 - 1
packages/compiler-core/src/transforms/vBind.ts

@@ -7,7 +7,7 @@ import { CAMELIZE } from '../runtimeHelpers'
 // v-bind without arg is handled directly in ./element.ts due to it affecting
 // codegen for the entire props object. This transform here is only for v-bind
 // *with* args.
-export const transformBind: DirectiveTransform = (dir, context) => {
+export const transformBind: DirectiveTransform = (dir, node, context) => {
   const { exp, modifiers, loc } = dir
   const arg = dir.arg!
   if (!exp) {

+ 1 - 1
packages/compiler-core/src/transforms/vOn.ts

@@ -17,7 +17,7 @@ const simplePathRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]
 // v-on without arg is handled directly in ./element.ts due to it affecting
 // codegen for the entire props object. This transform here is only for v-on
 // *with* args.
-export const transformOn: DirectiveTransform = (dir, context) => {
+export const transformOn: DirectiveTransform = (dir, node, context) => {
   const { loc, modifiers } = dir
   const arg = dir.arg!
   if (!dir.exp && !modifiers.length) {

+ 74 - 0
packages/compiler-dom/__tests__/transforms/vHtml.spec.ts

@@ -0,0 +1,74 @@
+import {
+  parse,
+  transform,
+  PlainElementNode,
+  CompilerOptions,
+  ErrorCodes
+} from '@vue/compiler-core'
+import { transformVHtml } from '../../src/transforms/vHtml'
+import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
+import {
+  createObjectMatcher,
+  genFlagText
+} from '../../../compiler-core/__tests__/testUtils'
+import { PatchFlags } from '@vue/shared'
+
+function transformWithVHtml(template: string, options: CompilerOptions = {}) {
+  const ast = parse(template)
+  transform(ast, {
+    nodeTransforms: [transformElement],
+    directiveTransforms: {
+      html: transformVHtml
+    },
+    ...options
+  })
+  return ast
+}
+
+describe('compiler: v-html transform', () => {
+  it('should convert v-html to innerHTML', () => {
+    const ast = transformWithVHtml(`<div v-html="test"/>`)
+    expect((ast.children[0] as PlainElementNode).codegenNode).toMatchObject({
+      arguments: [
+        `"div"`,
+        createObjectMatcher({
+          innerHTML: `[test]`
+        }),
+        `null`,
+        genFlagText(PatchFlags.PROPS),
+        `["innerHTML"]`
+      ]
+    })
+  })
+
+  it('should raise error and ignore children when v-html is present', () => {
+    const onError = jest.fn()
+    const ast = transformWithVHtml(`<div v-html="test">hello</div>`, {
+      onError
+    })
+    expect(onError.mock.calls).toMatchObject([
+      [{ code: ErrorCodes.X_V_HTML_WITH_CHILDREN }]
+    ])
+    expect((ast.children[0] as PlainElementNode).codegenNode).toMatchObject({
+      arguments: [
+        `"div"`,
+        createObjectMatcher({
+          innerHTML: `[test]`
+        }),
+        `null`, // <-- children should have been removed
+        genFlagText(PatchFlags.PROPS),
+        `["innerHTML"]`
+      ]
+    })
+  })
+
+  it('should raise error if has no expression', () => {
+    const onError = jest.fn()
+    transformWithVHtml(`<div v-html></div>`, {
+      onError
+    })
+    expect(onError.mock.calls).toMatchObject([
+      [{ code: ErrorCodes.X_V_HTML_NO_EXPRESSION }]
+    ])
+  })
+})

+ 2 - 1
packages/compiler-dom/src/index.ts

@@ -2,6 +2,7 @@ import { baseCompile, CompilerOptions, CodegenResult } from '@vue/compiler-core'
 import { parserOptionsMinimal } from './parserOptionsMinimal'
 import { parserOptionsStandard } from './parserOptionsStandard'
 import { transformStyle } from './transforms/transformStyle'
+import { transformVHtml } from './transforms/vHtml'
 
 export function compile(
   template: string,
@@ -12,7 +13,7 @@ export function compile(
     ...(__BROWSER__ ? parserOptionsMinimal : parserOptionsStandard),
     nodeTransforms: [transformStyle, ...(options.nodeTransforms || [])],
     directiveTransforms: {
-      // TODO include DOM-specific directiveTransforms
+      html: transformVHtml,
       ...(options.directiveTransforms || {})
     }
   })

+ 25 - 1
packages/compiler-dom/src/transforms/vHtml.ts

@@ -1 +1,25 @@
-// TODO
+import {
+  DirectiveTransform,
+  createCompilerError,
+  ErrorCodes,
+  createObjectProperty,
+  createSimpleExpression
+} from '@vue/compiler-core'
+
+export const transformVHtml: DirectiveTransform = (dir, node, context) => {
+  const { exp, loc } = dir
+  if (!exp) {
+    context.onError(createCompilerError(ErrorCodes.X_V_HTML_NO_EXPRESSION, loc))
+  }
+  if (node.children.length) {
+    context.onError(createCompilerError(ErrorCodes.X_V_HTML_WITH_CHILDREN, loc))
+    node.children.length = 0
+  }
+  return {
+    props: createObjectProperty(
+      createSimpleExpression(`innerHTML`, true, loc),
+      exp || createSimpleExpression('', true)
+    ),
+    needRuntime: false
+  }
+}