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

This commit is contained in:
Evan You 2019-10-08 15:35:57 -04:00
parent 5c4478b00b
commit eadcaead37
10 changed files with 121 additions and 21 deletions

View File

@ -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. ` +

View File

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

View File

@ -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[]

View File

@ -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)

View File

@ -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(

View File

@ -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) {

View File

@ -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) {

View File

@ -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 }]
])
})
})

View File

@ -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 || {})
}
})

View File

@ -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
}
}