feat(compiler-dom): transform for v-html
This commit is contained in:
parent
5c4478b00b
commit
eadcaead37
@ -68,6 +68,8 @@ export const enum ErrorCodes {
|
|||||||
X_FOR_MALFORMED_EXPRESSION,
|
X_FOR_MALFORMED_EXPRESSION,
|
||||||
X_V_BIND_NO_EXPRESSION,
|
X_V_BIND_NO_EXPRESSION,
|
||||||
X_V_ON_NO_EXPRESSION,
|
X_V_ON_NO_EXPRESSION,
|
||||||
|
X_V_HTML_NO_EXPRESSION,
|
||||||
|
X_V_HTML_WITH_CHILDREN,
|
||||||
X_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
|
X_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
|
||||||
X_NAMED_SLOT_ON_COMPONENT,
|
X_NAMED_SLOT_ON_COMPONENT,
|
||||||
X_MIXED_SLOT_USAGE,
|
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_FOR_MALFORMED_EXPRESSION]: `v-for has invalid expression.`,
|
||||||
[ErrorCodes.X_V_BIND_NO_EXPRESSION]: `v-bind is missing 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_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_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET]: `Unexpected custom directive on <slot> outlet.`,
|
||||||
[ErrorCodes.X_NAMED_SLOT_ON_COMPONENT]:
|
[ErrorCodes.X_NAMED_SLOT_ON_COMPONENT]:
|
||||||
`Named v-slot on component. ` +
|
`Named v-slot on component. ` +
|
||||||
|
@ -78,7 +78,8 @@ export {
|
|||||||
TransformOptions,
|
TransformOptions,
|
||||||
TransformContext,
|
TransformContext,
|
||||||
NodeTransform,
|
NodeTransform,
|
||||||
StructuralDirectiveTransform
|
StructuralDirectiveTransform,
|
||||||
|
DirectiveTransform
|
||||||
} from './transform'
|
} from './transform'
|
||||||
export {
|
export {
|
||||||
generate,
|
generate,
|
||||||
|
@ -45,6 +45,7 @@ export type NodeTransform = (
|
|||||||
// It translates the raw directive into actual props for the VNode.
|
// It translates the raw directive into actual props for the VNode.
|
||||||
export type DirectiveTransform = (
|
export type DirectiveTransform = (
|
||||||
dir: DirectiveNode,
|
dir: DirectiveNode,
|
||||||
|
node: ElementNode,
|
||||||
context: TransformContext
|
context: TransformContext
|
||||||
) => {
|
) => {
|
||||||
props: Property | Property[]
|
props: Property | Property[]
|
||||||
|
@ -13,8 +13,7 @@ import {
|
|||||||
createObjectProperty,
|
createObjectProperty,
|
||||||
createSimpleExpression,
|
createSimpleExpression,
|
||||||
createObjectExpression,
|
createObjectExpression,
|
||||||
Property,
|
Property
|
||||||
SourceLocation
|
|
||||||
} from '../ast'
|
} from '../ast'
|
||||||
import { isArray, PatchFlags, PatchFlagNames } from '@vue/shared'
|
import { isArray, PatchFlags, PatchFlagNames } from '@vue/shared'
|
||||||
import { createCompilerError, ErrorCodes } from '../errors'
|
import { createCompilerError, ErrorCodes } from '../errors'
|
||||||
@ -44,7 +43,6 @@ export const transformElement: NodeTransform = (node, context) => {
|
|||||||
return () => {
|
return () => {
|
||||||
const isComponent = node.tagType === ElementTypes.COMPONENT
|
const isComponent = node.tagType === ElementTypes.COMPONENT
|
||||||
let hasProps = node.props.length > 0
|
let hasProps = node.props.length > 0
|
||||||
const hasChildren = node.children.length > 0
|
|
||||||
let patchFlag: number = 0
|
let patchFlag: number = 0
|
||||||
let runtimeDirectives: DirectiveNode[] | undefined
|
let runtimeDirectives: DirectiveNode[] | undefined
|
||||||
let dynamicPropNames: string[] | undefined
|
let dynamicPropNames: string[] | undefined
|
||||||
@ -59,12 +57,7 @@ export const transformElement: NodeTransform = (node, context) => {
|
|||||||
]
|
]
|
||||||
// props
|
// props
|
||||||
if (hasProps) {
|
if (hasProps) {
|
||||||
const propsBuildResult = buildProps(
|
const propsBuildResult = buildProps(node, context)
|
||||||
node.props,
|
|
||||||
node.loc,
|
|
||||||
context,
|
|
||||||
isComponent
|
|
||||||
)
|
|
||||||
patchFlag = propsBuildResult.patchFlag
|
patchFlag = propsBuildResult.patchFlag
|
||||||
dynamicPropNames = propsBuildResult.dynamicPropNames
|
dynamicPropNames = propsBuildResult.dynamicPropNames
|
||||||
runtimeDirectives = propsBuildResult.directives
|
runtimeDirectives = propsBuildResult.directives
|
||||||
@ -75,6 +68,7 @@ export const transformElement: NodeTransform = (node, context) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// children
|
// children
|
||||||
|
const hasChildren = node.children.length > 0
|
||||||
if (hasChildren) {
|
if (hasChildren) {
|
||||||
if (!hasProps) {
|
if (!hasProps) {
|
||||||
args.push(`null`)
|
args.push(`null`)
|
||||||
@ -162,16 +156,17 @@ export const transformElement: NodeTransform = (node, context) => {
|
|||||||
export type PropsExpression = ObjectExpression | CallExpression | ExpressionNode
|
export type PropsExpression = ObjectExpression | CallExpression | ExpressionNode
|
||||||
|
|
||||||
export function buildProps(
|
export function buildProps(
|
||||||
props: ElementNode['props'],
|
node: ElementNode,
|
||||||
elementLoc: SourceLocation,
|
|
||||||
context: TransformContext,
|
context: TransformContext,
|
||||||
isComponent: boolean = false
|
props: ElementNode['props'] = node.props
|
||||||
): {
|
): {
|
||||||
props: PropsExpression | undefined
|
props: PropsExpression | undefined
|
||||||
directives: DirectiveNode[]
|
directives: DirectiveNode[]
|
||||||
patchFlag: number
|
patchFlag: number
|
||||||
dynamicPropNames: string[]
|
dynamicPropNames: string[]
|
||||||
} {
|
} {
|
||||||
|
const elementLoc = node.loc
|
||||||
|
const isComponent = node.tagType === ElementTypes.COMPONENT
|
||||||
let properties: ObjectExpression['properties'] = []
|
let properties: ObjectExpression['properties'] = []
|
||||||
const mergeArgs: PropsExpression[] = []
|
const mergeArgs: PropsExpression[] = []
|
||||||
const runtimeDirectives: DirectiveNode[] = []
|
const runtimeDirectives: DirectiveNode[] = []
|
||||||
@ -278,7 +273,7 @@ export function buildProps(
|
|||||||
const directiveTransform = context.directiveTransforms[name]
|
const directiveTransform = context.directiveTransforms[name]
|
||||||
if (directiveTransform) {
|
if (directiveTransform) {
|
||||||
// has built-in directive transform.
|
// has built-in directive transform.
|
||||||
const { props, needRuntime } = directiveTransform(prop, context)
|
const { props, needRuntime } = directiveTransform(prop, node, context)
|
||||||
if (isArray(props)) {
|
if (isArray(props)) {
|
||||||
properties.push(...props)
|
properties.push(...props)
|
||||||
properties.forEach(analyzePatchFlag)
|
properties.forEach(analyzePatchFlag)
|
||||||
|
@ -52,9 +52,9 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
|
|||||||
let hasProps = propsWithoutName.length > 0
|
let hasProps = propsWithoutName.length > 0
|
||||||
if (hasProps) {
|
if (hasProps) {
|
||||||
const { props: propsExpression, directives } = buildProps(
|
const { props: propsExpression, directives } = buildProps(
|
||||||
propsWithoutName,
|
node,
|
||||||
loc,
|
context,
|
||||||
context
|
propsWithoutName
|
||||||
)
|
)
|
||||||
if (directives.length) {
|
if (directives.length) {
|
||||||
context.onError(
|
context.onError(
|
||||||
|
@ -7,7 +7,7 @@ import { CAMELIZE } from '../runtimeHelpers'
|
|||||||
// v-bind without arg is handled directly in ./element.ts due to it affecting
|
// 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
|
// codegen for the entire props object. This transform here is only for v-bind
|
||||||
// *with* args.
|
// *with* args.
|
||||||
export const transformBind: DirectiveTransform = (dir, context) => {
|
export const transformBind: DirectiveTransform = (dir, node, context) => {
|
||||||
const { exp, modifiers, loc } = dir
|
const { exp, modifiers, loc } = dir
|
||||||
const arg = dir.arg!
|
const arg = dir.arg!
|
||||||
if (!exp) {
|
if (!exp) {
|
||||||
|
@ -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
|
// 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
|
// codegen for the entire props object. This transform here is only for v-on
|
||||||
// *with* args.
|
// *with* args.
|
||||||
export const transformOn: DirectiveTransform = (dir, context) => {
|
export const transformOn: DirectiveTransform = (dir, node, context) => {
|
||||||
const { loc, modifiers } = dir
|
const { loc, modifiers } = dir
|
||||||
const arg = dir.arg!
|
const arg = dir.arg!
|
||||||
if (!dir.exp && !modifiers.length) {
|
if (!dir.exp && !modifiers.length) {
|
||||||
|
74
packages/compiler-dom/__tests__/transforms/vHtml.spec.ts
Normal file
74
packages/compiler-dom/__tests__/transforms/vHtml.spec.ts
Normal 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 }]
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
@ -2,6 +2,7 @@ import { baseCompile, CompilerOptions, CodegenResult } from '@vue/compiler-core'
|
|||||||
import { parserOptionsMinimal } from './parserOptionsMinimal'
|
import { parserOptionsMinimal } from './parserOptionsMinimal'
|
||||||
import { parserOptionsStandard } from './parserOptionsStandard'
|
import { parserOptionsStandard } from './parserOptionsStandard'
|
||||||
import { transformStyle } from './transforms/transformStyle'
|
import { transformStyle } from './transforms/transformStyle'
|
||||||
|
import { transformVHtml } from './transforms/vHtml'
|
||||||
|
|
||||||
export function compile(
|
export function compile(
|
||||||
template: string,
|
template: string,
|
||||||
@ -12,7 +13,7 @@ export function compile(
|
|||||||
...(__BROWSER__ ? parserOptionsMinimal : parserOptionsStandard),
|
...(__BROWSER__ ? parserOptionsMinimal : parserOptionsStandard),
|
||||||
nodeTransforms: [transformStyle, ...(options.nodeTransforms || [])],
|
nodeTransforms: [transformStyle, ...(options.nodeTransforms || [])],
|
||||||
directiveTransforms: {
|
directiveTransforms: {
|
||||||
// TODO include DOM-specific directiveTransforms
|
html: transformVHtml,
|
||||||
...(options.directiveTransforms || {})
|
...(options.directiveTransforms || {})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user