JS: Refactor embed (#9653)

This commit is contained in:
fisker Cheung authored and GitHub committed 2020-11-16 01:15:01 +08:00
1 parent f9f66711b9
commit dc541cc9b9
7 files changed
+432 -385

No files matched your search

+16 -383
View File
@@ -1,31 +1,20 @@
"use strict";
const {
builders: {
indent,
join,
line,
hardline,
softline,
literalline,
concat,
group,
dedentToRoot,
lineSuffixBoundary,
},
utils: { mapDoc, replaceNewlinesWithLiterallines },
} = require("../document");
const { isBlockComment } = require("./utils");
const { hasLeadingComment } = require("./comments");
const formatMarkdown = require("./embed/markdown");
const formatCss = require("./embed/css");
const formatGraphql = require("./embed/graphql");
const formatHtml = require("./embed/html");
function getParser(path) {
function getLanguage(path) {
if (
isStyledJsx(path) ||
isStyledComponents(path) ||
isCssProp(path) ||
isAngularComponentStyles(path)
) {
return "scss";
return "css";
}
if (isGraphQL(path)) {
@@ -57,130 +46,25 @@ function embed(path, print, textToDoc, options) {
return;
}
const parser = getParser(path);
if (!parser) {
const language = getLanguage(path);
if (!language) {
return;
}
if (parser === "markdown") {
let text = node.quasis[0].value.raw.replace(
/((?:\\\\)*)\\`/g,
(_, backslashes) => "\\".repeat(backslashes.length / 2) + "`"
);
const indentation = getIndentation(text);
const hasIndent = indentation !== "";
if (hasIndent) {
text = text.replace(new RegExp(`^${indentation}`, "gm"), "");
}
const doc = printMarkdown(text, textToDoc);
return concat([
"`",
hasIndent
? indent(concat([softline, doc]))
: concat([literalline, dedentToRoot(doc)]),
softline,
"`",
]);
if (language === "markdown") {
return formatMarkdown(path, print, textToDoc);
}
const expressionDocs = path.map(
(path) => printTemplateExpression(path, print),
"expressions"
);
if (parser === "scss") {
// Get full template literal with expressions replaced by placeholders
const rawQuasis = node.quasis.map((q) => q.value.raw);
let placeholderID = 0;
const text = rawQuasis.reduce((prevVal, currVal, idx) => {
return idx === 0
? currVal
: prevVal +
"@prettier-placeholder-" +
placeholderID++ +
"-id" +
currVal;
}, "");
const doc = textToDoc(text, { parser }, { stripTrailingHardline: true });
return transformCssDoc(doc, node, expressionDocs);
if (language === "css") {
return formatCss(path, print, textToDoc);
}
if (parser === "graphql") {
const numQuasis = node.quasis.length;
if (numQuasis === 1 && node.quasis[0].value.raw.trim() === "") {
return "``";
}
const parts = [];
for (let i = 0; i < numQuasis; i++) {
const templateElement = node.quasis[i];
const isFirst = i === 0;
const isLast = i === numQuasis - 1;
const text = templateElement.value.cooked;
const lines = text.split("\n");
const numLines = lines.length;
const expressionDoc = expressionDocs[i];
const startsWithBlankLine =
numLines > 2 && lines[0].trim() === "" && lines[1].trim() === "";
const endsWithBlankLine =
numLines > 2 &&
lines[numLines - 1].trim() === "" &&
lines[numLines - 2].trim() === "";
const commentsAndWhitespaceOnly = lines.every((line) =>
/^\s*(?:#[^\n\r]*)?$/.test(line)
);
// Bail out if an interpolation occurs within a comment.
if (!isLast && /#[^\n\r]*$/.test(lines[numLines - 1])) {
return null;
}
let doc = null;
if (commentsAndWhitespaceOnly) {
doc = printGraphqlComments(lines);
} else {
doc = textToDoc(text, { parser }, { stripTrailingHardline: true });
}
if (doc) {
doc = escapeTemplateCharacters(doc, false);
if (!isFirst && startsWithBlankLine) {
parts.push("");
}
parts.push(doc);
if (!isLast && endsWithBlankLine) {
parts.push("");
}
} else if (!isFirst && !isLast && startsWithBlankLine) {
parts.push("");
}
if (expressionDoc) {
parts.push(expressionDoc);
}
}
return concat([
"`",
indent(concat([hardline, join(hardline, parts)])),
hardline,
"`",
]);
if (language === "graphql") {
return formatGraphql(path, print, textToDoc);
}
if (parser === "html" || parser === "angular") {
return printHtmlTemplateLiteral(
node,
expressionDocs,
textToDoc,
parser,
options
);
if (language === "html" || language === "angular") {
return formatHtml(path, print, textToDoc, options, { parser: language });
}
}
@@ -200,153 +84,6 @@ function isMarkdown(path) {
);
}
function printTemplateExpression(path, print) {
const node = path.getValue();
let printed = print(path);
if (node.comments && node.comments.length) {
printed = group(concat([indent(concat([softline, printed])), softline]));
}
return concat(["${", printed, lineSuffixBoundary, "}"]);
}
function printMarkdown(text, textToDoc) {
const doc = textToDoc(
text,
{ parser: "markdown", __inJsTemplate: true },
{ stripTrailingHardline: true }
);
return escapeTemplateCharacters(doc, true);
}
function getIndentation(str) {
const firstMatchedIndent = str.match(/^([^\S\n]*)\S/m);
return firstMatchedIndent === null ? "" : firstMatchedIndent[1];
}
function uncook(cookedValue) {
return cookedValue.replace(/([\\`]|\${)/g, "\\$1");
}
function escapeTemplateCharacters(doc, raw) {
return mapDoc(doc, (currentDoc) => {
if (!currentDoc.parts) {
return currentDoc;
}
const parts = currentDoc.parts.map((part) => {
if (typeof part === "string") {
return raw ? part.replace(/(\\*)`/g, "$1$1\\`") : uncook(part);
}
return part;
});
return { ...currentDoc, parts };
});
}
function transformCssDoc(quasisDoc, parentNode, expressionDocs) {
const isEmpty =
parentNode.quasis.length === 1 && !parentNode.quasis[0].value.raw.trim();
if (isEmpty) {
return "``";
}
const newDoc = replacePlaceholders(quasisDoc, expressionDocs);
/* istanbul ignore if */
if (!newDoc) {
throw new Error("Couldn't insert all the expressions");
}
return concat(["`", indent(concat([hardline, newDoc])), softline, "`"]);
}
// Search all the placeholders in the quasisDoc tree
// and replace them with the expression docs one by one
// returns a new doc with all the placeholders replaced,
// or null if it couldn't replace any expression
function replacePlaceholders(quasisDoc, expressionDocs) {
if (!expressionDocs || !expressionDocs.length) {
return quasisDoc;
}
let replaceCounter = 0;
const newDoc = mapDoc(quasisDoc, (doc) => {
if (!doc || !doc.parts || !doc.parts.length) {
return doc;
}
let { parts } = doc;
const atIndex = parts.indexOf("@");
const placeholderIndex = atIndex + 1;
if (
atIndex > -1 &&
typeof parts[placeholderIndex] === "string" &&
parts[placeholderIndex].startsWith("prettier-placeholder")
) {
// If placeholder is split, join it
const at = parts[atIndex];
const placeholder = parts[placeholderIndex];
const rest = parts.slice(placeholderIndex + 1);
parts = parts
.slice(0, atIndex)
.concat([at + placeholder])
.concat(rest);
}
const replacedParts = [];
parts.forEach((part) => {
if (typeof part !== "string" || !part.includes("@prettier-placeholder")) {
replacedParts.push(part);
return;
}
// When we have multiple placeholders in one line, like:
// ${Child}${Child2}:not(:first-child)
part.split(/@prettier-placeholder-(\d+)-id/).forEach((component, idx) => {
// The placeholder is always at odd indices
if (idx % 2 === 0) {
replacedParts.push(replaceNewlinesWithLiterallines(component));
return;
}
// The component will always be a number at odd index
replacedParts.push(expressionDocs[component]);
replaceCounter++;
});
});
return { ...doc, parts: replacedParts };
});
return expressionDocs.length === replaceCounter ? newDoc : null;
}
function printGraphqlComments(lines) {
const parts = [];
let seenComment = false;
lines
.map((textLine) => textLine.trim())
.forEach((textLine, i, array) => {
// Lines are either whitespace only, or a comment (with potential whitespace
// around it). Drop whitespace-only lines.
if (textLine === "") {
return;
}
if (array[i - 1] === "" && seenComment) {
// If a non-first comment is preceded by a blank (whitespace only) line,
// add in a blank line.
parts.push(concat([hardline, textLine]));
} else {
parts.push(textLine);
}
seenComment = true;
});
// If `lines` was whitespace only, return `null`.
return parts.length === 0 ? null : join(hardline, parts);
}
/**
* Template literal in these contexts:
* <style jsx>{`div{color:red}`}</style>
@@ -555,110 +292,6 @@ function isHtml(path) {
);
}
// The counter is needed to distinguish nested embeds.
let htmlTemplateLiteralCounter = 0;
function printHtmlTemplateLiteral(
node,
expressionDocs,
textToDoc,
parser,
options
) {
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.cooked
: quasi.value.cooked + composePlaceholder(index)
)
.join("");
if (expressionDocs.length === 0 && text.trim().length === 0) {
return "``";
}
const placeholderRegex = new RegExp(composePlaceholder("(\\d+)"), "g");
let topLevelCount = 0;
const contentDoc = mapDoc(
textToDoc(
text,
{
parser,
__onHtmlRoot(root) {
topLevelCount = root.children.length;
},
},
{ stripTrailingHardline: true }
),
(doc) => {
if (typeof doc !== "string") {
return doc;
}
const parts = [];
const components = doc.split(placeholderRegex);
for (let i = 0; i < components.length; i++) {
let component = components[i];
if (i % 2 === 0) {
if (component) {
component = uncook(component);
if (options.embeddedInHtml) {
component = component.replace(/<\/(script)\b/gi, "<\\/$1");
}
parts.push(component);
}
continue;
}
const placeholderIndex = +component;
parts.push(expressionDocs[placeholderIndex]);
}
return concat(parts);
}
);
const leadingWhitespace = /^\s/.test(text) ? " " : "";
const trailingWhitespace = /\s$/.test(text) ? " " : "";
const linebreak =
options.htmlWhitespaceSensitivity === "ignore"
? hardline
: leadingWhitespace && trailingWhitespace
? line
: null;
if (linebreak) {
return group(
concat([
"`",
indent(concat([linebreak, group(contentDoc)])),
linebreak,
"`",
])
);
}
return group(
concat([
"`",
leadingWhitespace,
topLevelCount > 1 ? indent(group(contentDoc)) : group(contentDoc),
trailingWhitespace,
"`",
])
);
}
function hasInvalidCookedValue({ quasis }) {
return quasis.some(({ value: { cooked } }) => cooked === null);
}
+103
View File
@@ -0,0 +1,103 @@
"use strict";
const {
builders: { indent, hardline, softline, concat },
utils: { mapDoc, replaceNewlinesWithLiterallines },
} = require("../../document");
const { printTemplateExpressions } = require("../print/template-literal");
function format(path, print, textToDoc) {
const node = path.getValue();
// Get full template literal with expressions replaced by placeholders
const rawQuasis = node.quasis.map((q) => q.value.raw);
let placeholderID = 0;
const text = rawQuasis.reduce((prevVal, currVal, idx) => {
return idx === 0
? currVal
: prevVal + "@prettier-placeholder-" + placeholderID++ + "-id" + currVal;
}, "");
const doc = textToDoc(
text,
{ parser: "scss" },
{ stripTrailingHardline: true }
);
const expressionDocs = printTemplateExpressions(path, print);
return transformCssDoc(doc, node, expressionDocs);
}
function transformCssDoc(quasisDoc, parentNode, expressionDocs) {
const isEmpty =
parentNode.quasis.length === 1 && !parentNode.quasis[0].value.raw.trim();
if (isEmpty) {
return "``";
}
const newDoc = replacePlaceholders(quasisDoc, expressionDocs);
/* istanbul ignore if */
if (!newDoc) {
throw new Error("Couldn't insert all the expressions");
}
return concat(["`", indent(concat([hardline, newDoc])), softline, "`"]);
}
// Search all the placeholders in the quasisDoc tree
// and replace them with the expression docs one by one
// returns a new doc with all the placeholders replaced,
// or null if it couldn't replace any expression
function replacePlaceholders(quasisDoc, expressionDocs) {
if (!expressionDocs || !expressionDocs.length) {
return quasisDoc;
}
let replaceCounter = 0;
const newDoc = mapDoc(quasisDoc, (doc) => {
if (!doc || !doc.parts || !doc.parts.length) {
return doc;
}
let { parts } = doc;
const atIndex = parts.indexOf("@");
const placeholderIndex = atIndex + 1;
if (
atIndex > -1 &&
typeof parts[placeholderIndex] === "string" &&
parts[placeholderIndex].startsWith("prettier-placeholder")
) {
// If placeholder is split, join it
const at = parts[atIndex];
const placeholder = parts[placeholderIndex];
const rest = parts.slice(placeholderIndex + 1);
parts = parts
.slice(0, atIndex)
.concat([at + placeholder])
.concat(rest);
}
const replacedParts = [];
parts.forEach((part) => {
if (typeof part !== "string" || !part.includes("@prettier-placeholder")) {
replacedParts.push(part);
return;
}
// When we have multiple placeholders in one line, like:
// ${Child}${Child2}:not(:first-child)
part.split(/@prettier-placeholder-(\d+)-id/).forEach((component, idx) => {
// The placeholder is always at odd indices
if (idx % 2 === 0) {
replacedParts.push(replaceNewlinesWithLiterallines(component));
return;
}
// The component will always be a number at odd index
replacedParts.push(expressionDocs[component]);
replaceCounter++;
});
});
return { ...doc, parts: replacedParts };
});
return expressionDocs.length === replaceCounter ? newDoc : null;
}
module.exports = format;
+114
View File
@@ -0,0 +1,114 @@
"use strict";
const {
builders: { indent, join, hardline, concat },
} = require("../../document");
const {
escapeTemplateCharacters,
printTemplateExpressions,
} = require("../print/template-literal");
function format(path, print, textToDoc) {
const node = path.getValue();
const numQuasis = node.quasis.length;
if (numQuasis === 1 && node.quasis[0].value.raw.trim() === "") {
return "``";
}
const expressionDocs = printTemplateExpressions(path, print);
const parts = [];
for (let i = 0; i < numQuasis; i++) {
const templateElement = node.quasis[i];
const isFirst = i === 0;
const isLast = i === numQuasis - 1;
const text = templateElement.value.cooked;
const lines = text.split("\n");
const numLines = lines.length;
const expressionDoc = expressionDocs[i];
const startsWithBlankLine =
numLines > 2 && lines[0].trim() === "" && lines[1].trim() === "";
const endsWithBlankLine =
numLines > 2 &&
lines[numLines - 1].trim() === "" &&
lines[numLines - 2].trim() === "";
const commentsAndWhitespaceOnly = lines.every((line) =>
/^\s*(?:#[^\n\r]*)?$/.test(line)
);
// Bail out if an interpolation occurs within a comment.
if (!isLast && /#[^\n\r]*$/.test(lines[numLines - 1])) {
return null;
}
let doc = null;
if (commentsAndWhitespaceOnly) {
doc = printGraphqlComments(lines);
} else {
doc = textToDoc(
text,
{ parser: "graphql" },
{ stripTrailingHardline: true }
);
}
if (doc) {
doc = escapeTemplateCharacters(doc, false);
if (!isFirst && startsWithBlankLine) {
parts.push("");
}
parts.push(doc);
if (!isLast && endsWithBlankLine) {
parts.push("");
}
} else if (!isFirst && !isLast && startsWithBlankLine) {
parts.push("");
}
if (expressionDoc) {
parts.push(expressionDoc);
}
}
return concat([
"`",
indent(concat([hardline, join(hardline, parts)])),
hardline,
"`",
]);
}
function printGraphqlComments(lines) {
const parts = [];
let seenComment = false;
lines
.map((textLine) => textLine.trim())
.forEach((textLine, i, array) => {
// Lines are either whitespace only, or a comment (with potential whitespace
// around it). Drop whitespace-only lines.
if (textLine === "") {
return;
}
if (array[i - 1] === "" && seenComment) {
// If a non-first comment is preceded by a blank (whitespace only) line,
// add in a blank line.
parts.push(concat([hardline, textLine]));
} else {
parts.push(textLine);
}
seenComment = true;
});
// If `lines` was whitespace only, return `null`.
return parts.length === 0 ? null : join(hardline, parts);
}
module.exports = format;
+109
View File
@@ -0,0 +1,109 @@
"use strict";
const {
builders: { indent, line, hardline, concat, group },
utils: { mapDoc },
} = require("../../document");
const {
printTemplateExpressions,
uncookTemplateElementValue,
} = require("../print/template-literal");
// The counter is needed to distinguish nested embeds.
let htmlTemplateLiteralCounter = 0;
function format(path, print, textToDoc, options, { parser }) {
const node = path.getValue();
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.cooked
: quasi.value.cooked + composePlaceholder(index)
)
.join("");
const expressionDocs = printTemplateExpressions(path, print);
if (expressionDocs.length === 0 && text.trim().length === 0) {
return "``";
}
const placeholderRegex = new RegExp(composePlaceholder("(\\d+)"), "g");
let topLevelCount = 0;
const doc = textToDoc(
text,
{
parser,
__onHtmlRoot(root) {
topLevelCount = root.children.length;
},
},
{ stripTrailingHardline: true }
);
const contentDoc = mapDoc(doc, (doc) => {
if (typeof doc !== "string") {
return doc;
}
const parts = [];
const components = doc.split(placeholderRegex);
for (let i = 0; i < components.length; i++) {
let component = components[i];
if (i % 2 === 0) {
if (component) {
component = uncookTemplateElementValue(component);
if (options.embeddedInHtml) {
component = component.replace(/<\/(script)\b/gi, "<\\/$1");
}
parts.push(component);
}
continue;
}
const placeholderIndex = +component;
parts.push(expressionDocs[placeholderIndex]);
}
return concat(parts);
});
const leadingWhitespace = /^\s/.test(text) ? " " : "";
const trailingWhitespace = /\s$/.test(text) ? " " : "";
const linebreak =
options.htmlWhitespaceSensitivity === "ignore"
? hardline
: leadingWhitespace && trailingWhitespace
? line
: null;
if (linebreak) {
return group(
concat([
"`",
indent(concat([linebreak, group(contentDoc)])),
linebreak,
"`",
])
);
}
return group(
concat([
"`",
leadingWhitespace,
topLevelCount > 1 ? indent(group(contentDoc)) : group(contentDoc),
trailingWhitespace,
"`",
])
);
}
module.exports = format;
+42
View File
@@ -0,0 +1,42 @@
"use strict";
const {
builders: { indent, softline, literalline, concat, dedentToRoot },
} = require("../../document");
const { escapeTemplateCharacters } = require("../print/template-literal");
function format(path, print, textToDoc) {
const node = path.getValue();
let text = node.quasis[0].value.raw.replace(
/((?:\\\\)*)\\`/g,
(_, backslashes) => "\\".repeat(backslashes.length / 2) + "`"
);
const indentation = getIndentation(text);
const hasIndent = indentation !== "";
if (hasIndent) {
text = text.replace(new RegExp(`^${indentation}`, "gm"), "");
}
const doc = escapeTemplateCharacters(
textToDoc(
text,
{ parser: "markdown", __inJsTemplate: true },
{ stripTrailingHardline: true }
),
true
);
return concat([
"`",
hasIndent
? indent(concat([softline, doc]))
: concat([literalline, dedentToRoot(doc)]),
softline,
"`",
]);
}
function getIndentation(str) {
const firstMatchedIndent = str.match(/^([^\S\n]*)\S/m);
return firstMatchedIndent === null ? "" : firstMatchedIndent[1];
}
module.exports = format;
+47 -1
View File
@@ -14,6 +14,7 @@ const {
addAlignmentToDoc,
},
printer: { printDocToString },
utils: { mapDoc },
} = require("../../document");
const {
isBinaryish,
@@ -190,4 +191,49 @@ function printJestEachTemplateLiteral(path, options, print) {
}
}
module.exports = printTemplateLiteral;
function printTemplateExpression(path, print) {
const node = path.getValue();
let printed = print(path);
if (node.comments && node.comments.length) {
printed = group(concat([indent(concat([softline, printed])), softline]));
}
return concat(["${", printed, lineSuffixBoundary, "}"]);
}
function printTemplateExpressions(path, print) {
return path.map(
(path) => printTemplateExpression(path, print),
"expressions"
);
}
function escapeTemplateCharacters(doc, raw) {
return mapDoc(doc, (currentDoc) => {
if (!currentDoc.parts) {
return currentDoc;
}
const parts = currentDoc.parts.map((part) => {
if (typeof part === "string") {
return raw
? part.replace(/(\\*)`/g, "$1$1\\`")
: uncookTemplateElementValue(part);
}
return part;
});
return { ...currentDoc, parts };
});
}
function uncookTemplateElementValue(cookedValue) {
return cookedValue.replace(/([\\`]|\${)/g, "\\$1");
}
module.exports = {
printTemplateLiteral,
printTemplateExpressions,
escapeTemplateCharacters,
uncookTemplateElementValue,
};
+1 -1
View File
@@ -123,7 +123,7 @@ const {
printFunctionParameters,
shouldHugFunctionParameters,
} = require("./print/function-parameters");
const printTemplateLiteral = require("./print/template-literal");
const { printTemplateLiteral } = require("./print/template-literal");
const needsQuoteProps = new WeakMap();