From 0292df6bf356a577de0dc72e6114c8a801978552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=98=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2?= Date: Sun, 20 Sep 2026 03:43:36 +0300 Subject: [PATCH] Preserve Angular control flow syntax in `ngNonBindable` directive (#20066) Co-authored-by: fisker --- changelog_unreleased/angular/20066.md | 20 ++++ src/language-html/print-preprocess.js | 83 ++++++++++++++ .../__snapshots__/format.test.js.snap | 101 +++++++++++++++++- .../angular/ng-non-bindable/control-flow.html | 23 ++++ ...g-non-bindable.html => interpolation.html} | 2 + .../ng-non-bindable/let-declaration.html | 13 +++ 6 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 changelog_unreleased/angular/20066.md create mode 100644 tests/format/angular/ng-non-bindable/control-flow.html rename tests/format/angular/ng-non-bindable/{ng-non-bindable.html => interpolation.html} (84%) create mode 100644 tests/format/angular/ng-non-bindable/let-declaration.html diff --git a/changelog_unreleased/angular/20066.md b/changelog_unreleased/angular/20066.md new file mode 100644 index 000000000..d227f66c7 --- /dev/null +++ b/changelog_unreleased/angular/20066.md @@ -0,0 +1,20 @@ +#### Preserve Angular syntax inside elements with `ngNonBindable` directive (#20066 by @splincode) + + +```html + +@if(foo){bar} +@let foo=bar+baz; + + + + @if (foo) { + bar + } + +@let foo = bar + baz; + + +@if(foo){bar} +@let foo=bar+baz; +``` diff --git a/src/language-html/print-preprocess.js b/src/language-html/print-preprocess.js index 487b012b8..61b3ad0fd 100644 --- a/src/language-html/print-preprocess.js +++ b/src/language-html/print-preprocess.js @@ -13,6 +13,7 @@ import { } from "./utilities/index.js"; const PREPROCESS_PIPELINE = [ + convertAngularNonBindableNodesToText, removeIgnorableFirstLf, mergeIfConditionalStartEndCommentIntoElementOpeningTag, mergeCdataIntoText, @@ -97,6 +98,88 @@ function mergeIfConditionalStartEndCommentIntoElementOpeningTag( }); } +function convertAngularNonBindableNodesToText(ast, options) { + if ( + options.parser !== "angular" || + !options.originalText.includes("ngNonBindable") + ) { + return; + } + + ast.walk((node) => { + if ( + node.kind === "element" && + Object.hasOwn(node.attrMap, "ngNonBindable") + ) { + convertAngularNonBindableChildren(node, options); + } + }); +} + +// TODO: Avoid recursive call +function convertAngularNonBindableChildren(node, options) { + const { children } = node; + if (!children) { + return; + } + + for (let i = 0; i < children.length; i++) { + const child = children[i]; + + if (child.kind === "angularControlFlowBlock") { + convertAngularNonBindableChildren(child, options); + + const replacements = []; + let { start } = child.sourceSpan; + for (const blockChild of child.children) { + if (start.offset < blockChild.sourceSpan.start.offset) { + const sourceSpan = new ParseSourceSpan( + start, + blockChild.sourceSpan.start, + ); + replacements.push(createOriginalTextNode(sourceSpan, options)); + } + + replacements.push(blockChild); + start = blockChild.sourceSpan.end; + } + + if (start.offset < child.sourceSpan.end.offset) { + const sourceSpan = new ParseSourceSpan(start, child.sourceSpan.end); + replacements.push(createOriginalTextNode(sourceSpan, options)); + } + + children.splice( + i, + 1, + ...replacements.map((replacement) => node.createChild(replacement)), + ); + i += replacements.length - 1; + continue; + } + + if (child.kind === "angularLetDeclaration") { + children[i] = node.createChild( + createOriginalTextNode(child.sourceSpan, options), + ); + continue; + } + + convertAngularNonBindableChildren(child, options); + } +} + +function createOriginalTextNode(sourceSpan, options) { + return { + kind: "text", + value: options.originalText.slice( + sourceSpan.start.offset, + sourceSpan.end.offset, + ), + sourceSpan, + }; +} + function mergeNodeIntoText(ast, shouldMerge, getValue) { ast.walk((node) => { if (node.children) { diff --git a/tests/format/angular/ng-non-bindable/__snapshots__/format.test.js.snap b/tests/format/angular/ng-non-bindable/__snapshots__/format.test.js.snap index 3ae72e8ca..b92b175cb 100644 --- a/tests/format/angular/ng-non-bindable/__snapshots__/format.test.js.snap +++ b/tests/format/angular/ng-non-bindable/__snapshots__/format.test.js.snap @@ -1,6 +1,62 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`ng-non-bindable.html format 1`] = ` +exports[`control-flow.html format 1`] = ` +====================================options===================================== +parsers: ["angular"] + printWidth: 80 (default) | +=====================================input====================================== +
@if ( foo ){ bar }
+
_if ( foo )[ bar ]
+ @if ( foo ){ bar } + _if ( foo )[ bar ] +
   @if   (   foo   ){   bar   }   
+
   _if   (   foo   )[  bar   ]   
+ +
text @if ( foo ){ bar } text
+ text @if ( foo ){ bar } text +
   text  @if   (   foo   ){   bar   }  text 
+ +
@if ( foo ){ + + }
+ +
@if (foo ){text +}
+ +
@if ( foo ){ bar }
+ +=====================================output===================================== +
@if ( foo ){ bar }
+
_if ( foo )[ bar ]
+ @if ( foo ){ bar } + _if ( foo )[ bar ] +
   @if   (   foo   ){   bar   }   
+
   _if   (   foo   )[  bar   ]   
+ +
text @if ( foo ){ bar } text
+ text @if ( foo ){ bar } text +
   text  @if   (   foo   ){   bar   }  text 
+ +
+ @if ( foo ){ + + + + } +
+ +
@if (foo ){text }
+ +
@if ( foo ){ bar }
+ +================================================================================ +`; + +exports[`interpolation.html format 1`] = ` ====================================options===================================== parsers: ["angular"] printWidth: 80 (default) | @@ -20,6 +76,8 @@ parsers: ["angular"] } +
{{foo   +bar}}
+ =====================================output===================================== {{foo +bar}} @@ -32,9 +90,48 @@ parsers: ["angular"] @if (A) { - {{foo +bar}} + {{foo +bar}} } +
{{foo   +bar}}
+ +================================================================================ +`; + +exports[`let-declaration.html format 1`] = ` +====================================options===================================== +parsers: ["angular"] + printWidth: 80 (default) | +=====================================input====================================== +
@let foo = bar+baz;
+
_let foo = bar+baz;
+ @let foo = bar+baz; + _let foo = bar+baz; +
   @let   foo   =    bar+baz;   
+
   _let   foo   =    bar+baz;   
+ +
text @let foo = bar+baz; text
+ text @let foo = bar+baz; text +
   text  @let   foo   =    bar+baz;  text 
+ +@if(foo){bar} +
@let foo = bar+baz;
+ +=====================================output===================================== +
@let foo = bar+baz;
+
_let foo = bar+baz;
+ @let foo = bar+baz; + _let foo = bar+baz; +
   @let   foo   =    bar+baz;   
+
   _let   foo   =    bar+baz;   
+ +
text @let foo = bar+baz; text
+ text @let foo = bar+baz; text +
   text  @let   foo   =    bar+baz;  text 
+ +@if(foo){bar} +
@let foo = bar+baz;
+ ================================================================================ `; diff --git a/tests/format/angular/ng-non-bindable/control-flow.html b/tests/format/angular/ng-non-bindable/control-flow.html new file mode 100644 index 000000000..b41e3789e --- /dev/null +++ b/tests/format/angular/ng-non-bindable/control-flow.html @@ -0,0 +1,23 @@ +
@if ( foo ){ bar }
+
_if ( foo )[ bar ]
+ @if ( foo ){ bar } + _if ( foo )[ bar ] +
   @if   (   foo   ){   bar   }   
+
   _if   (   foo   )[  bar   ]   
+ +
text @if ( foo ){ bar } text
+ text @if ( foo ){ bar } text +
   text  @if   (   foo   ){   bar   }  text 
+ +
@if ( foo ){ + + }
+ +
@if (foo ){text +}
+ +
@if ( foo ){ bar }
diff --git a/tests/format/angular/ng-non-bindable/ng-non-bindable.html b/tests/format/angular/ng-non-bindable/interpolation.html similarity index 84% rename from tests/format/angular/ng-non-bindable/ng-non-bindable.html rename to tests/format/angular/ng-non-bindable/interpolation.html index f4caf08c5..5f7abb049 100644 --- a/tests/format/angular/ng-non-bindable/ng-non-bindable.html +++ b/tests/format/angular/ng-non-bindable/interpolation.html @@ -12,3 +12,5 @@ {{foo +bar}} } + +
{{foo   +bar}}
diff --git a/tests/format/angular/ng-non-bindable/let-declaration.html b/tests/format/angular/ng-non-bindable/let-declaration.html new file mode 100644 index 000000000..c589d381a --- /dev/null +++ b/tests/format/angular/ng-non-bindable/let-declaration.html @@ -0,0 +1,13 @@ +
@let foo = bar+baz;
+
_let foo = bar+baz;
+ @let foo = bar+baz; + _let foo = bar+baz; +
   @let   foo   =    bar+baz;   
+
   _let   foo   =    bar+baz;   
+ +
text @let foo = bar+baz; text
+ text @let foo = bar+baz; text +
   text  @let   foo   =    bar+baz;  text 
+ +@if(foo){bar} +
@let foo = bar+baz;