fix(compiler): fix pre tag whitespace handling

- should preserve whitespace even in nested elements
- should remove leading newline per spec

fix #908
This commit is contained in:
Evan You 2020-04-03 21:02:20 -04:00
parent c7c3a6a3be
commit 7f30cb5772
2 changed files with 83 additions and 40 deletions

View File

@ -64,7 +64,8 @@ interface ParserContext {
offset: number offset: number
line: number line: number
column: number column: number
inPre: boolean inPre: boolean // HTML <pre> tag, preserve whitespaces
inVPre: boolean // v-pre, do not process directives and interpolations
} }
export function baseParse( export function baseParse(
@ -93,7 +94,8 @@ function createParserContext(
offset: 0, offset: 0,
originalSource: content, originalSource: content,
source: content, source: content,
inPre: false inPre: false,
inVPre: false
} }
} }
@ -112,7 +114,7 @@ function parseChildren(
let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined
if (mode === TextModes.DATA || mode === TextModes.RCDATA) { if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
if (!context.inPre && startsWith(s, context.options.delimiters[0])) { if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
// '{{' // '{{'
node = parseInterpolation(context, mode) node = parseInterpolation(context, mode)
} else if (mode === TextModes.DATA && s[0] === '<') { } else if (mode === TextModes.DATA && s[0] === '<') {
@ -187,41 +189,47 @@ function parseChildren(
// Whitespace management for more efficient output // Whitespace management for more efficient output
// (same as v2 whitespace: 'condense') // (same as v2 whitespace: 'condense')
let removedWhitespace = false let removedWhitespace = false
if ( if (mode !== TextModes.RAWTEXT) {
mode !== TextModes.RAWTEXT && if (!context.inPre) {
(!parent || !context.options.isPreTag(parent.tag)) for (let i = 0; i < nodes.length; i++) {
) { const node = nodes[i]
for (let i = 0; i < nodes.length; i++) { if (node.type === NodeTypes.TEXT) {
const node = nodes[i] if (!node.content.trim()) {
if (node.type === NodeTypes.TEXT) { const prev = nodes[i - 1]
if (!node.content.trim()) { const next = nodes[i + 1]
const prev = nodes[i - 1] // If:
const next = nodes[i + 1] // - the whitespace is the first or last node, or:
// If: // - the whitespace is adjacent to a comment, or:
// - the whitespace is the first or last node, or: // - the whitespace is between two elements AND contains newline
// - the whitespace is adjacent to a comment, or: // Then the whitespace is ignored.
// - the whitespace is between two elements AND contains newline if (
// Then the whitespace is ignored. !prev ||
if ( !next ||
!prev || prev.type === NodeTypes.COMMENT ||
!next || next.type === NodeTypes.COMMENT ||
prev.type === NodeTypes.COMMENT || (prev.type === NodeTypes.ELEMENT &&
next.type === NodeTypes.COMMENT || next.type === NodeTypes.ELEMENT &&
(prev.type === NodeTypes.ELEMENT && /[\r\n]/.test(node.content))
next.type === NodeTypes.ELEMENT && ) {
/[\r\n]/.test(node.content)) removedWhitespace = true
) { nodes[i] = null as any
removedWhitespace = true } else {
nodes[i] = null as any // Otherwise, condensed consecutive whitespace inside the text down to
// a single space
node.content = ' '
}
} else { } else {
// Otherwise, condensed consecutive whitespace inside the text down to node.content = node.content.replace(/\s+/g, ' ')
// a single space
node.content = ' '
} }
} else {
node.content = node.content.replace(/\s+/g, ' ')
} }
} }
} else {
// remove leading newline per html spec
// https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
const first = nodes[0]
if (first && first.type === NodeTypes.TEXT) {
first.content = first.content.replace(/^\r?\n/, '')
}
} }
} }
@ -347,9 +355,11 @@ function parseElement(
// Start tag. // Start tag.
const wasInPre = context.inPre const wasInPre = context.inPre
const wasInVPre = context.inVPre
const parent = last(ancestors) const parent = last(ancestors)
const element = parseTag(context, TagType.Start, parent) const element = parseTag(context, TagType.Start, parent)
const isPreBoundary = context.inPre && !wasInPre const isPreBoundary = context.inPre && !wasInPre
const isVPreBoundary = context.inVPre && !wasInVPre
if (element.isSelfClosing || context.options.isVoidTag(element.tag)) { if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
return element return element
@ -381,6 +391,9 @@ function parseElement(
if (isPreBoundary) { if (isPreBoundary) {
context.inPre = false context.inPre = false
} }
if (isVPreBoundary) {
context.inVPre = false
}
return element return element
} }
@ -423,12 +436,17 @@ function parseTag(
// Attributes. // Attributes.
let props = parseAttributes(context, type) let props = parseAttributes(context, type)
// check <pre> tag
if (context.options.isPreTag(tag)) {
context.inPre = true
}
// check v-pre // check v-pre
if ( if (
!context.inPre && !context.inVPre &&
props.some(p => p.type === NodeTypes.DIRECTIVE && p.name === 'pre') props.some(p => p.type === NodeTypes.DIRECTIVE && p.name === 'pre')
) { ) {
context.inPre = true context.inVPre = true
// reset context // reset context
extend(context, cursor) extend(context, cursor)
context.source = currentSource context.source = currentSource
@ -450,7 +468,7 @@ function parseTag(
let tagType = ElementTypes.ELEMENT let tagType = ElementTypes.ELEMENT
const options = context.options const options = context.options
if (!context.inPre && !options.isCustomElement(tag)) { if (!context.inVPre && !options.isCustomElement(tag)) {
const hasVIs = props.some( const hasVIs = props.some(
p => p.type === NodeTypes.DIRECTIVE && p.name === 'is' p => p.type === NodeTypes.DIRECTIVE && p.name === 'is'
) )
@ -580,7 +598,7 @@ function parseAttribute(
} }
const loc = getSelection(context, start) const loc = getSelection(context, start)
if (!context.inPre && /^(v-|:|@|#)/.test(name)) { if (!context.inVPre && /^(v-|:|@|#)/.test(name)) {
const match = /(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)([^\.]+))?(.+)?$/i.exec( const match = /(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)([^\.]+))?(.+)?$/i.exec(
name name
)! )!

View File

@ -116,11 +116,36 @@ describe('DOM parser', () => {
}) })
test('<pre> tag should preserve raw whitespace', () => { test('<pre> tag should preserve raw whitespace', () => {
const rawText = ` \na b \n c` const rawText = ` \na <div>foo \n bar</div> \n c`
const ast = parse(`<pre>${rawText}</pre>`, parserOptions)
expect((ast.children[0] as ElementNode).children).toMatchObject([
{
type: NodeTypes.TEXT,
content: ` \na `
},
{
type: NodeTypes.ELEMENT,
children: [
{
type: NodeTypes.TEXT,
content: `foo \n bar`
}
]
},
{
type: NodeTypes.TEXT,
content: ` \n c`
}
])
})
// #908
test('<pre> tag should remove leading newline', () => {
const rawText = `\nhello`
const ast = parse(`<pre>${rawText}</pre>`, parserOptions) const ast = parse(`<pre>${rawText}</pre>`, parserOptions)
expect((ast.children[0] as ElementNode).children[0]).toMatchObject({ expect((ast.children[0] as ElementNode).children[0]).toMatchObject({
type: NodeTypes.TEXT, type: NodeTypes.TEXT,
content: rawText content: rawText.slice(1)
}) })
}) })
}) })