}"
`;
+exports[`compiler: transform component slots > named slots w/ implicit default slot containing non-breaking space 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = _Vue
+
+ const _component_Comp = _resolveComponent("Comp")
+
+ return (_openBlock(), _createBlock(_component_Comp, null, {
+ one: _withCtx(() => ["foo"]),
+ default: _withCtx(() => [" "]),
+ _: 1 /* STABLE */
+ }))
+ }
+}"
+`;
+
exports[`compiler: transform component slots > nested slots scoping 1`] = `
"const { toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = Vue
}"
`;
+exports[`compiler: transform component slots > with whitespace: 'preserve' > implicit default slot with non-breaking space 1`] = `
+"const { resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = Vue
+
+return function render(_ctx, _cache) {
+ const _component_Comp = _resolveComponent("Comp")
+
+ return (_openBlock(), _createBlock(_component_Comp, null, {
+ header: _withCtx(() => [" Header "]),
+ default: _withCtx(() => ["\\n \\n "]),
+ _: 1 /* STABLE */
+ }))
+}"
+`;
+
exports[`compiler: transform component slots > with whitespace: 'preserve' > named default slot + implicit whitespace content 1`] = `
"const { resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = Vue
}"
`;
+exports[`compiler: transform component slots > with whitespace: 'preserve' > named slot with v-if + v-else and comments 1`] = `
+"const { createTextVNode: _createTextVNode, createCommentVNode: _createCommentVNode, resolveComponent: _resolveComponent, withCtx: _withCtx, createSlots: _createSlots, openBlock: _openBlock, createBlock: _createBlock } = Vue
+
+return function render(_ctx, _cache) {
+ const _component_Comp = _resolveComponent("Comp")
+
+ return (_openBlock(), _createBlock(_component_Comp, null, _createSlots({ _: 2 /* DYNAMIC */ }, [
+ ok
+ ? {
+ name: "one",
+ fn: _withCtx(() => [
+ _createTextVNode("foo")
+ ]),
+ key: "0"
+ }
+ : {
+ name: "two",
+ fn: _withCtx(() => [
+ _createTextVNode("baz")
+ ]),
+ key: "1"
+ }
+ ]), 1024 /* DYNAMIC_SLOTS */))
+}"
+`;
+
exports[`compiler: transform component slots > with whitespace: 'preserve' > should not generate whitespace only default slot 1`] = `
"const { resolveComponent: _resolveComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = Vue
type ForNode,
NodeTypes,
generate,
+ isWhitespaceText,
baseParse as parse,
transform,
} from '../../src'
expect(generate(root).code).toMatchSnapshot()
})
+ test('whitespace text', () => {
+ const root = transformWithTextOpt(`<div/>hello<div/> <div/>`)
+ expect(root.children.length).toBe(5)
+ expect(root.children[0].type).toBe(NodeTypes.ELEMENT)
+ expect(root.children[1].type).toBe(NodeTypes.TEXT_CALL)
+ expect(root.children[2].type).toBe(NodeTypes.ELEMENT)
+ expect(root.children[3].type).toBe(NodeTypes.TEXT_CALL)
+ expect(root.children[4].type).toBe(NodeTypes.ELEMENT)
+
+ expect(root.children.map(isWhitespaceText)).toEqual([
+ false,
+ false,
+ false,
+ true,
+ false,
+ ])
+ })
+
test('consecutive text mixed with elements', () => {
const root = transformWithTextOpt(
`<div/>{{ foo }} bar {{ baz }}<div/>hello<div/>`,
loc: node3.loc,
},
])
+
+ const { node: node4 } = parseWithIfTransform(
+ `<div v-if="bar"/>foo<div v-else/>`,
+ { onError },
+ 2,
+ )
+ expect(onError.mock.calls[3]).toMatchObject([
+ {
+ code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF,
+ loc: node4.loc,
+ },
+ ])
+
+ // Non-breaking space
+ const { node: node5 } = parseWithIfTransform(
+ `<div v-if="bar"/>\u00a0<div v-else/>`,
+ { onError },
+ 2,
+ )
+ expect(onError.mock.calls[4]).toMatchObject([
+ {
+ code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF,
+ loc: node5.loc,
+ },
+ ])
})
test('error on v-else-if missing adjacent v-if or v-else-if', () => {
},
])
+ const { node: node4 } = parseWithIfTransform(
+ `<div v-if="bar"/>foo<div v-else-if="foo"/>`,
+ { onError },
+ 2,
+ )
+ expect(onError.mock.calls[3]).toMatchObject([
+ {
+ code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF,
+ loc: node4.loc,
+ },
+ ])
+
+ // Non-breaking space
+ const { node: node5 } = parseWithIfTransform(
+ `<div v-if="bar"/>\u00a0<div v-else-if="foo"/>`,
+ { onError },
+ 2,
+ )
+ expect(onError.mock.calls[4]).toMatchObject([
+ {
+ code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF,
+ loc: node5.loc,
+ },
+ ])
+
const {
node: { branches },
} = parseWithIfTransform(
0,
)
- expect(onError.mock.calls[3]).toMatchObject([
+ expect(onError.mock.calls[5]).toMatchObject([
{
code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF,
loc: branches[branches.length - 1].loc,
import { PatchFlags } from '@vue/shared'
import { transformFor } from '../../src/transforms/vFor'
import { transformIf } from '../../src/transforms/vIf'
+import { transformText } from '../../src/transforms/transformText'
-function parseWithSlots(template: string, options: CompilerOptions = {}) {
+function parseWithSlots(
+ template: string,
+ options: CompilerOptions & { transformText?: boolean } = {},
+) {
const ast = parse(template, {
whitespace: options.whitespace,
})
transformSlotOutlet,
transformElement,
trackSlotScopes,
+ ...(options.transformText ? [transformText] : []),
],
directiveTransforms: {
on: transformOn,
expect(generate(root).code).toMatchSnapshot()
})
+ test('named slots w/ implicit default slot containing non-breaking space', () => {
+ const { root, slots } = parseWithSlots(
+ `<Comp>
+ \u00a0
+ <template #one>foo</template>
+ </Comp>`,
+ )
+ expect(slots).toMatchObject(
+ createSlotMatcher({
+ one: {
+ type: NodeTypes.JS_FUNCTION_EXPRESSION,
+ params: undefined,
+ returns: [
+ {
+ type: NodeTypes.TEXT,
+ content: `foo`,
+ },
+ ],
+ },
+ default: {
+ type: NodeTypes.JS_FUNCTION_EXPRESSION,
+ params: undefined,
+ returns: [
+ {
+ type: NodeTypes.TEXT,
+ content: ` \u00a0 `,
+ },
+ ],
+ },
+ }),
+ )
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
test('dynamically named slots', () => {
const { root, slots } = parseWithSlots(
`<Comp>
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
+ test('implicit default slot with non-breaking space', () => {
+ const source = `
+ <Comp>
+
+ <template #header> Header </template>
+ </Comp>
+ `
+ const { root } = parseWithSlots(source, {
+ whitespace: 'preserve',
+ })
+
+ const slots = (root as any).children[0].codegenNode.children
+ .properties as ObjectExpression['properties']
+
+ expect(
+ slots.some(p => (p.key as SimpleExpressionNode).content === 'default'),
+ ).toBe(true)
+
+ expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
+ })
+
test('named slot with v-if + v-else', () => {
const source = `
<Comp>
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
+
+ test('named slot with v-if + v-else and comments', () => {
+ const source = `
+ <Comp>
+ <template #one v-if="ok">foo</template>
+ <!-- start -->
+
+ <!-- end -->
+ <template #two v-else>baz</template>
+ </Comp>
+ `
+ const { root } = parseWithSlots(source, {
+ transformText: true,
+ whitespace: 'preserve',
+ })
+
+ expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
+ })
})
})
} from './errors'
import {
forAliasRE,
+ isAllWhitespace,
isCoreComponent,
isSimpleIdentifier,
isStaticArgOf,
return removedWhitespace ? nodes.filter(Boolean) : nodes
}
-function isAllWhitespace(str: string) {
- for (let i = 0; i < str.length; i++) {
- if (!isWhitespace(str.charCodeAt(i))) {
- return false
- }
- }
- return true
-}
-
function hasNewlineChar(str: string) {
for (let i = 0; i < str.length; i++) {
const c = str.charCodeAt(i)
import { validateBrowserExpression } from '../validateExpression'
import { cloneLoc } from '../parser'
import { CREATE_COMMENT, FRAGMENT } from '../runtimeHelpers'
-import { findDir, findProp, getMemoedVNodeCall, injectProp } from '../utils'
+import {
+ findDir,
+ findProp,
+ getMemoedVNodeCall,
+ injectProp,
+ isCommentOrWhitespace,
+} from '../utils'
import { PatchFlags } from '@vue/shared'
export const transformIf: NodeTransform = createStructuralDirectiveTransform(
let i = siblings.indexOf(node)
while (i-- >= -1) {
const sibling = siblings[i]
- if (sibling && sibling.type === NodeTypes.COMMENT) {
- context.removeNode(sibling)
- __DEV__ && comments.unshift(sibling)
- continue
- }
-
- if (
- sibling &&
- sibling.type === NodeTypes.TEXT &&
- !sibling.content.trim().length
- ) {
+ if (sibling && isCommentOrWhitespace(sibling)) {
context.removeNode(sibling)
+ if (__DEV__ && sibling.type === NodeTypes.COMMENT) {
+ comments.unshift(sibling)
+ }
continue
}
assert,
findDir,
hasScopeRef,
+ isCommentOrWhitespace,
isStaticExp,
isTemplateNode,
isVSlot,
+ isWhitespaceText,
} from '../utils'
import { CREATE_SLOTS, RENDER_LIST, WITH_CTX } from '../runtimeHelpers'
import { createForLoopParams, finalizeForParseResult } from './vFor'
let prev
while (j--) {
prev = children[j]
- if (prev.type !== NodeTypes.COMMENT && isNonWhitespaceContent(prev)) {
+ if (!isCommentOrWhitespace(prev)) {
break
}
}
// #3766
// with whitespace: 'preserve', whitespaces between slots will end up in
// implicitDefaultChildren. Ignore if all implicit children are whitespaces.
- implicitDefaultChildren.some(node => isNonWhitespaceContent(node))
+ !implicitDefaultChildren.every(isWhitespaceText)
) {
// implicit default slot (mixed with named slots)
if (hasNamedDefaultSlot) {
}
return false
}
-
-function isNonWhitespaceContent(node: TemplateChildNode): boolean {
- if (node.type !== NodeTypes.TEXT && node.type !== NodeTypes.TEXT_CALL)
- return true
- return node.type === NodeTypes.TEXT
- ? !!node.content.trim()
- : isNonWhitespaceContent(node.content)
-}
import { parseExpression } from '@babel/parser'
import type { Expression, Node } from '@babel/types'
import { unwrapTSNode } from './babelUtils'
+import { isWhitespace } from './tokenizer'
export const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStatic
}
export const forAliasRE: RegExp = /([\s\S]*?)\s+(?:in|of)\s+(\S[\s\S]*)/
+
+export function isAllWhitespace(str: string): boolean {
+ for (let i = 0; i < str.length; i++) {
+ if (!isWhitespace(str.charCodeAt(i))) {
+ return false
+ }
+ }
+ return true
+}
+
+export function isWhitespaceText(node: TemplateChildNode): boolean {
+ return (
+ (node.type === NodeTypes.TEXT && isAllWhitespace(node.content)) ||
+ (node.type === NodeTypes.TEXT_CALL && isWhitespaceText(node.content))
+ )
+}
+
+export function isCommentOrWhitespace(node: TemplateChildNode): boolean {
+ return node.type === NodeTypes.COMMENT || isWhitespaceText(node)
+}
false,
)
})
+
+ test('non-breaking spaces are treated as normal text', () => {
+ checkWarning(
+ `
+ <transition>
+ \u00a0
+ <div>foo</div>
+ </transition>
+ `,
+ true,
+ )
+ })
})
test('inject persisted when child has v-show', () => {
`).code,
).toMatchSnapshot()
})
+
+test('comments and preserved whitespace are ignored', () => {
+ expect(
+ compile(
+ `
+ <transition>
+ <!-- foo --> <!-- bar -->
+ <div>foo bar</div>
+ </transition>
+ `,
+ {
+ whitespace: 'preserve',
+ },
+ ).code,
+ ).toMatchSnapshot()
+})
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+exports[`comments and preserved whitespace are ignored 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, Transition: _Transition, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock } = _Vue
+
+ return (_openBlock(), _createBlock(_Transition, null, {
+ default: _withCtx(() => [
+ _createElementVNode("div", null, "foo bar")
+ ]),
+ _: 1 /* STABLE */
+ }))
+ }
+}"
+`;
+
exports[`inject persisted when child has v-show 1`] = `
"const _Vue = Vue
type IfBranchNode,
type NodeTransform,
NodeTypes,
+ isCommentOrWhitespace,
} from '@vue/compiler-core'
import { TRANSITION } from '../runtimeHelpers'
import { DOMErrorCodes, createDOMCompilerError } from '../errors'
}
function hasMultipleChildren(node: ComponentNode | IfBranchNode): boolean {
- // #1352 filter out potential comment nodes.
+ // filter out potential comment nodes (#1352) and whitespace (#4637)
const children = (node.children = node.children.filter(
- c =>
- c.type !== NodeTypes.COMMENT &&
- !(c.type === NodeTypes.TEXT && !c.content.trim()),
+ c => !isCommentOrWhitespace(c),
))
const child = children[0]
return (