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 = ` + +

+ +

+ + ` + 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,