Skip to content

Commit eb20730

Browse files
committed
feat(compiler): preserve whitespace in pre tag, add tests
1 parent 9298f46 commit eb20730

File tree

4 files changed

+89
-27
lines changed

4 files changed

+89
-27
lines changed

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1600,6 +1600,55 @@ foo
16001600
})
16011601
})
16021602

1603+
describe('whitespace management', () => {
1604+
it('should remove whitespaces at start/end inside an element', () => {
1605+
const ast = parse(`<div> <span/> </div>`)
1606+
expect((ast.children[0] as ElementNode).children.length).toBe(1)
1607+
})
1608+
1609+
it('should remove whitespaces w/ newline between elements', () => {
1610+
const ast = parse(`<div/> \n <div/> \n <div/>`)
1611+
expect(ast.children.length).toBe(3)
1612+
expect(ast.children.every(c => c.type === NodeTypes.ELEMENT)).toBe(true)
1613+
})
1614+
1615+
it('should remove whitespaces w/ newline between comments and elements', () => {
1616+
const ast = parse(`<div/> \n <!--foo--> \n <div/>`)
1617+
expect(ast.children.length).toBe(3)
1618+
expect(ast.children[0].type).toBe(NodeTypes.ELEMENT)
1619+
expect(ast.children[1].type).toBe(NodeTypes.COMMENT)
1620+
expect(ast.children[2].type).toBe(NodeTypes.ELEMENT)
1621+
})
1622+
1623+
it('should NOT remove whitespaces w/ newline between interpolations', () => {
1624+
const ast = parse(`{{ foo }} \n {{ bar }}`)
1625+
expect(ast.children.length).toBe(3)
1626+
expect(ast.children[0].type).toBe(NodeTypes.INTERPOLATION)
1627+
expect(ast.children[1]).toMatchObject({
1628+
type: NodeTypes.TEXT,
1629+
content: ' '
1630+
})
1631+
expect(ast.children[2].type).toBe(NodeTypes.INTERPOLATION)
1632+
})
1633+
1634+
it('should NOT remove whitespaces w/o newline between elements', () => {
1635+
const ast = parse(`<div/> <div/> <div/>`)
1636+
expect(ast.children.length).toBe(5)
1637+
expect(ast.children.map(c => c.type)).toMatchObject([
1638+
NodeTypes.ELEMENT,
1639+
NodeTypes.TEXT,
1640+
NodeTypes.ELEMENT,
1641+
NodeTypes.TEXT,
1642+
NodeTypes.ELEMENT
1643+
])
1644+
})
1645+
1646+
it('should condense consecutive whitespaces in text', () => {
1647+
const ast = parse(` foo \n bar baz `)
1648+
expect((ast.children[0] as TextNode).content).toBe(` foo bar baz `)
1649+
})
1650+
})
1651+
16031652
describe('Errors', () => {
16041653
const patterns: {
16051654
[key: string]: Array<{

packages/compiler-core/src/parse.ts

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { extend } from '@vue/shared'
3232
export interface ParserOptions {
3333
isVoidTag?: (tag: string) => boolean // e.g. img, br, hr
3434
isNativeTag?: (tag: string) => boolean // e.g. loading-indicator in weex
35+
isPreTag?: (tag: string) => boolean // e.g. <pre> where whitespace is intact
3536
isCustomElement?: (tag: string) => boolean
3637
getNamespace?: (tag: string, parent: ElementNode | undefined) => Namespace
3738
getTextMode?: (tag: string, ns: Namespace) => TextModes
@@ -53,6 +54,7 @@ export const defaultParserOptions: MergedParserOptions = {
5354
getNamespace: () => Namespaces.HTML,
5455
getTextMode: () => TextModes.DATA,
5556
isVoidTag: NO,
57+
isPreTag: NO,
5658
isCustomElement: NO,
5759
namedCharacterReferences: {
5860
'gt;': '>',
@@ -207,34 +209,36 @@ function parseChildren(
207209
// Whitespace management for more efficient output
208210
// (same as v2 whitespance: 'condense')
209211
let removedWhitespace = false
210-
for (let i = 0; i < nodes.length; i++) {
211-
const node = nodes[i]
212-
if (node.type === NodeTypes.TEXT) {
213-
if (!node.content.trim()) {
214-
const prev = nodes[i - 1]
215-
const next = nodes[i + 1]
216-
// If:
217-
// - the whitespace is the first or last node, or:
218-
// - the whitespace contains newline AND is between two element or comments
219-
// Then the whitespace is ignored.
220-
if (
221-
!prev ||
222-
!next ||
223-
((prev.type === NodeTypes.ELEMENT ||
224-
prev.type === NodeTypes.COMMENT) &&
225-
(next.type === NodeTypes.ELEMENT ||
226-
next.type === NodeTypes.COMMENT) &&
227-
/[\r\n]/.test(node.content))
228-
) {
229-
removedWhitespace = true
230-
nodes[i] = null as any
212+
if (!parent || !context.options.isPreTag(parent.tag)) {
213+
for (let i = 0; i < nodes.length; i++) {
214+
const node = nodes[i]
215+
if (node.type === NodeTypes.TEXT) {
216+
if (!node.content.trim()) {
217+
const prev = nodes[i - 1]
218+
const next = nodes[i + 1]
219+
// If:
220+
// - the whitespace is the first or last node, or:
221+
// - the whitespace contains newline AND is between two element or comments
222+
// Then the whitespace is ignored.
223+
if (
224+
!prev ||
225+
!next ||
226+
((prev.type === NodeTypes.ELEMENT ||
227+
prev.type === NodeTypes.COMMENT) &&
228+
(next.type === NodeTypes.ELEMENT ||
229+
next.type === NodeTypes.COMMENT) &&
230+
/[\r\n]/.test(node.content))
231+
) {
232+
removedWhitespace = true
233+
nodes[i] = null as any
234+
} else {
235+
// Otherwise, condensed consecutive whitespace inside the text down to
236+
// a single space
237+
node.content = ' '
238+
}
231239
} else {
232-
// Otherwise, condensed consecutive whitespace inside the text down to
233-
// a single space
234-
node.content = ' '
240+
node.content = node.content.replace(/\s+/g, ' ')
235241
}
236-
} else {
237-
node.content = node.content.replace(/\s+/g, ' ')
238242
}
239243
}
240244
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,15 @@ describe('DOM parser', () => {
9898
}
9999
})
100100
})
101+
102+
test('<pre> tag should preserve raw whitespace', () => {
103+
const rawText = ` \na b \n c`
104+
const ast = parse(`<pre>${rawText}</pre>`, parserOptions)
105+
expect((ast.children[0] as ElementNode).children[0]).toMatchObject({
106+
type: NodeTypes.TEXT,
107+
content: rawText
108+
})
109+
})
101110
})
102111

103112
describe('Interpolation', () => {

packages/compiler-dom/src/parserOptionsMinimal.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ export const enum DOMNamespaces {
1515

1616
export const parserOptionsMinimal: ParserOptions = {
1717
isVoidTag,
18-
1918
isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag),
19+
isPreTag: tag => tag === 'pre',
2020

2121
// https://html.spec.whatwg.org/multipage/parsing.html#tree-construction-dispatcher
2222
getNamespace(tag: string, parent: ElementNode | undefined): DOMNamespaces {

0 commit comments

Comments
 (0)