diff --git a/packages/compiler-core/src/parse.ts b/packages/compiler-core/src/parse.ts index 11cad7f1..1e94d645 100644 --- a/packages/compiler-core/src/parse.ts +++ b/packages/compiler-core/src/parse.ts @@ -64,7 +64,8 @@ interface ParserContext { offset: number line: number column: number - inPre: boolean + inPre: boolean // HTML
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) + // checktag + 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 )! diff --git a/packages/compiler-dom/__tests__/parse.spec.ts b/packages/compiler-dom/__tests__/parse.spec.ts index 809d014f..58e37753 100644 --- a/packages/compiler-dom/__tests__/parse.spec.ts +++ b/packages/compiler-dom/__tests__/parse.spec.ts @@ -116,11 +116,36 @@ describe('DOM parser', () => { }) test('tag should preserve raw whitespace', () => { - const rawText = ` \na b \n c` + const rawText = ` \nafoo \n bar\n c` + const ast = parse(`${rawText}`, 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('tag should remove leading newline', () => { + const rawText = `\nhello` const ast = parse(`${rawText}`, parserOptions) expect((ast.children[0] as ElementNode).children[0]).toMatchObject({ type: NodeTypes.TEXT, - content: rawText + content: rawText.slice(1) }) }) })