From e1abedb9e66b21da8a7e93e175b9dabe334dfebd Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 26 Dec 2018 14:12:34 -0500 Subject: [PATCH] feat(compiler): add whitespace option, deprecate preserveWhitespace option close #9208 --- flow/compiler.js | 3 +- packages/vue-template-compiler/README.md | 46 +++++++++++++++++-- .../vue-template-compiler/types/index.d.ts | 1 + packages/vue-template-compiler/types/test.ts | 1 + src/compiler/parser/index.js | 38 +++++++++++---- test/unit/modules/compiler/parser.spec.js | 41 +++++++++++++++++ 6 files changed, 117 insertions(+), 13 deletions(-) diff --git a/flow/compiler.js b/flow/compiler.js index a7acacee595..de1e2df9a4a 100644 --- a/flow/compiler.js +++ b/flow/compiler.js @@ -6,7 +6,8 @@ declare type CompilerOptions = { isUnaryTag?: (tag: string) => ?boolean; // check if a tag is unary for the platform canBeLeftOpenTag?: (tag: string) => ?boolean; // check if a tag can be left opened isReservedTag?: (tag: string) => ?boolean; // check if a tag is a native for the platform - preserveWhitespace?: boolean; // preserve whitespace between elements? + preserveWhitespace?: boolean; // preserve whitespace between elements? (Deprecated) + whitespace?: 'preserve' | 'condense'; // whitespace handling strategy optimize?: boolean; // optimize static content? // web specific diff --git a/packages/vue-template-compiler/README.md b/packages/vue-template-compiler/README.md index 41d9b58b858..922622e5d4e 100644 --- a/packages/vue-template-compiler/README.md +++ b/packages/vue-template-compiler/README.md @@ -33,12 +33,48 @@ Note the returned function code uses `with` and thus cannot be used in strict mo #### Options -It's possible to hook into the compilation process to support custom template features. **However, beware that by injecting custom compile-time modules, your templates will not work with other build tools built on standard built-in modules, e.g `vue-loader` and `vueify`.** +- `whitespace` + - Type: `string` + - Valid values: `'preserve' | 'condense'` + - Default: `'preserve'` -The optional `options` object can contain the following: + The default value `'preserve'` handles whitespaces as follows: + + - A whitespace-only text node between element tags is condensed into a single space. + - All other whitespaces are preserved as-is. + + If set to `'condense'`: + + - A whitespace-only text node between element tags is removed if it contains new lines. Otherwise, it is condensed into a single space. + - Consecutive whitespaces inside a non-whitespace-only text node is condensed into a single space. + + Using condense mode will result in smaller compiled code size and slightly improved performance. However, it will produce minor visual layout differences compared to plain HTML in certain cases. + + **This option does not affect the `
` tag.**
+
+  Example:
+
+  ``` html
+  
+  
+ + foo + bar +
+ + +
+ foo + bar
+ + +
foo bar
+ ``` - `modules` + It's possible to hook into the compilation process to support custom template features. **However, beware that by injecting custom compile-time modules, your templates will not work with other build tools built on standard built-in modules, e.g `vue-loader` and `vueify`.** + An array of compiler modules. For details on compiler modules, refer to the `ModuleOptions` type in [flow declarations](https://github.com/vuejs/vue/blob/dev/flow/compiler.js#L38-L45) and the [built-in modules](https://github.com/vuejs/vue/tree/dev/src/platforms/web/compiler/modules). - `directives` @@ -59,9 +95,11 @@ The optional `options` object can contain the following: Refer to the implementation of some [built-in compile-time directives](https://github.com/vuejs/vue/tree/dev/src/platforms/web/compiler/directives). -- `preserveWhitespace` +- `preserveWhitespace` **Deprecated since 2.6** + - Type: `boolean` + - Default: `true` - Defaults to `true`. This means the compiled render function preserves all whitespace characters between HTML tags. If set to `false`, whitespace between tags will be ignored. This can result in slightly better performance but may affect layout for inline elements. + By default, the compiled render function preserves all whitespace characters between HTML tags. If set to `false`, whitespace between tags will be ignored. This can result in slightly better performance but may affect layout for inline elements. --- diff --git a/packages/vue-template-compiler/types/index.d.ts b/packages/vue-template-compiler/types/index.d.ts index 0d1c810ba3f..936b1c91c84 100644 --- a/packages/vue-template-compiler/types/index.d.ts +++ b/packages/vue-template-compiler/types/index.d.ts @@ -7,6 +7,7 @@ interface CompilerOptions { modules?: ModuleOptions[]; directives?: Record; preserveWhitespace?: boolean; + whitespace?: 'preserve' | 'condense'; } interface CompiledResult { diff --git a/packages/vue-template-compiler/types/test.ts b/packages/vue-template-compiler/types/test.ts index be00815c78f..cf2c1be6b9b 100644 --- a/packages/vue-template-compiler/types/test.ts +++ b/packages/vue-template-compiler/types/test.ts @@ -10,6 +10,7 @@ import { // check compile options const compiled = compile("
hi
", { preserveWhitespace: false, + whitespace: 'condense', modules: [ { preTransformNode: el => el, diff --git a/src/compiler/parser/index.js b/src/compiler/parser/index.js index 8900a1d90f9..55b8da7cf4b 100644 --- a/src/compiler/parser/index.js +++ b/src/compiler/parser/index.js @@ -30,6 +30,9 @@ const argRE = /:(.*)$/ export const bindRE = /^:|^v-bind:/ const modifierRE = /\.[^.]+/g +const lineBreakRE = /[\r\n]/ +const whitespaceRE = /\s+/g + const decodeHTMLCached = cached(he.decode) // configurable state @@ -79,6 +82,7 @@ export function parse ( const stack = [] const preserveWhitespace = options.preserveWhitespace !== false + const whitespaceOption = options.whitespace let root let currentParent let inVPre = false @@ -235,11 +239,13 @@ export function parse ( }, end (tag, start, end) { - // remove trailing whitespace const element = stack[stack.length - 1] - const lastNode = element.children[element.children.length - 1] - if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) { - element.children.pop() + if (!inPre) { + // remove trailing whitespace node + const lastNode = element.children[element.children.length - 1] + if (lastNode && lastNode.type === 3 && lastNode.text === ' ') { + element.children.pop() + } } // pop stack stack.length -= 1 @@ -276,11 +282,27 @@ export function parse ( return } const children = currentParent.children - text = inPre || text.trim() - ? isTextTag(currentParent) ? text : decodeHTMLCached(text) - // only preserve whitespace if its not right after a starting tag - : preserveWhitespace && children.length ? ' ' : '' + if (inPre || text.trim()) { + text = isTextTag(currentParent) ? text : decodeHTMLCached(text) + } else if (!children.length) { + // remove the whitespace-only node right after an opening tag + text = '' + } else if (whitespaceOption) { + if (whitespaceOption === 'condense') { + // in condense mode, remove the whitespace node if it contains + // line break, otherwise condense to a single space + text = lineBreakRE.test(text) ? '' : ' ' + } else { + text = ' ' + } + } else { + text = preserveWhitespace ? ' ' : '' + } if (text) { + if (whitespaceOption === 'condense') { + // condense consecutive whitespaces into single space + text = text.replace(whitespaceRE, ' ') + } let res let child: ?ASTNode if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) { diff --git a/test/unit/modules/compiler/parser.spec.js b/test/unit/modules/compiler/parser.spec.js index efa5daf4eb3..fd67677948f 100644 --- a/test/unit/modules/compiler/parser.spec.js +++ b/test/unit/modules/compiler/parser.spec.js @@ -757,4 +757,45 @@ describe('parser', () => { const ast = parse(`

{{\r\nmsg\r\n}}

`, baseOptions) expect(ast.children[0].expression).toBe('_s(msg)') }) + + it('preserveWhitespace: false', () => { + const options = extend({ + preserveWhitespace: false + }, baseOptions) + + const ast = parse('

\n Welcome to Vue.js world \n .\n Have fun!\n

', options) + expect(ast.tag).toBe('p') + expect(ast.children.length).toBe(4) + expect(ast.children[0].type).toBe(3) + expect(ast.children[0].text).toBe('\n Welcome to ') + expect(ast.children[1].tag).toBe('b') + expect(ast.children[1].children[0].text).toBe('Vue.js') + expect(ast.children[2].tag).toBe('i') + expect(ast.children[2].children[0].text).toBe('world') + expect(ast.children[3].tag).toBe('span') + expect(ast.children[3].children[0].text).toBe('.\n Have fun!\n') + }) + + it(`whitespace: 'condense'`, () => { + const options = extend({ + whitespace: 'condense', + // should be ignored when whitespace is specified + preserveWhitespace: false + }, baseOptions) + const ast = parse('

\n Welcome to Vue.js world \n .\n Have fun!\n

', options) + expect(ast.tag).toBe('p') + expect(ast.children.length).toBe(5) + expect(ast.children[0].type).toBe(3) + expect(ast.children[0].text).toBe(' Welcome to ') + expect(ast.children[1].tag).toBe('b') + expect(ast.children[1].children[0].text).toBe('Vue.js') + expect(ast.children[2].type).toBe(3) + // should condense inline whitespace into single space + expect(ast.children[2].text).toBe(' ') + expect(ast.children[3].tag).toBe('i') + expect(ast.children[3].children[0].text).toBe('world') + // should have removed the whitespace node between tags that contains newlines + expect(ast.children[4].tag).toBe('span') + expect(ast.children[4].children[0].text).toBe('. Have fun! ') + }) })