diff --git a/packages/compiler-core/src/parse.ts b/packages/compiler-core/src/parse.ts index 11cad7f19a8..1e94d645116 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)
 
+  // check 
 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
     )!
diff --git a/packages/compiler-dom/__tests__/parse.spec.ts b/packages/compiler-dom/__tests__/parse.spec.ts
index 809d014fa65..58e37753040 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 = `  \na   
foo \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) }) }) })