Skip to content

fix(compiler-core): preserve start/end whitespace nodes in whitespace: 'preserve' mode #13513

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions packages/compiler-core/__tests__/parse.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(`<div> <span/> </div>`)
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(` <span/> `)
expect(ast.children).toMatchObject([{ type: NodeTypes.ELEMENT }])
})

test('should preserve whitespaces w/ newline between elements', () => {
Expand All @@ -2436,6 +2441,48 @@ describe('compiler: parse', () => {
])
})

test('should preserve surrounding whitespace for element with text between comments', () => {
const ast = parse(`<div> <!--comment-->Hi<!--comment--> </div>`)
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(`<div>\n <!--comment-->Hi<!--comment-->\n</div>`)
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(`<div> </div>`)
expect(ast.children).toMatchObject([
{
type: NodeTypes.ELEMENT,
children: [{ type: NodeTypes.TEXT, content: ' ' }],
},
])
})

test('should preserve whitespaces adjacent to comments', () => {
const ast = parse(`<div/> \n <!--foo--> <div/>`)
expect(ast.children.length).toBe(5)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
}))
Expand Down
18 changes: 18 additions & 0 deletions packages/compiler-core/__tests__/transforms/vSlot.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<Comp>
<p/>
<template #header> Header </template>
<p/>
</Comp>
`
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 = `
<Comp>
Expand Down
14 changes: 8 additions & 6 deletions packages/compiler-core/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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++) {
Expand All @@ -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)) ||
Expand Down Expand Up @@ -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
}
22 changes: 16 additions & 6 deletions packages/vue/__tests__/e2e/TransitionGroup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,24 +623,34 @@ describe('e2e: TransitionGroup', () => {
app.mount('#app')
})

expect(await html('#container')).toBe(`<div class="test">foo</div>` + ` `)
expect(await html('#container')).toBe(
` ` + `<div class="test">foo</div>` + ` `,
)

expect(await htmlWhenTransitionStart()).toBe(
`<div class="test">foo</div>` +
` ` +
`<div class="test">foo</div>` +
` ` +
`<div class="test test-enter-from test-enter-active">bar</div>`,
`<div class="test test-enter-from test-enter-active">bar</div>` +
` `,
)

await nextFrame()
expect(await html('#container')).toBe(
`<div class="test">foo</div>` +
` ` +
`<div class="test">foo</div>` +
` ` +
`<div class="test test-enter-active test-enter-to">bar</div>`,
`<div class="test test-enter-active test-enter-to">bar</div>` +
` `,
)

await transitionFinish(duration)
expect(await html('#container')).toBe(
`<div class="test">foo</div>` + ` ` + `<div class="test">bar</div>`,
` ` +
`<div class="test">foo</div>` +
` ` +
`<div class="test">bar</div>` +
` `,
)
},
E2E_TIMEOUT,
Expand Down