diff --git a/packages/compiler-core/__tests__/parse.spec.ts b/packages/compiler-core/__tests__/parse.spec.ts index 1266d534..84f4b94a 100644 --- a/packages/compiler-core/__tests__/parse.spec.ts +++ b/packages/compiler-core/__tests__/parse.spec.ts @@ -1600,6 +1600,55 @@ foo }) }) + describe('whitespace management', () => { + it('should remove whitespaces at start/end inside an element', () => { + const ast = parse(`
`) + expect((ast.children[0] as ElementNode).children.length).toBe(1) + }) + + it('should remove whitespaces w/ newline between elements', () => { + const ast = parse(`
\n
\n
`) + 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(`
\n \n
`) + 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(`
`) + 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<{ diff --git a/packages/compiler-core/src/parse.ts b/packages/compiler-core/src/parse.ts index 8c8516aa..8b58b669 100644 --- a/packages/compiler-core/src/parse.ts +++ b/packages/compiler-core/src/parse.ts @@ -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.
 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, ' ')
       }
     }
   }
diff --git a/packages/compiler-dom/__tests__/parse.spec.ts b/packages/compiler-dom/__tests__/parse.spec.ts
index aa05c557..4043f366 100644
--- a/packages/compiler-dom/__tests__/parse.spec.ts
+++ b/packages/compiler-dom/__tests__/parse.spec.ts
@@ -98,6 +98,15 @@ describe('DOM parser', () => {
         }
       })
     })
+
+    test('
 tag should preserve raw whitespace', () => {
+      const rawText = `  \na    b    \n   c`
+      const ast = parse(`
${rawText}
`, parserOptions) + expect((ast.children[0] as ElementNode).children[0]).toMatchObject({ + type: NodeTypes.TEXT, + content: rawText + }) + }) }) describe('Interpolation', () => { diff --git a/packages/compiler-dom/src/parserOptionsMinimal.ts b/packages/compiler-dom/src/parserOptionsMinimal.ts index 41e069de..38b3e6a8 100644 --- a/packages/compiler-dom/src/parserOptionsMinimal.ts +++ b/packages/compiler-dom/src/parserOptionsMinimal.ts @@ -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 {