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(); 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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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