]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler): preserve whitespace in pre tag, add tests
authorEvan You <yyx990803@gmail.com>
Thu, 24 Oct 2019 20:42:09 +0000 (16:42 -0400)
committerEvan You <yyx990803@gmail.com>
Thu, 24 Oct 2019 20:42:09 +0000 (16:42 -0400)
packages/compiler-core/__tests__/parse.spec.ts
packages/compiler-core/src/parse.ts
packages/compiler-dom/__tests__/parse.spec.ts
packages/compiler-dom/src/parserOptionsMinimal.ts

index 1266d534108cbf97128358e3ee9cfbbc1e245821..84f4b94a5ad0176eef2433fd2111b85d64e9c662 100644 (file)
@@ -1600,6 +1600,55 @@ foo
     })
   })
 
+  describe('whitespace management', () => {
+    it('should remove whitespaces at start/end inside an element', () => {
+      const ast = parse(`<div>   <span/>    </div>`)
+      expect((ast.children[0] as ElementNode).children.length).toBe(1)
+    })
+
+    it('should remove whitespaces w/ newline between elements', () => {
+      const ast = parse(`<div/> \n <div/> \n <div/>`)
+      expect(ast.children.length).toBe(3)
+      expect(ast.children.every(c => c.type === NodeTypes.ELEMENT)).toBe(true)
+    })
+
+    it('should remove whitespaces w/ newline between comments and elements', () => {
+      const ast = parse(`<div/> \n <!--foo--> \n <div/>`)
+      expect(ast.children.length).toBe(3)
+      expect(ast.children[0].type).toBe(NodeTypes.ELEMENT)
+      expect(ast.children[1].type).toBe(NodeTypes.COMMENT)
+      expect(ast.children[2].type).toBe(NodeTypes.ELEMENT)
+    })
+
+    it('should NOT remove whitespaces w/ newline between interpolations', () => {
+      const ast = parse(`{{ foo }} \n {{ bar }}`)
+      expect(ast.children.length).toBe(3)
+      expect(ast.children[0].type).toBe(NodeTypes.INTERPOLATION)
+      expect(ast.children[1]).toMatchObject({
+        type: NodeTypes.TEXT,
+        content: ' '
+      })
+      expect(ast.children[2].type).toBe(NodeTypes.INTERPOLATION)
+    })
+
+    it('should NOT remove whitespaces w/o newline between elements', () => {
+      const ast = parse(`<div/> <div/> <div/>`)
+      expect(ast.children.length).toBe(5)
+      expect(ast.children.map(c => c.type)).toMatchObject([
+        NodeTypes.ELEMENT,
+        NodeTypes.TEXT,
+        NodeTypes.ELEMENT,
+        NodeTypes.TEXT,
+        NodeTypes.ELEMENT
+      ])
+    })
+
+    it('should condense consecutive whitespaces in text', () => {
+      const ast = parse(`   foo  \n    bar     baz     `)
+      expect((ast.children[0] as TextNode).content).toBe(` foo bar baz `)
+    })
+  })
+
   describe('Errors', () => {
     const patterns: {
       [key: string]: Array<{
index 8c8516aab87846228d1e3056325a90c5dd4a5702..8b58b66972efd64f99560c0027e545fefb0c08da 100644 (file)
@@ -32,6 +32,7 @@ import { extend } from '@vue/shared'
 export interface ParserOptions {
   isVoidTag?: (tag: string) => boolean // e.g. img, br, hr
   isNativeTag?: (tag: string) => boolean // e.g. loading-indicator in weex
+  isPreTag?: (tag: string) => boolean // e.g. <pre> where whitespace is intact
   isCustomElement?: (tag: string) => boolean
   getNamespace?: (tag: string, parent: ElementNode | undefined) => Namespace
   getTextMode?: (tag: string, ns: Namespace) => TextModes
@@ -53,6 +54,7 @@ export const defaultParserOptions: MergedParserOptions = {
   getNamespace: () => Namespaces.HTML,
   getTextMode: () => TextModes.DATA,
   isVoidTag: NO,
+  isPreTag: NO,
   isCustomElement: NO,
   namedCharacterReferences: {
     'gt;': '>',
@@ -207,34 +209,36 @@ function parseChildren(
   // Whitespace management for more efficient output
   // (same as v2 whitespance: 'condense')
   let removedWhitespace = false
-  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 contains newline AND is between two element or comments
-        // Then the whitespace is ignored.
-        if (
-          !prev ||
-          !next ||
-          ((prev.type === NodeTypes.ELEMENT ||
-            prev.type === NodeTypes.COMMENT) &&
-            (next.type === NodeTypes.ELEMENT ||
-              next.type === NodeTypes.COMMENT) &&
-            /[\r\n]/.test(node.content))
-        ) {
-          removedWhitespace = true
-          nodes[i] = null as any
+  if (!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 contains newline AND is between two element or comments
+          // Then the whitespace is ignored.
+          if (
+            !prev ||
+            !next ||
+            ((prev.type === NodeTypes.ELEMENT ||
+              prev.type === NodeTypes.COMMENT) &&
+              (next.type === NodeTypes.ELEMENT ||
+                next.type === NodeTypes.COMMENT) &&
+              /[\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, ' ')
       }
     }
   }
index aa05c5571c4c437d29297fd61a4dfae7d4c60524..4043f3662fea329b526cabf2d21b4c9e55717798 100644 (file)
@@ -98,6 +98,15 @@ describe('DOM parser', () => {
         }
       })
     })
+
+    test('<pre> tag should preserve raw whitespace', () => {
+      const rawText = `  \na    b    \n   c`
+      const ast = parse(`<pre>${rawText}</pre>`, parserOptions)
+      expect((ast.children[0] as ElementNode).children[0]).toMatchObject({
+        type: NodeTypes.TEXT,
+        content: rawText
+      })
+    })
   })
 
   describe('Interpolation', () => {
index 41e069dee8a8e4e9481b233dc9a959eec7ee351c..38b3e6a8343f48fbc58e41be307e9d2bcd24e1d7 100644 (file)
@@ -15,8 +15,8 @@ export const enum DOMNamespaces {
 
 export const parserOptionsMinimal: ParserOptions = {
   isVoidTag,
-
   isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag),
+  isPreTag: tag => tag === 'pre',
 
   // https://html.spec.whatwg.org/multipage/parsing.html#tree-construction-dispatcher
   getNamespace(tag: string, parent: ElementNode | undefined): DOMNamespaces {