diff --git a/changelog_unreleased/angular/20117.md b/changelog_unreleased/angular/20117.md
new file mode 100644
index 000000000..c7e061f04
--- /dev/null
+++ b/changelog_unreleased/angular/20117.md
@@ -0,0 +1,17 @@
+#### Preserve double-brace placeholders inside string literals (#20117 by @splincode)
+
+
+```html
+
+{{ "{{num_of_selected_files}} / {{num_of_total_files}} files selected" | translate: {num_of_selected_files: 20, num_of_total_files: 30} }}
+
+
+{{ "{{num_of_selected_files}} / {{ num_of_total_files }} files selected" |
+translate: {num_of_selected_files: 20, num_of_total_files: 30} }}
+
+
+{{
+ "{{num_of_selected_files}} / {{num_of_total_files}} files selected"
+ | translate: { num_of_selected_files: 20, num_of_total_files: 30 }
+}}
+```
diff --git a/changelog_unreleased/vue/20117-2.md b/changelog_unreleased/vue/20117-2.md
new file mode 100644
index 000000000..da6ec0a33
--- /dev/null
+++ b/changelog_unreleased/vue/20117-2.md
@@ -0,0 +1,19 @@
+#### Preserve double-brace placeholders inside string literals (#20117 by @splincode)
+
+
+```html
+
+
+ {{"{{first}} / {{second}} files selected"}}
+
+
+
+
+ {{"{{first}} / {{ second }} files selected"}}
+
+
+
+
+ {{ "{{first}} / {{second}} files selected" }}
+
+```
diff --git a/package.json b/package.json
index b05255b46..6ee6dcb46 100644
--- a/package.json
+++ b/package.json
@@ -61,7 +61,7 @@
"acorn": "8.18.0",
"acorn-jsx": "5.3.2",
"angular-estree-parser": "15.5.0",
- "angular-html-parser": "10.12.0",
+ "angular-html-parser": "10.13.0",
"camelcase": "9.0.0",
"ci-info": "4.4.0",
"cjk-regex": "3.5.0",
diff --git a/src/language-html/print-preprocess.js b/src/language-html/print-preprocess.js
index f7710a2ea..eafbe4c02 100644
--- a/src/language-html/print-preprocess.js
+++ b/src/language-html/print-preprocess.js
@@ -1,4 +1,4 @@
-import { ParseSourceSpan } from "angular-html-parser";
+import { ParseSourceSpan, TokenType } from "angular-html-parser";
import htmlWhitespace from "../utilities/html-whitespace.js";
import isNonEmptyArray from "../utilities/is-non-empty-array.js";
import {
@@ -273,7 +273,6 @@ function extractInterpolation(ast, options) {
return;
}
- const interpolationRegex = /\{\{(.+?)\}\}/s;
ast.walk((node) => {
if (!canHaveInterpolation(node, options)) {
return;
@@ -286,7 +285,8 @@ function extractInterpolation(ast, options) {
let startSourceSpan = child.sourceSpan.start;
let endSourceSpan;
- const components = child.value.split(interpolationRegex);
+ const components = splitInterpolation(child);
+
for (
let i = 0;
i < components.length;
@@ -331,6 +331,37 @@ function extractInterpolation(ast, options) {
});
}
+function splitInterpolation(node) {
+ const interpolationTokens = node.tokens?.filter(
+ (token) =>
+ token.type === TokenType.INTERPOLATION && token.parts.length === 3,
+ );
+
+ if (!interpolationTokens?.some((token) => token.parts[1].includes("}}"))) {
+ return node.value.split(/\{\{(.+?)\}\}/s);
+ }
+
+ const components = [];
+ const { content } = node.sourceSpan.start.file;
+ let startOffset = node.sourceSpan.start.offset;
+
+ for (const { parts, sourceSpan } of interpolationTokens) {
+ components.push(
+ content.slice(startOffset, sourceSpan.start.offset),
+ content.slice(
+ sourceSpan.start.offset + parts[0].length,
+ sourceSpan.end.offset - parts[2].length,
+ ),
+ );
+
+ startOffset = sourceSpan.end.offset;
+ }
+
+ components.push(content.slice(startOffset, node.sourceSpan.end.offset));
+
+ return components;
+}
+
/**
* - add `hasLeadingSpaces` field
* - add `hasTrailingSpaces` field
diff --git a/tests/format/angular/interpolation/html/18939.html b/tests/format/angular/interpolation/html/18939.html
new file mode 100644
index 000000000..b1b7b18d4
--- /dev/null
+++ b/tests/format/angular/interpolation/html/18939.html
@@ -0,0 +1,2 @@
+{{}}
+{{ "{{num_of_selected_files}} / {{num_of_total_files}} files selected" | translate: {num_of_selected_files: 20, num_of_total_files: 30} }}
diff --git a/tests/format/angular/interpolation/html/__snapshots__/format.test.js.snap b/tests/format/angular/interpolation/html/__snapshots__/format.test.js.snap
index 944a8cd6b..b7127688c 100644
--- a/tests/format/angular/interpolation/html/__snapshots__/format.test.js.snap
+++ b/tests/format/angular/interpolation/html/__snapshots__/format.test.js.snap
@@ -16,6 +16,24 @@ parsers: ["angular"]
================================================================================
`;
+exports[`18939.html format 1`] = `
+====================================options=====================================
+parsers: ["angular"]
+ printWidth: 80 (default) |
+=====================================input======================================
+{{}}
+{{ "{{num_of_selected_files}} / {{num_of_total_files}} files selected" | translate: {num_of_selected_files: 20, num_of_total_files: 30} }}
+
+=====================================output=====================================
+{{}}
+{{
+ "{{num_of_selected_files}} / {{num_of_total_files}} files selected"
+ | translate: { num_of_selected_files: 20, num_of_total_files: 30 }
+}}
+
+================================================================================
+`;
+
exports[`block.html format 1`] = `
====================================options=====================================
parsers: ["angular"]
diff --git a/tests/format/vue/interpolation/__snapshots__/format.test.js.snap b/tests/format/vue/interpolation/__snapshots__/format.test.js.snap
index fef3ec101..93777ecdb 100644
--- a/tests/format/vue/interpolation/__snapshots__/format.test.js.snap
+++ b/tests/format/vue/interpolation/__snapshots__/format.test.js.snap
@@ -75,6 +75,27 @@ parsers: ["vue"]
================================================================================
`;
+exports[`double-brace-string.vue format 1`] = `
+====================================options=====================================
+parsers: ["vue"]
+ printWidth: 80 (default) |
+=====================================input======================================
+
+ {{"{{first}} / {{second}} files selected"}}
+ {{'{{first}} / {{second}} files selected'}}
+ Before {{ "}}" }} between {{value}} after {{ "{{third}}" }}
+
+
+=====================================output=====================================
+
+ {{ "{{first}} / {{second}} files selected" }}
+ {{ "{{first}} / {{second}} files selected" }}
+ Before {{ "}}" }} between {{ value }} after {{ "{{third}}" }}
+
+
+================================================================================
+`;
+
exports[`ng-non-bindable.vue format 1`] = `
====================================options=====================================
parsers: ["vue"]
diff --git a/tests/format/vue/interpolation/double-brace-string.vue b/tests/format/vue/interpolation/double-brace-string.vue
new file mode 100644
index 000000000..1fd2d201b
--- /dev/null
+++ b/tests/format/vue/interpolation/double-brace-string.vue
@@ -0,0 +1,5 @@
+
+ {{"{{first}} / {{second}} files selected"}}
+ {{'{{first}} / {{second}} files selected'}}
+ Before {{ "}}" }} between {{value}} after {{ "{{third}}" }}
+
diff --git a/yarn.lock b/yarn.lock
index e81e4451e..0403d72cb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3372,10 +3372,10 @@ __metadata:
languageName: node
linkType: hard
-"angular-html-parser@npm:10.12.0":
- version: 10.12.0
- resolution: "angular-html-parser@npm:10.12.0"
- checksum: 10/d71e0faa33c3ad91d07a71f4c5878eb047bb42f506fef3b66d15c5dd94a59eba12599de186adf4518ebe28a2f58b669394943281b91aebefed35987d8e29cbbe
+"angular-html-parser@npm:10.13.0":
+ version: 10.13.0
+ resolution: "angular-html-parser@npm:10.13.0"
+ checksum: 10/e2d37993e888469d061145f2880f523a74b0bbfd772438cd3b17b435b5be7f648ce83d2e83714a8322f755f5b2275fae8ff31b9c9ec37faf6a840d9ea8ce6d86
languageName: node
linkType: hard
@@ -8288,7 +8288,7 @@ __metadata:
acorn: "npm:8.18.0"
acorn-jsx: "npm:5.3.2"
angular-estree-parser: "npm:15.5.0"
- angular-html-parser: "npm:10.12.0"
+ angular-html-parser: "npm:10.13.0"
browserslist: "npm:4.29.0"
browserslist-to-esbuild: "npm:2.1.1"
buffer: "npm:6.0.3"