diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap
index 2cd13bab0..0ce40337c 100644
--- a/packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap
+++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap
@@ -139,6 +139,24 @@ return function render(_ctx, _cache) {
}"
`;
+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
@@ -232,6 +250,20 @@ return function render(_ctx, _cache) {
}"
`;
+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
@@ -268,6 +300,32 @@ return function render(_ctx, _cache) {
}"
`;
+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
diff --git a/packages/compiler-core/__tests__/transforms/transformText.spec.ts b/packages/compiler-core/__tests__/transforms/transformText.spec.ts
index 1a6d6916a..057250de1 100644
--- a/packages/compiler-core/__tests__/transforms/transformText.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/transformText.spec.ts
@@ -4,6 +4,7 @@ import {
type ForNode,
NodeTypes,
generate,
+ isWhitespaceText,
baseParse as parse,
transform,
} from '../../src'
@@ -109,6 +110,24 @@ describe('compiler: transform text', () => {
expect(generate(root).code).toMatchSnapshot()
})
+ test('whitespace text', () => {
+ const root = transformWithTextOpt(`
hello `)
+ 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(
`{{ foo }} bar {{ baz }}hello`,
diff --git a/packages/compiler-core/__tests__/transforms/vIf.spec.ts b/packages/compiler-core/__tests__/transforms/vIf.spec.ts
index 2c2fedab0..e2f85f283 100644
--- a/packages/compiler-core/__tests__/transforms/vIf.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/vIf.spec.ts
@@ -246,6 +246,31 @@ describe('compiler: v-if', () => {
loc: node3.loc,
},
])
+
+ const { node: node4 } = parseWithIfTransform(
+ `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(
+ `\u00a0`,
+ { 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', () => {
@@ -285,6 +310,31 @@ describe('compiler: v-if', () => {
},
])
+ const { node: node4 } = parseWithIfTransform(
+ `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(
+ `\u00a0`,
+ { onError },
+ 2,
+ )
+ expect(onError.mock.calls[4]).toMatchObject([
+ {
+ code: ErrorCodes.X_V_ELSE_NO_ADJACENT_IF,
+ loc: node5.loc,
+ },
+ ])
+
const {
node: { branches },
} = parseWithIfTransform(
@@ -293,7 +343,7 @@ describe('compiler: v-if', () => {
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,
diff --git a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts
index e0f44a064..78bd129a6 100644
--- a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts
@@ -28,8 +28,12 @@ import { createObjectMatcher } from '../testUtils'
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,
})
@@ -43,6 +47,7 @@ function parseWithSlots(template: string, options: CompilerOptions = {}) {
transformSlotOutlet,
transformElement,
trackSlotScopes,
+ ...(options.transformText ? [transformText] : []),
],
directiveTransforms: {
on: transformOn,
@@ -307,6 +312,40 @@ describe('compiler: transform component slots', () => {
expect(generate(root).code).toMatchSnapshot()
})
+ test('named slots w/ implicit default slot containing non-breaking space', () => {
+ const { root, slots } = parseWithSlots(
+ `
+ \u00a0
+ foo
+ `,
+ )
+ 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(
`
@@ -989,6 +1028,27 @@ describe('compiler: transform component slots', () => {
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
+ test('implicit default slot with non-breaking space', () => {
+ const source = `
+
+
+ Header
+
+ `
+ 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 = `
@@ -1002,5 +1062,23 @@ describe('compiler: transform component slots', () => {
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
+
+ test('named slot with v-if + v-else and comments', () => {
+ const source = `
+
+ foo
+
+
+
+ baz
+
+ `
+ const { root } = parseWithSlots(source, {
+ transformText: true,
+ whitespace: 'preserve',
+ })
+
+ expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
+ })
})
})
diff --git a/packages/compiler-core/src/parser.ts b/packages/compiler-core/src/parser.ts
index 3eb3a976f..7b85b52fc 100644
--- a/packages/compiler-core/src/parser.ts
+++ b/packages/compiler-core/src/parser.ts
@@ -40,6 +40,7 @@ import {
} from './errors'
import {
forAliasRE,
+ isAllWhitespace,
isCoreComponent,
isSimpleIdentifier,
isStaticArgOf,
@@ -880,15 +881,6 @@ function condenseWhitespace(nodes: TemplateChildNode[]): TemplateChildNode[] {
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)
diff --git a/packages/compiler-core/src/transforms/vIf.ts b/packages/compiler-core/src/transforms/vIf.ts
index 54c505407..c4333ccf4 100644
--- a/packages/compiler-core/src/transforms/vIf.ts
+++ b/packages/compiler-core/src/transforms/vIf.ts
@@ -32,7 +32,13 @@ import { processExpression } from './transformExpression'
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(
@@ -125,18 +131,11 @@ export function processIf(
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
}
diff --git a/packages/compiler-core/src/transforms/vSlot.ts b/packages/compiler-core/src/transforms/vSlot.ts
index 43296dcc9..d113cc629 100644
--- a/packages/compiler-core/src/transforms/vSlot.ts
+++ b/packages/compiler-core/src/transforms/vSlot.ts
@@ -26,9 +26,11 @@ import {
assert,
findDir,
hasScopeRef,
+ isCommentOrWhitespace,
isStaticExp,
isTemplateNode,
isVSlot,
+ isWhitespaceText,
} from '../utils'
import { CREATE_SLOTS, RENDER_LIST, WITH_CTX } from '../runtimeHelpers'
import { createForLoopParams, finalizeForParseResult } from './vFor'
@@ -222,7 +224,7 @@ export function buildSlots(
let prev
while (j--) {
prev = children[j]
- if (prev.type !== NodeTypes.COMMENT && isNonWhitespaceContent(prev)) {
+ if (!isCommentOrWhitespace(prev)) {
break
}
}
@@ -319,7 +321,7 @@ export function buildSlots(
// #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) {
@@ -411,11 +413,3 @@ function hasForwardedSlots(children: TemplateChildNode[]): boolean {
}
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)
-}
diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts
index b49d70bb2..d7a701202 100644
--- a/packages/compiler-core/src/utils.ts
+++ b/packages/compiler-core/src/utils.ts
@@ -42,6 +42,7 @@ import type { PropsExpression } from './transforms/transformElement'
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
@@ -564,3 +565,23 @@ export function getMemoedVNodeCall(
}
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)
+}
diff --git a/packages/compiler-dom/__tests__/transforms/Transition.spec.ts b/packages/compiler-dom/__tests__/transforms/Transition.spec.ts
index 8f64adb80..93039d2e4 100644
--- a/packages/compiler-dom/__tests__/transforms/Transition.spec.ts
+++ b/packages/compiler-dom/__tests__/transforms/Transition.spec.ts
@@ -135,6 +135,18 @@ describe('Transition multi children warnings', () => {
false,
)
})
+
+ test('non-breaking spaces are treated as normal text', () => {
+ checkWarning(
+ `
+
+ \u00a0
+ foo
+
+ `,
+ true,
+ )
+ })
})
test('inject persisted when child has v-show', () => {
@@ -164,3 +176,19 @@ test('the v-if/else-if/else branches in Transition should ignore comments', () =
`).code,
).toMatchSnapshot()
})
+
+test('comments and preserved whitespace are ignored', () => {
+ expect(
+ compile(
+ `
+
+
+ foo bar
+
+ `,
+ {
+ whitespace: 'preserve',
+ },
+ ).code,
+ ).toMatchSnapshot()
+})
diff --git a/packages/compiler-dom/__tests__/transforms/__snapshots__/Transition.spec.ts.snap b/packages/compiler-dom/__tests__/transforms/__snapshots__/Transition.spec.ts.snap
index aa08c1366..404ad8cd2 100644
--- a/packages/compiler-dom/__tests__/transforms/__snapshots__/Transition.spec.ts.snap
+++ b/packages/compiler-dom/__tests__/transforms/__snapshots__/Transition.spec.ts.snap
@@ -1,5 +1,22 @@
// 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
diff --git a/packages/compiler-dom/src/transforms/Transition.ts b/packages/compiler-dom/src/transforms/Transition.ts
index f6cf968e3..ccf96e8aa 100644
--- a/packages/compiler-dom/src/transforms/Transition.ts
+++ b/packages/compiler-dom/src/transforms/Transition.ts
@@ -4,6 +4,7 @@ import {
type IfBranchNode,
type NodeTransform,
NodeTypes,
+ isCommentOrWhitespace,
} from '@vue/compiler-core'
import { TRANSITION } from '../runtimeHelpers'
import { DOMErrorCodes, createDOMCompilerError } from '../errors'
@@ -56,11 +57,9 @@ export const transformTransition: NodeTransform = (node, context) => {
}
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 (