Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: v-html and v-text can only be used on elements #12518

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion packages/compiler-dom/__tests__/transforms/vHtml.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import {
} from '@vue/compiler-core'
import { transformVHtml } from '../../src/transforms/vHtml'
import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
import { transformSlotOutlet } from '../../../compiler-core/src/transforms/transformSlotOutlet'
import { createObjectMatcher } from '../../../compiler-core/__tests__/testUtils'
import { PatchFlags } from '@vue/shared'
import { DOMErrorCodes } from '../../src/errors'

function transformWithVHtml(template: string, options: CompilerOptions = {}) {
const ast = parse(template)
transform(ast, {
nodeTransforms: [transformElement],
nodeTransforms: [transformElement, transformSlotOutlet],
directiveTransforms: {
html: transformVHtml,
},
Expand Down Expand Up @@ -64,4 +65,24 @@ describe('compiler: v-html transform', () => {
[{ code: DOMErrorCodes.X_V_HTML_NO_EXPRESSION }],
])
})

it('should raise error if uses on component', () => {
const onError = vi.fn()
transformWithVHtml(`<Comp v-html="'<div>foo</div>'"></Comp>`, {
onError,
})
expect(onError.mock.calls).toMatchObject([
[{ code: DOMErrorCodes.X_V_HTML_ON_INVALID_ELEMENT }],
])
})

it('should raise error if uses on slot', () => {
const onError = vi.fn()
transformWithVHtml(`<slot v-html="'<div>foo</div>'"></slot>`, {
onError,
})
expect(onError.mock.calls).toMatchObject([
[{ code: DOMErrorCodes.X_V_HTML_ON_INVALID_ELEMENT }],
])
})
})
23 changes: 22 additions & 1 deletion packages/compiler-dom/__tests__/transforms/vText.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import {
} from '@vue/compiler-core'
import { transformVText } from '../../src/transforms/vText'
import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
import { transformSlotOutlet } from '../../../compiler-core/src/transforms/transformSlotOutlet'
import { createObjectMatcher } from '../../../compiler-core/__tests__/testUtils'
import { PatchFlags } from '@vue/shared'
import { DOMErrorCodes } from '../../src/errors'

function transformWithVText(template: string, options: CompilerOptions = {}) {
const ast = parse(template)
transform(ast, {
nodeTransforms: [transformElement],
nodeTransforms: [transformElement, transformSlotOutlet],
directiveTransforms: {
text: transformVText,
},
Expand Down Expand Up @@ -68,4 +69,24 @@ describe('compiler: v-text transform', () => {
[{ code: DOMErrorCodes.X_V_TEXT_NO_EXPRESSION }],
])
})

it('should raise error if uses on component', () => {
const onError = vi.fn()
transformWithVText(`<Comp v-text="xxxxx'"></Comp>`, {
onError,
})
expect(onError.mock.calls).toMatchObject([
[{ code: DOMErrorCodes.X_V_TEXT_ON_INVALID_ELEMENT }],
])
})

it('should raise error if uses on slot', () => {
const onError = vi.fn()
transformWithVText(`<slot v-text="xxxxx"></slot>`, {
onError,
})
expect(onError.mock.calls).toMatchObject([
[{ code: DOMErrorCodes.X_V_TEXT_ON_INVALID_ELEMENT }],
])
})
})
4 changes: 4 additions & 0 deletions packages/compiler-dom/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ export function createDOMCompilerError(
export enum DOMErrorCodes {
X_V_HTML_NO_EXPRESSION = 53 /* ErrorCodes.__EXTEND_POINT__ */,
X_V_HTML_WITH_CHILDREN,
X_V_HTML_ON_INVALID_ELEMENT,
X_V_TEXT_NO_EXPRESSION,
X_V_TEXT_WITH_CHILDREN,
X_V_TEXT_ON_INVALID_ELEMENT,
X_V_MODEL_ON_INVALID_ELEMENT,
X_V_MODEL_ARG_ON_ELEMENT,
X_V_MODEL_ON_FILE_INPUT_ELEMENT,
Expand All @@ -51,8 +53,10 @@ if (__TEST__) {
export const DOMErrorMessages: { [code: number]: string } = {
[DOMErrorCodes.X_V_HTML_NO_EXPRESSION]: `v-html is missing expression.`,
[DOMErrorCodes.X_V_HTML_WITH_CHILDREN]: `v-html will override element children.`,
[DOMErrorCodes.X_V_HTML_ON_INVALID_ELEMENT]: `v-html can only be used on elements.`,
[DOMErrorCodes.X_V_TEXT_NO_EXPRESSION]: `v-text is missing expression.`,
[DOMErrorCodes.X_V_TEXT_WITH_CHILDREN]: `v-text will override element children.`,
[DOMErrorCodes.X_V_TEXT_ON_INVALID_ELEMENT]: `v-text can only be used on elements.`,
[DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT]: `v-model can only be used on <input>, <textarea> and <select> elements.`,
[DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT]: `v-model argument is not supported on plain elements.`,
[DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT]: `v-model cannot be used on file inputs since they are read-only. Use a v-on:change listener instead.`,
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler-dom/src/runtimeHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const V_ON_WITH_KEYS: unique symbol = Symbol(
)

export const V_SHOW: unique symbol = Symbol(__DEV__ ? `vShow` : ``)
export const V_HTML: unique symbol = Symbol(__DEV__ ? `vHtml` : ``)

export const TRANSITION: unique symbol = Symbol(__DEV__ ? `Transition` : ``)
export const TRANSITION_GROUP: unique symbol = Symbol(
Expand All @@ -35,6 +36,7 @@ registerRuntimeHelpers({
[V_ON_WITH_MODIFIERS]: `withModifiers`,
[V_ON_WITH_KEYS]: `withKeys`,
[V_SHOW]: `vShow`,
[V_HTML]: `vHtml`,
[TRANSITION]: `Transition`,
[TRANSITION_GROUP]: `TransitionGroup`,
})
25 changes: 25 additions & 0 deletions packages/compiler-dom/src/transforms/vHtml.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,37 @@
import {
type ComponentNode,
type DirectiveTransform,
ElementTypes,
RESOLVE_DYNAMIC_COMPONENT,
createObjectProperty,
createSimpleExpression,
resolveComponentType,
} from '@vue/compiler-core'
import { DOMErrorCodes, createDOMCompilerError } from '../errors'
import { isObject } from '@vue/shared'
import { V_HTML } from '../runtimeHelpers'

export const transformVHtml: DirectiveTransform = (dir, node, context) => {
const { exp, loc } = dir
const { tag } = node
const isComponent = node.tagType === ElementTypes.COMPONENT
let vnodeTag = isComponent
? resolveComponentType(node as ComponentNode, context)
: `"${tag}"`

const isDynamicComponent =
isObject(vnodeTag) && vnodeTag.callee === RESOLVE_DYNAMIC_COMPONENT
if (isDynamicComponent) {
return {
props: [],
needRuntime: context.helper(V_HTML),
}
}
if (node.tagType !== ElementTypes.ELEMENT) {
context.onError(
createDOMCompilerError(DOMErrorCodes.X_V_HTML_ON_INVALID_ELEMENT, loc),
)
}
if (!exp) {
context.onError(
createDOMCompilerError(DOMErrorCodes.X_V_HTML_NO_EXPRESSION, loc),
Expand Down
6 changes: 6 additions & 0 deletions packages/compiler-dom/src/transforms/vText.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
type DirectiveTransform,
ElementTypes,
TO_DISPLAY_STRING,
createCallExpression,
createObjectProperty,
Expand All @@ -10,6 +11,11 @@ import { DOMErrorCodes, createDOMCompilerError } from '../errors'

export const transformVText: DirectiveTransform = (dir, node, context) => {
const { exp, loc } = dir
if (node.tagType !== ElementTypes.ELEMENT) {
context.onError(
createDOMCompilerError(DOMErrorCodes.X_V_TEXT_ON_INVALID_ELEMENT, loc),
)
}
if (!exp) {
context.onError(
createDOMCompilerError(DOMErrorCodes.X_V_TEXT_NO_EXPRESSION, loc),
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler-ssr/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function createSSRCompilerError(
}

export enum SSRErrorCodes {
X_SSR_UNSAFE_ATTR_NAME = 65 /* DOMErrorCodes.__EXTEND_POINT__ */,
X_SSR_UNSAFE_ATTR_NAME = 67 /* DOMErrorCodes.__EXTEND_POINT__ */,
X_SSR_NO_TELEPORT_TARGET,
X_SSR_INVALID_AST_NODE,
}
Expand Down
18 changes: 17 additions & 1 deletion packages/runtime-core/src/directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ return withDirectives(h(comp), [
*/

import type { VNode } from './vnode'
import { EMPTY_OBJ, isBuiltInDirective, isFunction } from '@vue/shared'
import {
EMPTY_OBJ,
ShapeFlags,
isBuiltInDirective,
isFunction,
} from '@vue/shared'
import { warn } from './warning'
import {
type ComponentInternalInstance,
Expand Down Expand Up @@ -127,6 +132,12 @@ export type DirectiveArguments = Array<
| [Directive | undefined, any, string | undefined, DirectiveModifiers]
>

function isComponent(vnode: VNode): boolean {
const { shapeFlag } = vnode
if (shapeFlag & ShapeFlags.COMPONENT) return true
return false
}

/**
* Adds directives to a VNode.
*/
Expand All @@ -143,6 +154,11 @@ export function withDirectives<T extends VNode>(
for (let i = 0; i < directives.length; i++) {
let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]
if (dir) {
// @ts-expect-error check v-html
if (isComponent(vnode) && dir.name === '__v-html') {
warn(`v-html can only be used on elements.`, vnode)
continue
}
if (isFunction(dir)) {
dir = {
mounted: dir,
Expand Down
23 changes: 23 additions & 0 deletions packages/runtime-dom/src/directives/vHtml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { ObjectDirective } from '@vue/runtime-core'

export type VHtmlElementDirective = ObjectDirective<HTMLElement> & {
name: '__v-html'
}
// only work with resolveDynamicComponent
export const vHtml: VHtmlElementDirective = {
name: '__v-html',
beforeMount(el, { value }) {
setInnerHTML(el, value)
},
updated(el, { value, oldValue }) {
if (!value === !oldValue) return
setInnerHTML(el, value)
},
beforeUnmount(el, { value }) {
setInnerHTML(el, value)
},
}

function setInnerHTML(el: HTMLElement, value: unknown): void {
el.innerHTML = String(value)
}
1 change: 1 addition & 0 deletions packages/runtime-dom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ export {
} from './directives/vModel'
export { withModifiers, withKeys } from './directives/vOn'
export { vShow } from './directives/vShow'
export { vHtml } from './directives/vHtml'

import { initVModelForSSR } from './directives/vModel'
import { initVShowForSSR } from './directives/vShow'
Expand Down
Loading