]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler): fix pre tag whitespace handling
authorEvan You <yyx990803@gmail.com>
Sat, 4 Apr 2020 01:02:20 +0000 (21:02 -0400)
committerEvan You <yyx990803@gmail.com>
Sat, 4 Apr 2020 01:02:20 +0000 (21:02 -0400)
- should preserve whitespace even in nested elements
- should remove leading newline per spec

fix #908

packages/compiler-core/src/parse.ts
packages/compiler-dom/__tests__/parse.spec.ts

index 11cad7f19a8d216f1727e3a75db7870c035a380d..1e94d64511643cac5570f6f6b2b296936b2771a3 100644 (file)
@@ -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
     )!
index 809d014fa65a8cd15fb06258bea6a18432f99046..58e37753040dd85de836bd04b21fe14e84a823ad 100644 (file)
@@ -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)
       })
     })
   })