Skip to content

Commit 7f30cb5

Browse files
committed
fix(compiler): fix pre tag whitespace handling
- should preserve whitespace even in nested elements - should remove leading newline per spec fix #908
1 parent c7c3a6a commit 7f30cb5

File tree

2 files changed

+83
-40
lines changed

2 files changed

+83
-40
lines changed

packages/compiler-core/src/parse.ts

+56-38
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ interface ParserContext {
6464
offset: number
6565
line: number
6666
column: number
67-
inPre: boolean
67+
inPre: boolean // HTML <pre> tag, preserve whitespaces
68+
inVPre: boolean // v-pre, do not process directives and interpolations
6869
}
6970

7071
export function baseParse(
@@ -93,7 +94,8 @@ function createParserContext(
9394
offset: 0,
9495
originalSource: content,
9596
source: content,
96-
inPre: false
97+
inPre: false,
98+
inVPre: false
9799
}
98100
}
99101

@@ -112,7 +114,7 @@ function parseChildren(
112114
let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined
113115

114116
if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
115-
if (!context.inPre && startsWith(s, context.options.delimiters[0])) {
117+
if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
116118
// '{{'
117119
node = parseInterpolation(context, mode)
118120
} else if (mode === TextModes.DATA && s[0] === '<') {
@@ -187,41 +189,47 @@ function parseChildren(
187189
// Whitespace management for more efficient output
188190
// (same as v2 whitespace: 'condense')
189191
let removedWhitespace = false
190-
if (
191-
mode !== TextModes.RAWTEXT &&
192-
(!parent || !context.options.isPreTag(parent.tag))
193-
) {
194-
for (let i = 0; i < nodes.length; i++) {
195-
const node = nodes[i]
196-
if (node.type === NodeTypes.TEXT) {
197-
if (!node.content.trim()) {
198-
const prev = nodes[i - 1]
199-
const next = nodes[i + 1]
200-
// If:
201-
// - the whitespace is the first or last node, or:
202-
// - the whitespace is adjacent to a comment, or:
203-
// - the whitespace is between two elements AND contains newline
204-
// Then the whitespace is ignored.
205-
if (
206-
!prev ||
207-
!next ||
208-
prev.type === NodeTypes.COMMENT ||
209-
next.type === NodeTypes.COMMENT ||
210-
(prev.type === NodeTypes.ELEMENT &&
211-
next.type === NodeTypes.ELEMENT &&
212-
/[\r\n]/.test(node.content))
213-
) {
214-
removedWhitespace = true
215-
nodes[i] = null as any
192+
if (mode !== TextModes.RAWTEXT) {
193+
if (!context.inPre) {
194+
for (let i = 0; i < nodes.length; i++) {
195+
const node = nodes[i]
196+
if (node.type === NodeTypes.TEXT) {
197+
if (!node.content.trim()) {
198+
const prev = nodes[i - 1]
199+
const next = nodes[i + 1]
200+
// If:
201+
// - the whitespace is the first or last node, or:
202+
// - the whitespace is adjacent to a comment, or:
203+
// - the whitespace is between two elements AND contains newline
204+
// Then the whitespace is ignored.
205+
if (
206+
!prev ||
207+
!next ||
208+
prev.type === NodeTypes.COMMENT ||
209+
next.type === NodeTypes.COMMENT ||
210+
(prev.type === NodeTypes.ELEMENT &&
211+
next.type === NodeTypes.ELEMENT &&
212+
/[\r\n]/.test(node.content))
213+
) {
214+
removedWhitespace = true
215+
nodes[i] = null as any
216+
} else {
217+
// Otherwise, condensed consecutive whitespace inside the text down to
218+
// a single space
219+
node.content = ' '
220+
}
216221
} else {
217-
// Otherwise, condensed consecutive whitespace inside the text down to
218-
// a single space
219-
node.content = ' '
222+
node.content = node.content.replace(/\s+/g, ' ')
220223
}
221-
} else {
222-
node.content = node.content.replace(/\s+/g, ' ')
223224
}
224225
}
226+
} else {
227+
// remove leading newline per html spec
228+
// https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
229+
const first = nodes[0]
230+
if (first && first.type === NodeTypes.TEXT) {
231+
first.content = first.content.replace(/^\r?\n/, '')
232+
}
225233
}
226234
}
227235

@@ -347,9 +355,11 @@ function parseElement(
347355

348356
// Start tag.
349357
const wasInPre = context.inPre
358+
const wasInVPre = context.inVPre
350359
const parent = last(ancestors)
351360
const element = parseTag(context, TagType.Start, parent)
352361
const isPreBoundary = context.inPre && !wasInPre
362+
const isVPreBoundary = context.inVPre && !wasInVPre
353363

354364
if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
355365
return element
@@ -381,6 +391,9 @@ function parseElement(
381391
if (isPreBoundary) {
382392
context.inPre = false
383393
}
394+
if (isVPreBoundary) {
395+
context.inVPre = false
396+
}
384397
return element
385398
}
386399

@@ -423,12 +436,17 @@ function parseTag(
423436
// Attributes.
424437
let props = parseAttributes(context, type)
425438

439+
// check <pre> tag
440+
if (context.options.isPreTag(tag)) {
441+
context.inPre = true
442+
}
443+
426444
// check v-pre
427445
if (
428-
!context.inPre &&
446+
!context.inVPre &&
429447
props.some(p => p.type === NodeTypes.DIRECTIVE && p.name === 'pre')
430448
) {
431-
context.inPre = true
449+
context.inVPre = true
432450
// reset context
433451
extend(context, cursor)
434452
context.source = currentSource
@@ -450,7 +468,7 @@ function parseTag(
450468

451469
let tagType = ElementTypes.ELEMENT
452470
const options = context.options
453-
if (!context.inPre && !options.isCustomElement(tag)) {
471+
if (!context.inVPre && !options.isCustomElement(tag)) {
454472
const hasVIs = props.some(
455473
p => p.type === NodeTypes.DIRECTIVE && p.name === 'is'
456474
)
@@ -580,7 +598,7 @@ function parseAttribute(
580598
}
581599
const loc = getSelection(context, start)
582600

583-
if (!context.inPre && /^(v-|:|@|#)/.test(name)) {
601+
if (!context.inVPre && /^(v-|:|@|#)/.test(name)) {
584602
const match = /(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)([^\.]+))?(.+)?$/i.exec(
585603
name
586604
)!

packages/compiler-dom/__tests__/parse.spec.ts

+27-2
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,36 @@ describe('DOM parser', () => {
116116
})
117117

118118
test('<pre> tag should preserve raw whitespace', () => {
119-
const rawText = ` \na b \n c`
119+
const rawText = ` \na <div>foo \n bar</div> \n c`
120+
const ast = parse(`<pre>${rawText}</pre>`, parserOptions)
121+
expect((ast.children[0] as ElementNode).children).toMatchObject([
122+
{
123+
type: NodeTypes.TEXT,
124+
content: ` \na `
125+
},
126+
{
127+
type: NodeTypes.ELEMENT,
128+
children: [
129+
{
130+
type: NodeTypes.TEXT,
131+
content: `foo \n bar`
132+
}
133+
]
134+
},
135+
{
136+
type: NodeTypes.TEXT,
137+
content: ` \n c`
138+
}
139+
])
140+
})
141+
142+
// #908
143+
test('<pre> tag should remove leading newline', () => {
144+
const rawText = `\nhello`
120145
const ast = parse(`<pre>${rawText}</pre>`, parserOptions)
121146
expect((ast.children[0] as ElementNode).children[0]).toMatchObject({
122147
type: NodeTypes.TEXT,
123-
content: rawText
148+
content: rawText.slice(1)
124149
})
125150
})
126151
})

0 commit comments

Comments
 (0)