diff --git a/packages/compiler-core/src/errors.ts b/packages/compiler-core/src/errors.ts index 64716f7e..e9c16bfc 100644 --- a/packages/compiler-core/src/errors.ts +++ b/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 outlet.`, [ErrorCodes.X_NAMED_SLOT_ON_COMPONENT]: `Named v-slot on component. ` + diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts index 34998f73..35624b10 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -78,7 +78,8 @@ export { TransformOptions, TransformContext, NodeTransform, - StructuralDirectiveTransform + StructuralDirectiveTransform, + DirectiveTransform } from './transform' export { generate, diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index 4e8877f3..1fe68d5d 100644 --- a/packages/compiler-core/src/transform.ts +++ b/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[] diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts index 3a483adb..33ddcb60 100644 --- a/packages/compiler-core/src/transforms/transformElement.ts +++ b/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) diff --git a/packages/compiler-core/src/transforms/transformSlotOutlet.ts b/packages/compiler-core/src/transforms/transformSlotOutlet.ts index 70421c6f..1c65d317 100644 --- a/packages/compiler-core/src/transforms/transformSlotOutlet.ts +++ b/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( diff --git a/packages/compiler-core/src/transforms/vBind.ts b/packages/compiler-core/src/transforms/vBind.ts index 1d28e354..065889f0 100644 --- a/packages/compiler-core/src/transforms/vBind.ts +++ b/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) { diff --git a/packages/compiler-core/src/transforms/vOn.ts b/packages/compiler-core/src/transforms/vOn.ts index 68131746..521d37df 100644 --- a/packages/compiler-core/src/transforms/vOn.ts +++ b/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) { diff --git a/packages/compiler-dom/__tests__/transforms/vHtml.spec.ts b/packages/compiler-dom/__tests__/transforms/vHtml.spec.ts new file mode 100644 index 00000000..1e3648ca --- /dev/null +++ b/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(`
`) + 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(`
hello
`, { + 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(`
`, { + onError + }) + expect(onError.mock.calls).toMatchObject([ + [{ code: ErrorCodes.X_V_HTML_NO_EXPRESSION }] + ]) + }) +}) diff --git a/packages/compiler-dom/src/index.ts b/packages/compiler-dom/src/index.ts index 17a53a3f..fa7855fd 100644 --- a/packages/compiler-dom/src/index.ts +++ b/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 || {}) } }) diff --git a/packages/compiler-dom/src/transforms/vHtml.ts b/packages/compiler-dom/src/transforms/vHtml.ts index 70b786d1..abd2a23b 100644 --- a/packages/compiler-dom/src/transforms/vHtml.ts +++ b/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 + } +}