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();
|
||||
```
|
||||
|
||||
### 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])
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -113,7 +113,7 @@ function embed(path, print, textToDoc, options) {
|
|||
|
||||
// lit-html: html`<my-element obj=${obj}></my-element>`
|
||||
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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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`<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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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=====================================
|
||||
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