fix: nested embeds (JS in HTML in JS) (#6038)
parent
f8875c1caa
commit
e4f0df5bed
|
@ -363,6 +363,26 @@ new (x())!.y();
|
||||||
new e[f().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 `<script>`) embedded in JS (via a template literal) contained template literals, the inner JS was not formatted.
|
||||||
|
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
```js
|
||||||
|
// Input
|
||||||
|
const html = /* HTML */ `<script>var a=\`\`</script>`;
|
||||||
|
|
||||||
|
// Output (Prettier stable)
|
||||||
|
// SyntaxError: Expecting Unicode escape sequence \uXXXX (1:8)
|
||||||
|
|
||||||
|
// Output (Prettier master)
|
||||||
|
const html = /* HTML */ `
|
||||||
|
<script>
|
||||||
|
var a = \`\`;
|
||||||
|
</script>
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
### TypeScript: Keep line breaks within mapped types.([#6146] by [@sosukesuzuki])
|
### 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.
|
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
|
[#6133]: https://github.com/prettier/prettier/pull/6133
|
||||||
[#6136]: https://github.com/prettier/prettier/pull/6136
|
[#6136]: https://github.com/prettier/prettier/pull/6136
|
||||||
[#6140]: https://github.com/prettier/prettier/pull/6140
|
[#6140]: https://github.com/prettier/prettier/pull/6140
|
||||||
|
[#6038]: https://github.com/prettier/prettier/pull/6038
|
||||||
[#6148]: https://github.com/prettier/prettier/pull/6148
|
[#6148]: https://github.com/prettier/prettier/pull/6148
|
||||||
[#6146]: https://github.com/prettier/prettier/pull/6146
|
[#6146]: https://github.com/prettier/prettier/pull/6146
|
||||||
[#6152]: https://github.com/prettier/prettier/pull/6152
|
[#6152]: https://github.com/prettier/prettier/pull/6152
|
||||||
|
|
|
@ -500,10 +500,7 @@ function printString(raw, options, isDirectiveLiteral) {
|
||||||
options.parser === "css" ||
|
options.parser === "css" ||
|
||||||
options.parser === "less" ||
|
options.parser === "less" ||
|
||||||
options.parser === "scss" ||
|
options.parser === "scss" ||
|
||||||
options.parentParser === "html" ||
|
options.embeddedInHtml
|
||||||
options.parentParser === "vue" ||
|
|
||||||
options.parentParser === "angular" ||
|
|
||||||
options.parentParser === "lwc"
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,7 +113,7 @@ function embed(path, print, textToDoc, options) {
|
||||||
|
|
||||||
// lit-html: html`<my-element obj=${obj}></my-element>`
|
// lit-html: html`<my-element obj=${obj}></my-element>`
|
||||||
if (
|
if (
|
||||||
/^PRETTIER_HTML_PLACEHOLDER_\d+_IN_JS$/.test(
|
/^PRETTIER_HTML_PLACEHOLDER_\d+_\d+_IN_JS$/.test(
|
||||||
options.originalText.slice(
|
options.originalText.slice(
|
||||||
node.valueSpan.start.offset,
|
node.valueSpan.start.offset,
|
||||||
node.valueSpan.end.offset
|
node.valueSpan.end.offset
|
||||||
|
|
|
@ -16,7 +16,7 @@ const {
|
||||||
utils: { mapDoc, stripTrailingHardline }
|
utils: { mapDoc, stripTrailingHardline }
|
||||||
} = require("../doc");
|
} = require("../doc");
|
||||||
|
|
||||||
function embed(path, print, textToDoc /*, options */) {
|
function embed(path, print, textToDoc, options) {
|
||||||
const node = path.getValue();
|
const node = path.getValue();
|
||||||
const parent = path.getParentNode();
|
const parent = path.getParentNode();
|
||||||
const parentParent = path.getParentNode(1);
|
const parentParent = path.getParentNode(1);
|
||||||
|
@ -135,12 +135,20 @@ function embed(path, print, textToDoc /*, options */) {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isHtml(path)) {
|
const htmlParser = isHtml(path)
|
||||||
return printHtmlTemplateLiteral(path, print, textToDoc, "html");
|
? "html"
|
||||||
}
|
: isAngularComponentTemplate(path)
|
||||||
|
? "angular"
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (isAngularComponentTemplate(path)) {
|
if (htmlParser) {
|
||||||
return printHtmlTemplateLiteral(path, print, textToDoc, "angular");
|
return printHtmlTemplateLiteral(
|
||||||
|
path,
|
||||||
|
print,
|
||||||
|
textToDoc,
|
||||||
|
htmlParser,
|
||||||
|
options.embeddedInHtml
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
@ -195,6 +203,10 @@ function getIndentation(str) {
|
||||||
return firstMatchedIndent === null ? "" : firstMatchedIndent[1];
|
return firstMatchedIndent === null ? "" : firstMatchedIndent[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function uncook(cookedValue) {
|
||||||
|
return cookedValue.replace(/([\\`]|\$\{)/g, "\\$1");
|
||||||
|
}
|
||||||
|
|
||||||
function escapeTemplateCharacters(doc, raw) {
|
function escapeTemplateCharacters(doc, raw) {
|
||||||
return mapDoc(doc, currentDoc => {
|
return mapDoc(doc, currentDoc => {
|
||||||
if (!currentDoc.parts) {
|
if (!currentDoc.parts) {
|
||||||
|
@ -205,11 +217,7 @@ function escapeTemplateCharacters(doc, raw) {
|
||||||
|
|
||||||
currentDoc.parts.forEach(part => {
|
currentDoc.parts.forEach(part => {
|
||||||
if (typeof part === "string") {
|
if (typeof part === "string") {
|
||||||
parts.push(
|
parts.push(raw ? part.replace(/(\\*)`/g, "$1$1\\`") : uncook(part));
|
||||||
raw
|
|
||||||
? part.replace(/(\\*)`/g, "$1$1\\`")
|
|
||||||
: part.replace(/([\\`]|\$\{)/g, "\\$1")
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
parts.push(part);
|
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 node = path.getValue();
|
||||||
|
|
||||||
const placeholderPattern = "PRETTIER_HTML_PLACEHOLDER_(\\d+)_IN_JS";
|
const counter = htmlTemplateLiteralCounter;
|
||||||
const placeholders = node.expressions.map(
|
htmlTemplateLiteralCounter = (htmlTemplateLiteralCounter + 1) >>> 0;
|
||||||
(_, i) => `PRETTIER_HTML_PLACEHOLDER_${i}_IN_JS`
|
|
||||||
);
|
const composePlaceholder = index =>
|
||||||
|
`PRETTIER_HTML_PLACEHOLDER_${index}_${counter}_IN_JS`;
|
||||||
|
|
||||||
const text = node.quasis
|
const text = node.quasis
|
||||||
.map((quasi, index, quasis) =>
|
.map((quasi, index, quasis) =>
|
||||||
index === quasis.length - 1
|
index === quasis.length - 1
|
||||||
? quasi.value.raw
|
? quasi.value.cooked
|
||||||
: quasi.value.raw + placeholders[index]
|
: quasi.value.cooked + composePlaceholder(index)
|
||||||
)
|
)
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
|
@ -598,14 +616,12 @@ function printHtmlTemplateLiteral(path, print, textToDoc, parser) {
|
||||||
return "``";
|
return "``";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const placeholderRegex = RegExp(composePlaceholder("(\\d+)"), "g");
|
||||||
|
|
||||||
const contentDoc = mapDoc(
|
const contentDoc = mapDoc(
|
||||||
stripTrailingHardline(textToDoc(text, { parser })),
|
stripTrailingHardline(textToDoc(text, { parser })),
|
||||||
doc => {
|
doc => {
|
||||||
const placeholderRegex = new RegExp(placeholderPattern, "g");
|
if (typeof doc !== "string") {
|
||||||
const hasPlaceholder =
|
|
||||||
typeof doc === "string" && placeholderRegex.test(doc);
|
|
||||||
|
|
||||||
if (!hasPlaceholder) {
|
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -613,10 +629,14 @@ function printHtmlTemplateLiteral(path, print, textToDoc, parser) {
|
||||||
|
|
||||||
const components = doc.split(placeholderRegex);
|
const components = doc.split(placeholderRegex);
|
||||||
for (let i = 0; i < components.length; i++) {
|
for (let i = 0; i < components.length; i++) {
|
||||||
const component = components[i];
|
let component = components[i];
|
||||||
|
|
||||||
if (i % 2 === 0) {
|
if (i % 2 === 0) {
|
||||||
if (component) {
|
if (component) {
|
||||||
|
component = uncook(component);
|
||||||
|
if (escapeClosingScriptTag) {
|
||||||
|
component = component.replace(/<\/(script)\b/gi, "<\\/$1");
|
||||||
|
}
|
||||||
parts.push(component);
|
parts.push(component);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -129,6 +129,18 @@ function needsParens(path, options) {
|
||||||
|
|
||||||
// Identifiers never need parentheses.
|
// Identifiers never need parentheses.
|
||||||
if (node.type === "Identifier") {
|
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`<script> f((${expr}) / 2); </script>`;
|
||||||
|
// 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,13 @@ function textToDoc(text, partialNextOptions, parentOptions, printAstToDoc) {
|
||||||
const nextOptions = normalize(
|
const nextOptions = normalize(
|
||||||
Object.assign({}, parentOptions, partialNextOptions, {
|
Object.assign({}, parentOptions, partialNextOptions, {
|
||||||
parentParser: parentOptions.parser,
|
parentParser: parentOptions.parser,
|
||||||
|
embeddedInHtml: !!(
|
||||||
|
parentOptions.embeddedInHtml ||
|
||||||
|
parentOptions.parser === "html" ||
|
||||||
|
parentOptions.parser === "vue" ||
|
||||||
|
parentOptions.parser === "angular" ||
|
||||||
|
parentOptions.parser === "lwc"
|
||||||
|
),
|
||||||
originalText: text
|
originalText: text
|
||||||
}),
|
}),
|
||||||
{ passThrough: true }
|
{ passThrough: true }
|
||||||
|
|
|
@ -31,3 +31,47 @@ printWidth: 80
|
||||||
|
|
||||||
================================================================================
|
================================================================================
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`script-tag-escaping.html 1`] = `
|
||||||
|
====================================options=====================================
|
||||||
|
parsers: ["html"]
|
||||||
|
printWidth: 80
|
||||||
|
| printWidth
|
||||||
|
=====================================input======================================
|
||||||
|
<script>
|
||||||
|
document.write(/* HTML */ \`
|
||||||
|
<script>
|
||||||
|
document.write(/* HTML */ \\\`
|
||||||
|
<!-- foo1 -->
|
||||||
|
<script>
|
||||||
|
document.write(/* HTML */ \\\\\\\`<!-- bar1 --> bar <!-- bar2 -->\\\\\\\`);
|
||||||
|
<\\\\/script>
|
||||||
|
<!-- foo2 -->
|
||||||
|
\\\`);
|
||||||
|
<\\/script>
|
||||||
|
\`);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
=====================================output=====================================
|
||||||
|
<script>
|
||||||
|
document.write(/* HTML */ \`
|
||||||
|
<script>
|
||||||
|
document.write(/* HTML */ \\\`
|
||||||
|
<!-- foo1 -->
|
||||||
|
<script>
|
||||||
|
document.write(
|
||||||
|
/* HTML */ \\\\\\\`
|
||||||
|
<!-- bar1 -->
|
||||||
|
bar
|
||||||
|
<!-- bar2 -->
|
||||||
|
\\\\\\\`
|
||||||
|
);
|
||||||
|
<\\\\/script>
|
||||||
|
<!-- foo2 -->
|
||||||
|
\\\`);
|
||||||
|
<\\/script>
|
||||||
|
\`);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
`;
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
<script>
|
||||||
|
document.write(/* HTML */ `
|
||||||
|
<script>
|
||||||
|
document.write(/* HTML */ \`
|
||||||
|
<!-- foo1 -->
|
||||||
|
<script>
|
||||||
|
document.write(/* HTML */ \\\`<!-- bar1 --> bar <!-- bar2 -->\\\`);
|
||||||
|
<\\/script>
|
||||||
|
<!-- foo2 -->
|
||||||
|
\`);
|
||||||
|
<\/script>
|
||||||
|
`);
|
||||||
|
</script>
|
|
@ -68,6 +68,17 @@ function HelloWorld() {
|
||||||
\`;
|
\`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const trickyParens = html\`<script> f((\${expr}) / 2); </script>\`;
|
||||||
|
const nestedFun = /* HTML */ \`\${outerExpr( 1 )} <script>const tpl = html\\\`<div>\\\${innerExpr( 1 )} \${outerExpr( 2 )}</div>\\\`</script>\`;
|
||||||
|
|
||||||
|
const closingScriptTagShouldBeEscapedProperly = /* HTML */ \`
|
||||||
|
<script>
|
||||||
|
const html = /* HTML */ \\\`<script><\\\\/script>\\\`;
|
||||||
|
</script>
|
||||||
|
\`;
|
||||||
|
|
||||||
|
const closingScriptTag2 = /* HTML */ \`<script>const scriptTag='<\\\\/script>'; <\\/script>\`;
|
||||||
|
|
||||||
=====================================output=====================================
|
=====================================output=====================================
|
||||||
import { LitElement, html } from "@polymer/lit-element";
|
import { LitElement, html } from "@polymer/lit-element";
|
||||||
|
|
||||||
|
@ -130,5 +141,33 @@ function HelloWorld() {
|
||||||
\`;
|
\`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const trickyParens = html\`
|
||||||
|
<script>
|
||||||
|
f((\${expr}) / 2);
|
||||||
|
</script>
|
||||||
|
\`;
|
||||||
|
const nestedFun = /* HTML */ \`
|
||||||
|
\${outerExpr(1)}
|
||||||
|
<script>
|
||||||
|
const tpl = html\\\`
|
||||||
|
<div>\\\${innerExpr(1)} \${outerExpr(2)}</div>
|
||||||
|
\\\`;
|
||||||
|
</script>
|
||||||
|
\`;
|
||||||
|
|
||||||
|
const closingScriptTagShouldBeEscapedProperly = /* HTML */ \`
|
||||||
|
<script>
|
||||||
|
const html = /* HTML */ \\\`
|
||||||
|
<script><\\\\/script>
|
||||||
|
\\\`;
|
||||||
|
</script>
|
||||||
|
\`;
|
||||||
|
|
||||||
|
const closingScriptTag2 = /* HTML */ \`
|
||||||
|
<script>
|
||||||
|
const scriptTag = "<\\\\/script>";
|
||||||
|
</script>
|
||||||
|
\`;
|
||||||
|
|
||||||
================================================================================
|
================================================================================
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -59,3 +59,14 @@ function HelloWorld() {
|
||||||
`)}
|
`)}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const trickyParens = html`<script> f((${expr}) / 2); </script>`;
|
||||||
|
const nestedFun = /* HTML */ `${outerExpr( 1 )} <script>const tpl = html\`<div>\${innerExpr( 1 )} ${outerExpr( 2 )}</div>\`</script>`;
|
||||||
|
|
||||||
|
const closingScriptTagShouldBeEscapedProperly = /* HTML */ `
|
||||||
|
<script>
|
||||||
|
const html = /* HTML */ \`<script><\\/script>\`;
|
||||||
|
</script>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const closingScriptTag2 = /* HTML */ `<script>const scriptTag='<\\/script>'; <\/script>`;
|
||||||
|
|
Loading…
Reference in New Issue