diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md
index 34391e12..a5faa7d4 100644
--- a/CHANGELOG.unreleased.md
+++ b/CHANGELOG.unreleased.md
@@ -363,6 +363,26 @@ new (x())!.y();
new e[f().x].y();
```
+### JavaScript: Fix nested embeds (JS in HTML in JS) ([#6038] by [@thorn0])
+
+Previously, if JS code embedded in HTML (via ``;
+
+// Output (Prettier stable)
+// SyntaxError: Expecting Unicode escape sequence \uXXXX (1:8)
+
+// Output (Prettier master)
+const html = /* HTML */ `
+
+`;
+```
+
### TypeScript: Keep line breaks within mapped types.([#6146] by [@sosukesuzuki])
Previously, Prettier has removed line breaks within mapped types.This change keeps it, similar to how it treats other object types.
@@ -429,6 +449,7 @@ f[a::b];
[#6133]: https://github.com/prettier/prettier/pull/6133
[#6136]: https://github.com/prettier/prettier/pull/6136
[#6140]: https://github.com/prettier/prettier/pull/6140
+[#6038]: https://github.com/prettier/prettier/pull/6038
[#6148]: https://github.com/prettier/prettier/pull/6148
[#6146]: https://github.com/prettier/prettier/pull/6146
[#6152]: https://github.com/prettier/prettier/pull/6152
diff --git a/src/common/util.js b/src/common/util.js
index 10c0a81b..45df067b 100644
--- a/src/common/util.js
+++ b/src/common/util.js
@@ -500,10 +500,7 @@ function printString(raw, options, isDirectiveLiteral) {
options.parser === "css" ||
options.parser === "less" ||
options.parser === "scss" ||
- options.parentParser === "html" ||
- options.parentParser === "vue" ||
- options.parentParser === "angular" ||
- options.parentParser === "lwc"
+ options.embeddedInHtml
)
);
}
diff --git a/src/language-html/printer-html.js b/src/language-html/printer-html.js
index ce5a8fab..115f015b 100644
--- a/src/language-html/printer-html.js
+++ b/src/language-html/printer-html.js
@@ -113,7 +113,7 @@ function embed(path, print, textToDoc, options) {
// lit-html: html``
if (
- /^PRETTIER_HTML_PLACEHOLDER_\d+_IN_JS$/.test(
+ /^PRETTIER_HTML_PLACEHOLDER_\d+_\d+_IN_JS$/.test(
options.originalText.slice(
node.valueSpan.start.offset,
node.valueSpan.end.offset
diff --git a/src/language-js/embed.js b/src/language-js/embed.js
index 45090e71..57a56a62 100644
--- a/src/language-js/embed.js
+++ b/src/language-js/embed.js
@@ -16,7 +16,7 @@ const {
utils: { mapDoc, stripTrailingHardline }
} = require("../doc");
-function embed(path, print, textToDoc /*, options */) {
+function embed(path, print, textToDoc, options) {
const node = path.getValue();
const parent = path.getParentNode();
const parentParent = path.getParentNode(1);
@@ -135,12 +135,20 @@ function embed(path, print, textToDoc /*, options */) {
]);
}
- if (isHtml(path)) {
- return printHtmlTemplateLiteral(path, print, textToDoc, "html");
- }
+ const htmlParser = isHtml(path)
+ ? "html"
+ : isAngularComponentTemplate(path)
+ ? "angular"
+ : undefined;
- if (isAngularComponentTemplate(path)) {
- return printHtmlTemplateLiteral(path, print, textToDoc, "angular");
+ if (htmlParser) {
+ return printHtmlTemplateLiteral(
+ path,
+ print,
+ textToDoc,
+ htmlParser,
+ options.embeddedInHtml
+ );
}
break;
@@ -195,6 +203,10 @@ function getIndentation(str) {
return firstMatchedIndent === null ? "" : firstMatchedIndent[1];
}
+function uncook(cookedValue) {
+ return cookedValue.replace(/([\\`]|\$\{)/g, "\\$1");
+}
+
function escapeTemplateCharacters(doc, raw) {
return mapDoc(doc, currentDoc => {
if (!currentDoc.parts) {
@@ -205,11 +217,7 @@ function escapeTemplateCharacters(doc, raw) {
currentDoc.parts.forEach(part => {
if (typeof part === "string") {
- parts.push(
- raw
- ? part.replace(/(\\*)`/g, "$1$1\\`")
- : part.replace(/([\\`]|\$\{)/g, "\\$1")
- );
+ parts.push(raw ? part.replace(/(\\*)`/g, "$1$1\\`") : uncook(part));
} else {
parts.push(part);
}
@@ -576,19 +584,29 @@ function isHtml(path) {
);
}
-function printHtmlTemplateLiteral(path, print, textToDoc, parser) {
+// The counter is needed to distinguish nested embeds.
+let htmlTemplateLiteralCounter = 0;
+
+function printHtmlTemplateLiteral(
+ path,
+ print,
+ textToDoc,
+ parser,
+ escapeClosingScriptTag
+) {
const node = path.getValue();
- const placeholderPattern = "PRETTIER_HTML_PLACEHOLDER_(\\d+)_IN_JS";
- const placeholders = node.expressions.map(
- (_, i) => `PRETTIER_HTML_PLACEHOLDER_${i}_IN_JS`
- );
+ const counter = htmlTemplateLiteralCounter;
+ htmlTemplateLiteralCounter = (htmlTemplateLiteralCounter + 1) >>> 0;
+
+ const composePlaceholder = index =>
+ `PRETTIER_HTML_PLACEHOLDER_${index}_${counter}_IN_JS`;
const text = node.quasis
.map((quasi, index, quasis) =>
index === quasis.length - 1
- ? quasi.value.raw
- : quasi.value.raw + placeholders[index]
+ ? quasi.value.cooked
+ : quasi.value.cooked + composePlaceholder(index)
)
.join("");
@@ -598,14 +616,12 @@ function printHtmlTemplateLiteral(path, print, textToDoc, parser) {
return "``";
}
+ const placeholderRegex = RegExp(composePlaceholder("(\\d+)"), "g");
+
const contentDoc = mapDoc(
stripTrailingHardline(textToDoc(text, { parser })),
doc => {
- const placeholderRegex = new RegExp(placeholderPattern, "g");
- const hasPlaceholder =
- typeof doc === "string" && placeholderRegex.test(doc);
-
- if (!hasPlaceholder) {
+ if (typeof doc !== "string") {
return doc;
}
@@ -613,10 +629,14 @@ function printHtmlTemplateLiteral(path, print, textToDoc, parser) {
const components = doc.split(placeholderRegex);
for (let i = 0; i < components.length; i++) {
- const component = components[i];
+ let component = components[i];
if (i % 2 === 0) {
if (component) {
+ component = uncook(component);
+ if (escapeClosingScriptTag) {
+ component = component.replace(/<\/(script)\b/gi, "<\\/$1");
+ }
parts.push(component);
}
continue;
diff --git a/src/language-js/needs-parens.js b/src/language-js/needs-parens.js
index 1d88b910..a9cc0e58 100644
--- a/src/language-js/needs-parens.js
+++ b/src/language-js/needs-parens.js
@@ -129,6 +129,18 @@ function needsParens(path, options) {
// Identifiers never need parentheses.
if (node.type === "Identifier") {
+ // ...unless those identifiers are embed placeholders. They might be substituted by complex
+ // expressions, so the parens around them should not be dropped. Example (JS-in-HTML-in-JS):
+ // let tpl = html``;
+ // If the inner JS formatter removes the parens, the expression might change its meaning:
+ // f((a + b) / 2) vs f(a + b / 2)
+ if (
+ node.extra &&
+ node.extra.parenthesized &&
+ /^PRETTIER_HTML_PLACEHOLDER_\d+_\d+_IN_JS$/.test(node.name)
+ ) {
+ return true;
+ }
return false;
}
diff --git a/src/main/multiparser.js b/src/main/multiparser.js
index c0f53a82..16e5fb60 100644
--- a/src/main/multiparser.js
+++ b/src/main/multiparser.js
@@ -19,6 +19,13 @@ function textToDoc(text, partialNextOptions, parentOptions, printAstToDoc) {
const nextOptions = normalize(
Object.assign({}, parentOptions, partialNextOptions, {
parentParser: parentOptions.parser,
+ embeddedInHtml: !!(
+ parentOptions.embeddedInHtml ||
+ parentOptions.parser === "html" ||
+ parentOptions.parser === "vue" ||
+ parentOptions.parser === "angular" ||
+ parentOptions.parser === "lwc"
+ ),
originalText: text
}),
{ passThrough: true }
diff --git a/tests/multiparser_html_js/__snapshots__/jsfmt.spec.js.snap b/tests/multiparser_html_js/__snapshots__/jsfmt.spec.js.snap
index fbcd0e3c..30c347df 100644
--- a/tests/multiparser_html_js/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/multiparser_html_js/__snapshots__/jsfmt.spec.js.snap
@@ -31,3 +31,47 @@ printWidth: 80
================================================================================
`;
+
+exports[`script-tag-escaping.html 1`] = `
+====================================options=====================================
+parsers: ["html"]
+printWidth: 80
+ | printWidth
+=====================================input======================================
+
+
+=====================================output=====================================
+
+
+================================================================================
+`;
diff --git a/tests/multiparser_html_js/script-tag-escaping.html b/tests/multiparser_html_js/script-tag-escaping.html
new file mode 100644
index 00000000..575e07b2
--- /dev/null
+++ b/tests/multiparser_html_js/script-tag-escaping.html
@@ -0,0 +1,13 @@
+
diff --git a/tests/multiparser_js_html/__snapshots__/jsfmt.spec.js.snap b/tests/multiparser_js_html/__snapshots__/jsfmt.spec.js.snap
index 3a7734fa..8736fba1 100644
--- a/tests/multiparser_js_html/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/multiparser_js_html/__snapshots__/jsfmt.spec.js.snap
@@ -68,6 +68,17 @@ function HelloWorld() {
\`;
}
+const trickyParens = html\`\`;
+const nestedFun = /* HTML */ \`\${outerExpr( 1 )} \`;
+
+const closingScriptTagShouldBeEscapedProperly = /* HTML */ \`
+
+\`;
+
+const closingScriptTag2 = /* HTML */ \`
+\`;
+const nestedFun = /* HTML */ \`
+ \${outerExpr(1)}
+
+\`;
+
+const closingScriptTagShouldBeEscapedProperly = /* HTML */ \`
+
+\`;
+
+const closingScriptTag2 = /* HTML */ \`
+
+\`;
+
================================================================================
`;
diff --git a/tests/multiparser_js_html/lit-html.js b/tests/multiparser_js_html/lit-html.js
index e5a511c3..d1479072 100644
--- a/tests/multiparser_js_html/lit-html.js
+++ b/tests/multiparser_js_html/lit-html.js
@@ -59,3 +59,14 @@ function HelloWorld() {
`)}
`;
}
+
+const trickyParens = html``;
+const nestedFun = /* HTML */ `${outerExpr( 1 )} `;
+
+const closingScriptTagShouldBeEscapedProperly = /* HTML */ `
+
+`;
+
+const closingScriptTag2 = /* HTML */ `