fix: nested embeds (JS in HTML in JS) (#6038)

master
Georgii Dolzhykov 2019-05-27 20:42:13 +03:00 committed by Lucas Duailibe
parent f8875c1caa
commit e4f0df5bed
10 changed files with 193 additions and 29 deletions

View File

@ -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

View File

@ -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
)
);
}

View File

@ -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

View File

@ -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;

View File

@ -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;
}

View File

@ -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 }

View File

@ -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>
================================================================================
`;

View File

@ -0,0 +1,13 @@
<script>
document.write(/* HTML */ `
<script>
document.write(/* HTML */ \`
<!-- foo1 -->
<script>
document.write(/* HTML */ \\\`<!-- bar1 --> bar <!-- bar2 -->\\\`);
<\\/script>
<!-- foo2 -->
\`);
<\/script>
`);
</script>

View File

@ -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>
\`;
================================================================================
`;

View File

@ -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>`;