diff --git a/packages/compiler-core/__tests__/parse.spec.ts b/packages/compiler-core/__tests__/parse.spec.ts
index cdc2b09fd48..63d264eb7b0 100644
--- a/packages/compiler-core/__tests__/parse.spec.ts
+++ b/packages/compiler-core/__tests__/parse.spec.ts
@@ -2419,9 +2419,14 @@ describe('compiler: parse', () => {
...options,
})
- test('should still remove whitespaces at start/end inside an element', () => {
+ test('should preserve whitespaces at start/end inside a non-root element', () => {
const ast = parse(`
`)
- expect((ast.children[0] as ElementNode).children.length).toBe(1)
+ expect((ast.children[0] as ElementNode).children.length).toBe(3)
+ })
+
+ test('should remove whitespaces at start/end inside a root element', () => {
+ const ast = parse(` `)
+ expect(ast.children).toMatchObject([{ type: NodeTypes.ELEMENT }])
})
test('should preserve whitespaces w/ newline between elements', () => {
@@ -2436,6 +2441,48 @@ describe('compiler: parse', () => {
])
})
+ test('should preserve surrounding whitespace for element with text between comments', () => {
+ const ast = parse(` Hi
`)
+ expect(ast.children).toMatchObject([
+ {
+ type: NodeTypes.ELEMENT,
+ children: [
+ { type: NodeTypes.TEXT, content: ' ' },
+ { type: NodeTypes.COMMENT, content: 'comment' },
+ { type: NodeTypes.TEXT, content: 'Hi' },
+ { type: NodeTypes.COMMENT, content: 'comment' },
+ { type: NodeTypes.TEXT, content: ' ' },
+ ],
+ },
+ ])
+ })
+
+ test('should preserve surrounding whitespace for element with text between comments and newlines', () => {
+ const ast = parse(`\n Hi\n
`)
+ expect(ast.children).toMatchObject([
+ {
+ type: NodeTypes.ELEMENT,
+ children: [
+ { type: NodeTypes.TEXT, content: ' ' },
+ { type: NodeTypes.COMMENT, content: 'comment' },
+ { type: NodeTypes.TEXT, content: 'Hi' },
+ { type: NodeTypes.COMMENT, content: 'comment' },
+ { type: NodeTypes.TEXT, content: ' ' },
+ ],
+ },
+ ])
+ })
+
+ test('should preserve whitespace for single element of only whitespace', () => {
+ const ast = parse(`
`)
+ expect(ast.children).toMatchObject([
+ {
+ type: NodeTypes.ELEMENT,
+ children: [{ type: NodeTypes.TEXT, content: ' ' }],
+ },
+ ])
+ })
+
test('should preserve whitespaces adjacent to comments', () => {
const ast = parse(` \n `)
expect(ast.children.length).toBe(5)
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 2cd13bab036..65cf4ac3eb8 100644
--- a/packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap
+++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap
@@ -225,7 +225,30 @@ return function render(_ctx, _cache) {
header: _withCtx(() => [" Header "]),
default: _withCtx(() => [
" ",
- _createElementVNode("p")
+ " ",
+ _createElementVNode("p"),
+ " "
+ ]),
+ _: 1 /* STABLE */
+ }))
+}"
+`;
+
+exports[`compiler: transform component slots > with whitespace: 'preserve' > implicit default slot before and after template 1`] = `
+"const { createElementVNode: _createElementVNode, 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(() => [
+ " ",
+ _createElementVNode("p"),
+ " ",
+ " ",
+ _createElementVNode("p"),
+ " "
]),
_: 1 /* STABLE */
}))
diff --git a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts
index e0f44a064fb..5cce925e16b 100644
--- a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts
@@ -965,6 +965,24 @@ describe('compiler: transform component slots', () => {
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
+ test('implicit default slot before and after template', () => {
+ const source = `
+
+
+ Header
+
+
+ `
+ const { root } = parseWithSlots(source, {
+ whitespace: 'preserve',
+ })
+
+ expect(
+ `Extraneous children found when component already has explicitly named default slot.`,
+ ).not.toHaveBeenWarned()
+ expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
+ })
+
test('should not generate whitespace only default slot', () => {
const source = `
diff --git a/packages/compiler-core/src/parser.ts b/packages/compiler-core/src/parser.ts
index 3eb3a976f4e..b200c542604 100644
--- a/packages/compiler-core/src/parser.ts
+++ b/packages/compiler-core/src/parser.ts
@@ -647,7 +647,7 @@ function onCloseTag(el: ElementNode, end: number, isImplied = false) {
// whitespace management
if (!tokenizer.inRCDATA) {
- el.children = condenseWhitespace(children)
+ el.children = condenseWhitespace(children, false)
}
if (ns === Namespaces.HTML && currentOptions.isIgnoreNewlineTag(tag)) {
@@ -832,7 +832,10 @@ function isUpperCase(c: number) {
}
const windowsNewlineRE = /\r\n/g
-function condenseWhitespace(nodes: TemplateChildNode[]): TemplateChildNode[] {
+function condenseWhitespace(
+ nodes: TemplateChildNode[],
+ isRoot: boolean,
+): TemplateChildNode[] {
const shouldCondense = currentOptions.whitespace !== 'preserve'
let removedWhitespace = false
for (let i = 0; i < nodes.length; i++) {
@@ -843,13 +846,12 @@ function condenseWhitespace(nodes: TemplateChildNode[]): TemplateChildNode[] {
const prev = nodes[i - 1] && nodes[i - 1].type
const next = nodes[i + 1] && nodes[i + 1].type
// Remove if:
- // - the whitespace is the first or last node, or:
+ // - the whitespace is the first or last node on a root template, or:
// - (condense mode) the whitespace is between two comments, or:
// - (condense mode) the whitespace is between comment and element, or:
// - (condense mode) the whitespace is between two elements AND contains newline
if (
- !prev ||
- !next ||
+ ((isRoot || shouldCondense) && (!prev || !next)) ||
(shouldCondense &&
((prev === NodeTypes.COMMENT &&
(next === NodeTypes.COMMENT || next === NodeTypes.ELEMENT)) ||
@@ -1080,7 +1082,7 @@ export function baseParse(input: string, options?: ParserOptions): RootNode {
const root = (currentRoot = createRoot([], input))
tokenizer.parse(currentInput)
root.loc = getLoc(0, input.length)
- root.children = condenseWhitespace(root.children)
+ root.children = condenseWhitespace(root.children, true)
currentRoot = null
return root
}
diff --git a/packages/vue/__tests__/e2e/TransitionGroup.spec.ts b/packages/vue/__tests__/e2e/TransitionGroup.spec.ts
index 62d89db4e31..80ae381e88b 100644
--- a/packages/vue/__tests__/e2e/TransitionGroup.spec.ts
+++ b/packages/vue/__tests__/e2e/TransitionGroup.spec.ts
@@ -623,24 +623,34 @@ describe('e2e: TransitionGroup', () => {
app.mount('#app')
})
- expect(await html('#container')).toBe(`foo
` + ` `)
+ expect(await html('#container')).toBe(
+ ` ` + `foo
` + ` `,
+ )
expect(await htmlWhenTransitionStart()).toBe(
- `foo
` +
+ ` ` +
+ `foo
` +
` ` +
- `bar
`,
+ `bar
` +
+ ` `,
)
await nextFrame()
expect(await html('#container')).toBe(
- `foo
` +
+ ` ` +
+ `foo
` +
` ` +
- `bar
`,
+ `bar
` +
+ ` `,
)
await transitionFinish(duration)
expect(await html('#container')).toBe(
- `foo
` + ` ` + `bar
`,
+ ` ` +
+ `foo
` +
+ ` ` +
+ `bar
` +
+ ` `,
)
},
E2E_TIMEOUT,