This commit is contained in:
skirtle 2025-07-01 14:33:26 +00:00 committed by GitHub
commit 46a3e014d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 292 additions and 37 deletions

View File

@ -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

View File

@ -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(`<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/>`,

View File

@ -246,6 +246,31 @@ describe('compiler: v-if', () => {
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', () => {
@ -285,6 +310,31 @@ describe('compiler: v-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(
@ -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,

View File

@ -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(
`<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>
@ -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 = `
<Comp>
&nbsp;
<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>
@ -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 = `
<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()
})
})
})

View File

@ -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)

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -135,6 +135,18 @@ describe('Transition multi children warnings', () => {
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', () => {
@ -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(
`
<transition>
<!-- foo --> <!-- bar -->
<div>foo bar</div>
</transition>
`,
{
whitespace: 'preserve',
},
).code,
).toMatchSnapshot()
})

View File

@ -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

View File

@ -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 (