if (klass != null && !isString(klass)) {
props.class = normalizeClass(klass)
}
- if (style != null) {
+ if (isObject(style)) {
// reactive state objects need to be cloned since they are likely to be
// mutated
if (isReactive(style) && !isArray(style)) {
+++ /dev/null
-test('ssr: escape HTML', () => {})
-describe('ssr: render props', () => {
- test('class', () => {})
+import { renderProps, renderClass, renderStyle } from '../src'
- test('style', () => {
- // only render numbers for properties that allow no unit numbers
+describe('ssr: renderProps', () => {
+ test('ignore reserved props', () => {
+ expect(
+ renderProps({
+ key: 1,
+ ref: () => {},
+ onClick: () => {}
+ })
+ ).toBe('')
})
- test('normal attrs', () => {})
+ test('normal attrs', () => {
+ expect(
+ renderProps({
+ id: 'foo',
+ title: 'bar'
+ })
+ ).toBe(` id="foo" title="bar"`)
+ })
+
+ test('escape attrs', () => {
+ expect(
+ renderProps({
+ id: '"><script'
+ })
+ ).toBe(` id=""><script"`)
+ })
+
+ test('boolean attrs', () => {
+ expect(
+ renderProps({
+ checked: true,
+ multiple: false
+ })
+ ).toBe(` checked`) // boolean attr w/ false should be ignored
+ })
+
+ test('ignore falsy values', () => {
+ expect(
+ renderProps({
+ foo: false,
+ title: null,
+ baz: undefined
+ })
+ ).toBe(` foo="false"`) // non boolean should render `false` as is
+ })
+
+ test('props to attrs', () => {
+ expect(
+ renderProps({
+ readOnly: true, // simple lower case conversion
+ htmlFor: 'foobar' // special cases
+ })
+ ).toBe(` readonly for="foobar"`)
+ })
+})
- test('boolean attrs', () => {})
+describe('ssr: renderClass', () => {
+ test('via renderProps', () => {
+ expect(
+ renderProps({
+ class: ['foo', 'bar']
+ })
+ ).toBe(` class="foo bar"`)
+ })
+
+ test('standalone', () => {
+ expect(renderClass(`foo`)).toBe(`foo`)
+ expect(renderClass([`foo`, `bar`])).toBe(`foo bar`)
+ expect(renderClass({ foo: true, bar: false })).toBe(`foo`)
+ expect(renderClass([{ foo: true, bar: false }, `baz`])).toBe(`foo baz`)
+ })
+
+ test('escape class values', () => {
+ expect(renderClass(`"><script`)).toBe(`"><script`)
+ })
+})
- test('enumerated attrs', () => {})
+describe('ssr: renderStyle', () => {
+ test('via renderProps', () => {
+ expect(
+ renderProps({
+ style: {
+ color: 'red'
+ }
+ })
+ ).toBe(` style="color:red;"`)
+ })
- test('ignore falsy values', () => {})
+ test('standalone', () => {
+ expect(renderStyle(`color:red`)).toBe(`color:red`)
+ expect(
+ renderStyle({
+ color: `red`
+ })
+ ).toBe(`color:red;`)
+ expect(
+ renderStyle([
+ { color: `red` },
+ { fontSize: `15px` } // case conversion
+ ])
+ ).toBe(`color:red;font-size:15px;`)
+ })
- test('props to attrs', () => {})
+ test('number handling', () => {
+ expect(
+ renderStyle({
+ fontSize: 15, // should be ignored since font-size requires unit
+ opacity: 0.5
+ })
+ ).toBe(`opacity:0.5;`)
+ })
- test('ignore non-renderable props', () => {})
+ test('escape inline CSS', () => {
+ expect(renderStyle(`"><script`)).toBe(`"><script`)
+ expect(
+ renderStyle({
+ color: `"><script`
+ })
+ ).toBe(`color:"><script;`)
+ })
})
--- /dev/null
+import { escapeHtml, interpolate } from '../src'
+
+test('ssr: escapeHTML', () => {
+ expect(escapeHtml(`foo`)).toBe(`foo`)
+ expect(escapeHtml(true)).toBe(`true`)
+ expect(escapeHtml(false)).toBe(`false`)
+ expect(escapeHtml(`a && b`)).toBe(`a && b`)
+ expect(escapeHtml(`"foo"`)).toBe(`"foo"`)
+ expect(escapeHtml(`'bar'`)).toBe(`'bar'`)
+ expect(escapeHtml(`<div>`)).toBe(`<div>`)
+})
+
+test('ssr: interpolate', () => {
+ expect(interpolate(0)).toBe(`0`)
+ expect(interpolate(`foo`)).toBe(`foo`)
+ expect(interpolate(`<div>`)).toBe(`<div>`)
+ // should escape interpolated values
+ expect(interpolate([1, 2, 3])).toBe(
+ escapeHtml(JSON.stringify([1, 2, 3], null, 2))
+ )
+ expect(
+ interpolate({
+ foo: 1,
+ bar: `<div>`
+ })
+ ).toBe(
+ escapeHtml(
+ JSON.stringify(
+ {
+ foo: 1,
+ bar: `<div>`
+ },
+ null,
+ 2
+ )
+ )
+ )
+})
-import { toDisplayString } from 'vue'
-import { escape } from './escape'
-
-export { escape }
-
-export function interpolate(value: unknown) {
- return escape(toDisplayString(value))
-}
-
export { renderToString, renderComponent, renderSlot } from './renderToString'
export { renderClass, renderStyle, renderProps } from './renderProps'
+export { escapeHtml, interpolate } from './ssrUtils'
-import { escape } from './escape'
+import { escapeHtml } from './ssrUtils'
import {
normalizeClass,
normalizeStyle,
isOn,
isSSRSafeAttrName,
isBooleanAttr
-} from '@vue/shared/src'
+} from '@vue/shared'
export function renderProps(
props: Record<string, unknown>,
ret += ` ${attrKey}`
}
} else if (isSSRSafeAttrName(attrKey)) {
- ret += ` ${attrKey}="${escape(value)}"`
+ ret += ` ${attrKey}="${escapeHtml(value)}"`
}
}
}
}
export function renderClass(raw: unknown): string {
- return escape(normalizeClass(raw))
+ return escapeHtml(normalizeClass(raw))
}
export function renderStyle(raw: unknown): string {
if (!raw) {
return ''
}
+ if (isString(raw)) {
+ return escapeHtml(raw)
+ }
const styles = normalizeStyle(raw)
let ret = ''
for (const key in styles) {
ret += `${normalizedKey}:${value};`
}
}
- return escape(ret)
+ return escapeHtml(ret)
}
isVoidTag
} from '@vue/shared'
import { renderProps } from './renderProps'
-import { escape } from './escape'
+import { escapeHtml } from './ssrUtils'
const {
createComponentInstance,
const instance = createComponentInstance(vnode, parentComponent)
const res = setupComponent(
instance,
- null /* parentSuspense */,
+ null /* parentSuspense (no need to track for SSR) */,
true /* isSSR */
)
if (isPromise(res)) {
push(props.innerHTML)
} else if (props.textContent) {
hasChildrenOverride = true
- push(escape(props.textContent))
+ push(escapeHtml(props.textContent))
} else if (tag === 'textarea' && props.value) {
hasChildrenOverride = true
- push(escape(props.value))
+ push(escapeHtml(props.value))
}
}
if (!hasChildrenOverride) {
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
- push(escape(children as string))
+ push(escapeHtml(children as string))
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
renderVNodeChildren(
push,
+import { toDisplayString } from '@vue/shared'
+
const escapeRE = /["'&<>]/
-export function escape(string: unknown) {
+export function escapeHtml(string: unknown) {
const str = '' + string
const match = escapeRE.exec(str)
return lastIndex !== index ? html + str.substring(lastIndex, index) : html
}
+
+export function interpolate(value: unknown) {
+ return escapeHtml(toDisplayString(value))
+}
// The full list is needed during SSR to produce the correct initial markup.
export const isBooleanAttr = /*#__PURE__*/ makeMap(
specialBooleanAttrs +
- `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,ismap,` +
- `loop,nomodule,open,required,reversed,scoped,seamless,` +
+ `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,` +
+ `loop,open,required,reversed,scoped,seamless,` +
`checked,muted,multiple,selected`
)