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:
parent
c7c3a6a3be
commit
7f30cb5772
@ -64,7 +64,8 @@ interface ParserContext {
|
||||
offset: number
|
||||
line: 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(
|
||||
@ -93,7 +94,8 @@ function createParserContext(
|
||||
offset: 0,
|
||||
originalSource: content,
|
||||
source: content,
|
||||
inPre: false
|
||||
inPre: false,
|
||||
inVPre: false
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,7 +114,7 @@ function parseChildren(
|
||||
let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined
|
||||
|
||||
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)
|
||||
} else if (mode === TextModes.DATA && s[0] === '<') {
|
||||
@ -187,41 +189,47 @@ function parseChildren(
|
||||
// Whitespace management for more efficient output
|
||||
// (same as v2 whitespace: 'condense')
|
||||
let removedWhitespace = false
|
||||
if (
|
||||
mode !== TextModes.RAWTEXT &&
|
||||
(!parent || !context.options.isPreTag(parent.tag))
|
||||
) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i]
|
||||
if (node.type === NodeTypes.TEXT) {
|
||||
if (!node.content.trim()) {
|
||||
const prev = nodes[i - 1]
|
||||
const next = nodes[i + 1]
|
||||
// If:
|
||||
// - the whitespace is the first or last node, or:
|
||||
// - the whitespace is adjacent to a comment, or:
|
||||
// - the whitespace is between two elements AND contains newline
|
||||
// Then the whitespace is ignored.
|
||||
if (
|
||||
!prev ||
|
||||
!next ||
|
||||
prev.type === NodeTypes.COMMENT ||
|
||||
next.type === NodeTypes.COMMENT ||
|
||||
(prev.type === NodeTypes.ELEMENT &&
|
||||
next.type === NodeTypes.ELEMENT &&
|
||||
/[\r\n]/.test(node.content))
|
||||
) {
|
||||
removedWhitespace = true
|
||||
nodes[i] = null as any
|
||||
if (mode !== TextModes.RAWTEXT) {
|
||||
if (!context.inPre) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i]
|
||||
if (node.type === NodeTypes.TEXT) {
|
||||
if (!node.content.trim()) {
|
||||
const prev = nodes[i - 1]
|
||||
const next = nodes[i + 1]
|
||||
// If:
|
||||
// - the whitespace is the first or last node, or:
|
||||
// - the whitespace is adjacent to a comment, or:
|
||||
// - the whitespace is between two elements AND contains newline
|
||||
// Then the whitespace is ignored.
|
||||
if (
|
||||
!prev ||
|
||||
!next ||
|
||||
prev.type === NodeTypes.COMMENT ||
|
||||
next.type === NodeTypes.COMMENT ||
|
||||
(prev.type === NodeTypes.ELEMENT &&
|
||||
next.type === NodeTypes.ELEMENT &&
|
||||
/[\r\n]/.test(node.content))
|
||||
) {
|
||||
removedWhitespace = true
|
||||
nodes[i] = null as any
|
||||
} else {
|
||||
// Otherwise, condensed consecutive whitespace inside the text down to
|
||||
// a single space
|
||||
node.content = ' '
|
||||
}
|
||||
} else {
|
||||
// Otherwise, condensed consecutive whitespace inside the text down to
|
||||
// a single space
|
||||
node.content = ' '
|
||||
node.content = node.content.replace(/\s+/g, ' ')
|
||||
}
|
||||
} 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.
|
||||
const wasInPre = context.inPre
|
||||
const wasInVPre = context.inVPre
|
||||
const parent = last(ancestors)
|
||||
const element = parseTag(context, TagType.Start, parent)
|
||||
const isPreBoundary = context.inPre && !wasInPre
|
||||
const isVPreBoundary = context.inVPre && !wasInVPre
|
||||
|
||||
if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
|
||||
return element
|
||||
@ -381,6 +391,9 @@ function parseElement(
|
||||
if (isPreBoundary) {
|
||||
context.inPre = false
|
||||
}
|
||||
if (isVPreBoundary) {
|
||||
context.inVPre = false
|
||||
}
|
||||
return element
|
||||
}
|
||||
|
||||
@ -423,12 +436,17 @@ function parseTag(
|
||||
// Attributes.
|
||||
let props = parseAttributes(context, type)
|
||||
|
||||
// check <pre> tag
|
||||
if (context.options.isPreTag(tag)) {
|
||||
context.inPre = true
|
||||
}
|
||||
|
||||
// check v-pre
|
||||
if (
|
||||
!context.inPre &&
|
||||
!context.inVPre &&
|
||||
props.some(p => p.type === NodeTypes.DIRECTIVE && p.name === 'pre')
|
||||
) {
|
||||
context.inPre = true
|
||||
context.inVPre = true
|
||||
// reset context
|
||||
extend(context, cursor)
|
||||
context.source = currentSource
|
||||
@ -450,7 +468,7 @@ function parseTag(
|
||||
|
||||
let tagType = ElementTypes.ELEMENT
|
||||
const options = context.options
|
||||
if (!context.inPre && !options.isCustomElement(tag)) {
|
||||
if (!context.inVPre && !options.isCustomElement(tag)) {
|
||||
const hasVIs = props.some(
|
||||
p => p.type === NodeTypes.DIRECTIVE && p.name === 'is'
|
||||
)
|
||||
@ -580,7 +598,7 @@ function parseAttribute(
|
||||
}
|
||||
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(
|
||||
name
|
||||
)!
|
||||
|
@ -116,11 +116,36 @@ describe('DOM parser', () => {
|
||||
})
|
||||
|
||||
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)
|
||||
expect((ast.children[0] as ElementNode).children[0]).toMatchObject({
|
||||
type: NodeTypes.TEXT,
|
||||
content: rawText
|
||||
content: rawText.slice(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user