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
|
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
|
||||||
)!
|
)!
|
||||||
|
@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user