diff --git a/src/common/fast-path.js b/src/common/fast-path.js index eb678a9a1..4c152b2f8 100644 --- a/src/common/fast-path.js +++ b/src/common/fast-path.js @@ -29,17 +29,18 @@ FastPath.prototype.getValue = function getValue() { }; function getNodeHelper(path, count) { - const s = path.stack; - - for (let i = s.length - 1; i >= 0; i -= 2) { - const value = s[i]; + const stackIndex = getNodeStackIndexHelper(path.stack, count); + return stackIndex === -1 ? null : path.stack[stackIndex]; +} +function getNodeStackIndexHelper(stack, count) { + for (let i = stack.length - 1; i >= 0; i -= 2) { + const value = stack[i]; if (value && !Array.isArray(value) && --count < 0) { - return value; + return i; } } - - return null; + return -1; } FastPath.prototype.getNode = function getNode(count) { @@ -70,6 +71,14 @@ FastPath.prototype.call = function call(callback /*, name1, name2, ... */) { return result; }; +FastPath.prototype.callParent = function callParent(callback, count) { + const stackIndex = getNodeStackIndexHelper(this.stack, ~~count + 1); + const parentValues = this.stack.splice(stackIndex + 1); + const result = callback(this); + Array.prototype.push.apply(this.stack, parentValues); + return result; +}; + // Similar to FastPath.prototype.call, except that the value obtained by // accessing this.getValue()[name1][name2]... should be array-like. The // callback will be called with a reference to this path object for each diff --git a/src/language-html/printer-html.js b/src/language-html/printer-html.js index c9bcada1a..896b54a37 100644 --- a/src/language-html/printer-html.js +++ b/src/language-html/printer-html.js @@ -85,11 +85,16 @@ function embed(path, print, textToDoc, options) { line, textToDoc( node.value, - options.parser === "angular" - ? { parser: "__ng_interpolation", trailingComma: "none" } - : options.parser === "vue" - ? { parser: "__vue_expression" } - : { parser: "__js_expression" } + Object.assign( + { + __isInHtmlInterpolation: true // to avoid unexpected `}}` + }, + options.parser === "angular" + ? { parser: "__ng_interpolation", trailingComma: "none" } + : options.parser === "vue" + ? { parser: "__vue_expression" } + : { parser: "__js_expression" } + ) ) ]) ), @@ -1043,7 +1048,10 @@ function printEmbeddedAttributeValue(node, originalTextToDoc, options) { indent( concat([ line, - ngTextToDoc(part, { parser: "__ng_interpolation" }) + ngTextToDoc(part, { + parser: "__ng_interpolation", + __isInHtmlInterpolation: true // to avoid unexpected `}}` + }) ]) ), line, diff --git a/src/language-js/needs-parens.js b/src/language-js/needs-parens.js index 68a92cced..402fee361 100644 --- a/src/language-js/needs-parens.js +++ b/src/language-js/needs-parens.js @@ -57,6 +57,16 @@ function needsParens(path, options) { return false; } + // to avoid unexpected `}}` in HTML interpolations + if ( + options.__isInHtmlInterpolation && + !options.bracketSpacing && + endsWithRightBracket(node) && + isFollowedByRightBracket(path) + ) { + return true; + } + // Only statements don't need parentheses. if (isStatement(node)) { return false; @@ -688,4 +698,55 @@ function isStatement(node) { ); } +function endsWithRightBracket(node) { + switch (node.type) { + case "ObjectExpression": + return true; + default: + return false; + } +} + +function isFollowedByRightBracket(path) { + const node = path.getValue(); + const parent = path.getParentNode(); + const name = path.getName(); + switch (parent.type) { + case "NGPipeExpression": + if ( + typeof name === "number" && + parent.arguments[name] === node && + parent.arguments.length - 1 === name + ) { + return path.callParent(isFollowedByRightBracket); + } + break; + case "ObjectProperty": + if (name === "value") { + const parentParent = path.getParentNode(1); + return ( + parentParent.properties[parentParent.properties.length - 1] === parent + ); + } + break; + case "BinaryExpression": + case "LogicalExpression": + if (name === "right") { + return path.callParent(isFollowedByRightBracket); + } + break; + case "ConditionalExpression": + if (name === "alternate") { + return path.callParent(isFollowedByRightBracket); + } + break; + case "UnaryExpression": + if (parent.prefix) { + return path.callParent(isFollowedByRightBracket); + } + break; + } + return false; +} + module.exports = needsParens; diff --git a/tests/html_angular/__snapshots__/jsfmt.spec.js.snap b/tests/html_angular/__snapshots__/jsfmt.spec.js.snap index 2bfac28a7..ec5d2dd29 100644 --- a/tests/html_angular/__snapshots__/jsfmt.spec.js.snap +++ b/tests/html_angular/__snapshots__/jsfmt.spec.js.snap @@ -61,6 +61,21 @@ printWidth: 80 ================================================================================ `; +exports[`attr-name.component.html 5`] = ` +====================================options===================================== +bracketSpacing: false +parsers: ["angular"] +printWidth: 80 + | printWidth +=====================================input====================================== +
+ +=====================================output===================================== + + +================================================================================ +`; + exports[`attributes.component.html 1`] = ` ====================================options===================================== parsers: ["angular"] @@ -1229,6 +1244,240 @@ printWidth: 80 ================================================================================ `; +exports[`attributes.component.html 5`] = ` +====================================options===================================== +bracketSpacing: false +parsers: ["angular"] +printWidth: 80 + | printWidth +=====================================input====================================== + + +=====================================output===================================== + + +================================================================================ +`; + exports[`first-lf.component.html 1`] = ` ====================================options===================================== parsers: ["angular"] @@ -1679,6 +1928,99 @@ printWidth: 80 ================================================================================ `; +exports[`first-lf.component.html 5`] = ` +====================================options===================================== +bracketSpacing: false +parsers: ["angular"] +printWidth: 80 + | printWidth +=====================================input====================================== + + + + + + + + + + + + + + + + + + + + + +=====================================output===================================== + + + + + + + + + + + + + + + + + + + + + +================================================================================ +`; + exports[`ignore-attribute.component.html 1`] = ` ====================================options===================================== parsers: ["angular"] @@ -1937,6 +2279,65 @@ printWidth: 80 ================================================================================ `; +exports[`ignore-attribute.component.html 5`] = ` +====================================options===================================== +bracketSpacing: false +parsers: ["angular"] +printWidth: 80 + | printWidth +=====================================input====================================== + + + + + + + + +=====================================output===================================== + + + + + + + + +================================================================================ +`; + exports[`interpolation.component.html 1`] = ` ====================================options===================================== parsers: ["angular"] @@ -1999,6 +2400,16 @@ printWidth: 80 {{ aNormalValue | aPipe }}: {{ aReallyReallySuperLongValue | andASuperLongPipeJustToBreakThis }} ++ {{ + 'delete' + | translate: {what: ('entities' | translate: {count: array.length})} + }} +
+{{ {a:1+{} } }}
+{{ {a:a==={} } }}
+{{ {a:!{} } }}
+{{ {a:a?b:{} } }}
=====================================output=====================================+ {{ + "delete" + | translate: { what: "entities" | translate: { count: array.length } } + }} +
+{{ { a: 1 + {} } }}
+{{ { a: a === {} } }}
+{{ { a: !{} } }}
+{{ { a: a ? b : {} } }}
================================================================================ `; @@ -2144,6 +2565,16 @@ trailingComma: "es5" {{ aNormalValue | aPipe }}: {{ aReallyReallySuperLongValue | andASuperLongPipeJustToBreakThis }} ++ {{ + 'delete' + | translate: {what: ('entities' | translate: {count: array.length})} + }} +
+{{ {a:1+{} } }}
+{{ {a:a==={} } }}
+{{ {a:!{} } }}
+{{ {a:a?b:{} } }}
=====================================output=====================================+ {{ + "delete" + | translate: { what: "entities" | translate: { count: array.length } } + }} +
+{{ { a: 1 + {} } }}
+{{ { a: a === {} } }}
+{{ { a: !{} } }}
+{{ { a: a ? b : {} } }}
================================================================================ `; @@ -2288,6 +2729,16 @@ printWidth: 1 {{ aNormalValue | aPipe }}: {{ aReallyReallySuperLongValue | andASuperLongPipeJustToBreakThis }} ++ {{ + 'delete' + | translate: {what: ('entities' | translate: {count: array.length})} + }} +
+{{ {a:1+{} } }}
+{{ {a:a==={} } }}
+{{ {a:!{} } }}
+{{ {a:a?b:{} } }}
=====================================output=====================================+ {{ + "delete" + | translate + : { + what: + "entities" + | translate + : { + count: + array.length + } + } + }} +
++ {{ + { + a: + 1 + + {} + } + }} +
++ {{ + { + a: + a === + {} + } + }} +
++ {{ + { + a: !{} + } + }} +
++ {{ + { + a: a + ? b + : {} + } + }} +
================================================================================ `; @@ -2595,6 +3095,16 @@ printWidth: 80 {{ aNormalValue | aPipe }}: {{ aReallyReallySuperLongValue | andASuperLongPipeJustToBreakThis }} ++ {{ + 'delete' + | translate: {what: ('entities' | translate: {count: array.length})} + }} +
+{{ {a:1+{} } }}
+{{ {a:a==={} } }}
+{{ {a:!{} } }}
+{{ {a:a?b:{} } }}
=====================================output=====================================+ {{ + "delete" + | translate: { what: "entities" | translate: { count: array.length } } + }} +
+{{ { a: 1 + {} } }}
+{{ { a: a === {} } }}
+{{ { a: !{} } }}
+{{ { a: a ? b : {} } }}
+ +================================================================================ +`; + +exports[`interpolation.component.html 5`] = ` +====================================options===================================== +bracketSpacing: false +parsers: ["angular"] +printWidth: 80 + | printWidth +=====================================input====================================== ++ {{ + 'delete' + | translate: {what: ('entities' | translate: {count: array.length})} + }} +
+{{ {a:1+{} } }}
+{{ {a:a==={} } }}
+{{ {a:!{} } }}
+{{ {a:a?b:{} } }}
+ +=====================================output===================================== ++ {{ + "delete" + | translate: {what: "entities" | translate: ({count: array.length})} + }} +
+{{ {a: 1 + ({})} }}
+{{ {a: a === ({})} }}
+{{ {a: !({})} }}
+{{ {a: a ? b : ({})} }}
================================================================================ `; @@ -10809,6 +11494,1535 @@ can be found in the LICENSE file at http://angular.io/license ================================================================================ `; +exports[`real-world.component.html 5`] = ` +====================================options===================================== +bracketSpacing: false +parsers: ["angular"] +printWidth: 80 + | printWidth +=====================================input====================================== + + + +My current hero is {{currentHero.name}}
+ +The sum of 1 + 1 is {{1 + 1}}
+ + +The sum of 1 + 1 is not {{1 + 1 + getVal()}}
+ +top + +Component expression context ({{title}}, [hidden]="isUnchanged")
+Template input variable expression context (let hero)
+ +Template reference variable expression context (#heroInput)
+Component statement context ( (click)="onSave() ) +
Template $event statement context
+Template input variable statement context (let hero)
+ +Template reference variable statement context (#heroForm)
+
+
+
+
+ is the interpolated image.
is the property bound image.
"{{title}}" is the interpolated title.
+"" is the property bound title.
+ + +"{{evilTitle}}" is the interpolated evil title.
+"" is the property bound evil title.
+ +top + + +| One-Two | |
| Five | Six |
currentClasses is {{currentClasses | json}}
+currentStyles is {{currentStyles | json}}
+with semi-colon separator
+with comma separator
+without trackBy
+with trackBy
+with trackBy and semi-colon separator
+with trackBy and comma separator
+with trackBy and space separator
+with generic trackById function
+Pick your favorite hero
+
+ The name of the Color.Red enum is {{Color[Color.Red]}}.
+ The current color is {{Color[color]}} and its number is {{color}}.
+
+
My current hero is {{ currentHero.name }}
+ +The sum of 1 + 1 is {{ 1 + 1 }}
+ + +The sum of 1 + 1 is not {{ 1 + 1 + getVal() }}
+ +top + ++ Component expression context ({{title}}, + [hidden]="isUnchanged") +
+Template input variable expression context (let hero)
+ +Template reference variable expression context (#heroInput)
+Component statement context ( (click)="onSave() )
+Template $event statement context
+Template input variable statement context (let hero)
+ +Template reference variable statement context (#heroForm)
+
+
+
+
+ is the interpolated image.
is the property bound image.
+ "{{ title }}" is the interpolated title. +
+"" is the property bound title.
+ + ++ "{{ evilTitle }}" is the interpolated evil title. +
++ "" is the property bound evil + title. +
+ +top + + +| One-Two | +|
| Five | +Six | +
currentClasses is {{ currentClasses | json }}
+currentStyles is {{ currentStyles | json }}
+with semi-colon separator
+with comma separator
+without trackBy
+with trackBy
+with trackBy and semi-colon separator
+with trackBy and comma separator
+with trackBy and space separator
+with generic trackById function
+Pick your favorite hero
+
+ The name of the Color.Red enum is {{ Color[Color.Red] }}.
+ The current color is {{ Color[color] }} and its number is {{ color }}.
+
+
+ {{ + 'delete' + | translate: {what: ('entities' | translate: {count: array.length})} + }} +
+{{ {a:1+{} } }}
+{{ {a:a==={} } }}
+{{ {a:!{} } }}
+{{ {a:a?b:{} } }}
diff --git a/tests/html_angular/jsfmt.spec.js b/tests/html_angular/jsfmt.spec.js index b3183c499..a9de42141 100644 --- a/tests/html_angular/jsfmt.spec.js +++ b/tests/html_angular/jsfmt.spec.js @@ -2,3 +2,4 @@ run_spec(__dirname, ["angular"]); run_spec(__dirname, ["angular"], { trailingComma: "es5" }); run_spec(__dirname, ["angular"], { printWidth: 1 }); run_spec(__dirname, ["angular"], { htmlWhitespaceSensitivity: "ignore" }); +run_spec(__dirname, ["angular"], { bracketSpacing: false });