Support async parser for embedded languages (#13211)

Co-authored-by: fisker Cheung <lionkay@gmail.com>
This commit is contained in:
Georgii Dolzhykovandfisker Cheung authored and GitHub committed 2022-08-07 00:45:06 +08:00
1 parent 31cdcd0487
commit 4d5b645e0e
35 files changed
+588 -413

No files matched your search

+2 -1
View File
@@ -1,7 +1,8 @@
#### [BREAKING] Change public apis to asynchronous (#12574, #12788, #12790 by @fisker)
#### [BREAKING] Change public APIs to asynchronous (#12574, #12788, #12790 by @fisker)
- `prettier.format()` returns `Promise<string>`
- `prettier.formatWithCursor()` returns `Promise<{formatted: string, cursorOffset: number}>`
- `prettier.formatAST()` returns `Promise<string>`
- `prettier.check()` returns `Promise<boolean>`
- `prettier.getSupportInfo()` returns `Promise`
- `prettier.resolveConfig.sync` is removed
+4 -4
View File
@@ -1,7 +1,7 @@
#### [HIGHLIGHT] Support plugin with async parse function (#12748 by @fisker)
#### [HIGHLIGHT] Support plugins with async parsers (#12748 by @fisker, #13211 by @thorn0 and @fisker)
[`parse` function](https://prettier.io/docs/en/plugins.html#parsers) in plugin can return a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) now.
[`parse` function](https://prettier.io/docs/en/plugins.html#parsers) in a plugin can return a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) now.
<!-- TODO: Discuss this before v3 release -->
In order to support async parsers for embedded languages, we had to introduce a breaking change to the plugin API. Namely, the `embed` method of a printer has now to match a completely new signature, incompatible with previous versions. If you're a plugin author and your plugins don't define `embed`, you have nothing to worry about, otherwise see the [docs](/docs/plugins.html#optional-embed) for details.
Note: Since [`embed` functions in plugin](https://prettier.io/docs/en/plugins.html#optional-embed) are still sync, plugins with async parser can't be used to format embed code.
Also, the `preprocess` method of a printer can return a promise now.
+50 -24
View File
@@ -183,11 +183,14 @@ import { doc } from "prettier";
const { join, line, ifBreak, group } = doc.builders;
```
The printing process works as follows:
The printing process consists of the following steps:
1. `preprocess(ast: AST, options: object): AST`, if available, is called. It is passed the AST from the _parser_. The AST returned by `preprocess` will be used by Prettier. If `preprocess` is not defined, the AST returned from the _parser_ will be used.
2. Comments are attached to the AST (see _Handling comments in a printer_ for details).
3. A Doc is recursively constructed from the AST. i) `embed(path: AstPath, print, textToDoc, options: object): Doc | null` is called on each AST node. If `embed` returns a Doc, that Doc is used. ii) If `embed` is undefined or returns a falsy value, `print(path: AstPath, options: object, print): Doc` is called on each AST node.
1. **AST preprocessing** (optional). See [`preprocess`](#optional-preprocess).
2. **Comment attachment** (optional). See [Handling comments in a printer](#handling-comments-in-a-printer).
3. **Processing embedded languages** (optional). The [`embed`](#optional-embed) method, if defined, is called for each node, depth-first. While, for performance reasons, the recursion itself is synchronous, `embed` may return asynchronous functions that can call other parsers and printers to compose docs for embedded syntaxes like CSS-in-JS. These returned functions are queued up and sequentially executed before the next step.
4. **Recursive printing**. A doc is recursively constructed from the AST. Starting from the root node:
- If, from the step 3, there is an embedded language doc associated with the current node, this doc is used.
- Otherwise, the `print(path, options, print): Doc` method is called. It composes a doc for the current node, often by printing child nodes using the `print` callback.
#### `print`
@@ -212,9 +215,9 @@ The `print` function is passed the following parameters:
Here’s a simplified example to give an idea of what a typical implementation of `print` looks like:
```js
const {
builders: { group, indent, join, line, softline },
} = require("prettier").doc;
import { doc } from "prettier";
const { group, indent, join, line, softline } = doc.builders;
function print(path, options, print) {
const node = path.getValue();
@@ -248,42 +251,65 @@ Check out [prettier-python's printer](https://github.com/prettier/prettier-pytho
#### (optional) `embed`
The `embed` function is called when the plugin needs to print one language inside another. Examples of this are printing CSS-in-JS or fenced code blocks in Markdown. Its signature is:
A printer can have the `embed` method to print one language inside another. Examples of this are printing CSS-in-JS or fenced code blocks in Markdown. The signature is:
```ts
function embed(
// Path to the current AST node
path: AstPath,
// Print a node with the current printer
print: (selector?: string | number | Array<string | number> | AstPath) => Doc,
// Parse and print some text using a different parser.
// You should set `options.parser` to specify which parser to use.
textToDoc: (text: string, options: object) => Doc,
// Current options
options: object
): Doc | null;
options: Options
):
| ((
// Parses and prints the passed text using a different parser.
// You should set `options.parser` to specify which parser to use.
textToDoc: (text: string, options: Options) => Promise<Doc>,
// Prints the current node or its descendant node with the current printer
print: (
selector?: string | number | Array<string | number> | AstPath
) => Doc,
// The following two arguments are passed for convenience.
// They're the same `path` and `options` that are passed to `embed`.
path: AstPath,
options: Options
) => Promise<Doc | undefined> | Doc | undefined)
| Doc
| undefined;
```
The `embed` function acts like the `print` function, except that it is passed an additional `textToDoc` function, which can be used to render a doc using a different plugin. The `embed` function returns a Doc or a falsy value. If a falsy value is returned, the `print` function is called with the current `path`. If a Doc is returned, that Doc is used in printing and the `print` function is not called.
The `embed` method is similar to the `print` method in that it maps AST nodes to docs, but unlike `print`, it has power to do async work by returning an async function. That function's first parameter, the `textToDoc` async function, can be used to render a doc using a different plugin.
For example, a plugin that had nodes with embedded JavaScript might have the following `embed` function:
If a function returned from `embed` returns a doc or a promise that resolves to a doc, that doc will be used in printing, and the `print` method won’t be called for this node. It's also possible and, in rare situations, might be convenient to return a doc synchronously directly from `embed`, however `textToDoc` and the `print` callback aren’t available at that case. Return a function to get them.
If `embed` returns `undefined`, or if a function it returned returns `undefined` or a promise that resolves to `undefined`, the node will be printed normally with the `print` method. Same will happen if a returned function throws an error or returns a promise that rejects (e.g., if a parsing error has happened). Set the `PRETTIER_DEBUG` environment variable to a non-empty value if you want Prettier to rethrow these errors.
For example, a plugin that has nodes with embedded JavaScript might have the following `embed` method:
```js
function embed(path, print, textToDoc, options) {
function embed(path, options) {
const node = path.getValue();
if (node.type === "javascript") {
return textToDoc(node.javaScriptText, { ...options, parser: "babel" });
return async (textToDoc) => {
return [
"<script>",
hardline,
await textToDoc(node.javaScriptCode, { parser: "babel" }),
hardline,
"</script>",
];
};
}
return false;
}
```
If the [`--embedded-language-formatting`](options.md#embedded-language-formatting) option is set to `off`, the embedding step is entirely skipped, `embed` isn’t called, and all nodes are printed with the `print` method.
#### (optional) `preprocess`
The preprocess function can process the AST from parser before passing into `print` function.
The `preprocess` method can process the AST from the parser before passing it into the `print` method.
```ts
function preprocess(ast: AST, options: object): AST;
function preprocess(ast: AST, options: Options): AST | Promise<AST>;
```
#### (optional) `insertPragma`
@@ -455,9 +481,9 @@ function isPreviousLineEmpty<N>(text: string, node: N, locStart: (node: N) => nu
Since plugins can be resolved using relative paths, when working on one you can do:
```js
const prettier = require("prettier");
import * as prettier from "prettier";
const code = "(add 1 2)";
prettier.format(code, {
await prettier.format(code, {
parser: "lisp",
plugins: ["."],
});
+5 -3
View File
@@ -1,12 +1,14 @@
import { hardline } from "../document/builders.js";
import printFrontMatter from "../utils/front-matter/print.js";
function embed(path, print, textToDoc /*, options */) {
function embed(path) {
const node = path.getValue();
if (node.type === "front-matter") {
const doc = printFrontMatter(node, textToDoc);
return doc ? [doc, hardline] : "";
return async (textToDoc) => {
const doc = await printFrontMatter(node, textToDoc);
return doc ? [doc, hardline] : undefined;
};
}
}
+119 -109
View File
@@ -35,7 +35,7 @@ import {
} from "./utils/index.js";
import getNodeContent from "./get-node-content.js";
function printEmbeddedAttributeValue(node, htmlTextToDoc, options) {
async function printEmbeddedAttributeValue(node, htmlTextToDoc, options) {
const isKeyMatched = (patterns) =>
new RegExp(patterns.join("|")).test(node.fullName);
const getValue = () => unescapeQuoteEntities(node.value);
@@ -95,7 +95,7 @@ function printEmbeddedAttributeValue(node, htmlTextToDoc, options) {
const value = getValue();
if (!value.includes("{{")) {
return printExpand(
attributeTextToDoc(value, {
await attributeTextToDoc(value, {
parser: "css",
__isHTMLStyleAttribute: true,
})
@@ -136,22 +136,18 @@ function printEmbeddedAttributeValue(node, htmlTextToDoc, options) {
: options.__should_parse_vue_template_with_ts
? "__vue_ts_event_binding"
: "__vue_event_binding";
return printMaybeHug(
attributeTextToDoc(value, {
parser,
})
);
return printMaybeHug(await attributeTextToDoc(value, { parser }));
}
if (isKeyMatched(vueExpressionBindingPatterns)) {
return printMaybeHug(
attributeTextToDoc(getValue(), { parser: "__vue_expression" })
await attributeTextToDoc(getValue(), { parser: "__vue_expression" })
);
}
if (isKeyMatched(jsExpressionBindingPatterns)) {
return printMaybeHug(
attributeTextToDoc(getValue(), { parser: "__js_expression" })
await attributeTextToDoc(getValue(), { parser: "__js_expression" })
);
}
}
@@ -189,11 +185,15 @@ function printEmbeddedAttributeValue(node, htmlTextToDoc, options) {
const ngI18nPatterns = ["^i18n(-.+)?$"];
if (isKeyMatched(ngStatementBindingPatterns)) {
return printMaybeHug(ngTextToDoc(getValue(), { parser: "__ng_action" }));
return printMaybeHug(
await ngTextToDoc(getValue(), { parser: "__ng_action" })
);
}
if (isKeyMatched(ngExpressionBindingPatterns)) {
return printMaybeHug(ngTextToDoc(getValue(), { parser: "__ng_binding" }));
return printMaybeHug(
await ngTextToDoc(getValue(), { parser: "__ng_binding" })
);
}
if (isKeyMatched(ngI18nPatterns)) {
@@ -206,7 +206,7 @@ function printEmbeddedAttributeValue(node, htmlTextToDoc, options) {
if (isKeyMatched(ngDirectiveBindingPatterns)) {
return printMaybeHug(
ngTextToDoc(getValue(), { parser: "__ng_directive" })
await ngTextToDoc(getValue(), { parser: "__ng_directive" })
);
}
@@ -224,7 +224,7 @@ function printEmbeddedAttributeValue(node, htmlTextToDoc, options) {
"{{",
indent([
line,
ngTextToDoc(part, {
await ngTextToDoc(part, {
parser: "__ng_interpolation",
__isInHtmlInterpolation: true, // to avoid unexpected `}}`
}),
@@ -245,7 +245,7 @@ function printEmbeddedAttributeValue(node, htmlTextToDoc, options) {
return null;
}
function embed(path, print, textToDoc, options) {
function embed(path, options) {
const node = path.getValue();
switch (node.type) {
@@ -261,27 +261,29 @@ function embed(path, print, textToDoc, options) {
return;
}
const content = getNodeContent(node, options);
let isEmpty = /^\s*$/.test(content);
let doc = "";
if (!isEmpty) {
doc = textToDoc(
htmlTrimPreserveIndentation(content),
{ parser, __embeddedInHtml: true },
{ stripTrailingHardline: true }
);
isEmpty = doc === "";
}
return async (textToDoc, print) => {
const content = getNodeContent(node, options);
let isEmpty = /^\s*$/.test(content);
let doc = "";
if (!isEmpty) {
doc = await textToDoc(
htmlTrimPreserveIndentation(content),
{ parser, __embeddedInHtml: true },
{ stripTrailingHardline: true }
);
isEmpty = doc === "";
}
return [
printOpeningTagPrefix(node, options),
group(printOpeningTag(path, options, print)),
isEmpty ? "" : hardline,
doc,
isEmpty ? "" : hardline,
printClosingTag(node, options),
printClosingTagSuffix(node, options),
];
return [
printOpeningTagPrefix(node, options),
group(printOpeningTag(path, options, print)),
isEmpty ? "" : hardline,
doc,
isEmpty ? "" : hardline,
printClosingTag(node, options),
printClosingTagSuffix(node, options),
];
};
}
break;
}
@@ -289,60 +291,67 @@ function embed(path, print, textToDoc, options) {
if (isScriptLikeTag(node.parent)) {
const parser = inferScriptParser(node.parent, options);
if (parser) {
const value =
parser === "markdown"
? dedentString(node.value.replace(/^[^\S\n]*\n/, ""))
: node.value;
const textToDocOptions = { parser, __embeddedInHtml: true };
if (options.parser === "html" && parser === "babel") {
let sourceType = "script";
const { attrMap } = node.parent;
if (
attrMap &&
(attrMap.type === "module" ||
(attrMap.type === "text/babel" &&
attrMap["data-type"] === "module"))
) {
sourceType = "module";
return async (textToDoc) => {
const value =
parser === "markdown"
? dedentString(node.value.replace(/^[^\S\n]*\n/, ""))
: node.value;
const textToDocOptions = { parser, __embeddedInHtml: true };
if (options.parser === "html" && parser === "babel") {
let sourceType = "script";
const { attrMap } = node.parent;
if (
attrMap &&
(attrMap.type === "module" ||
(attrMap.type === "text/babel" &&
attrMap["data-type"] === "module"))
) {
sourceType = "module";
}
textToDocOptions.__babelSourceType = sourceType;
}
textToDocOptions.__babelSourceType = sourceType;
}
return [
breakParent,
printOpeningTagPrefix(node, options),
textToDoc(value, textToDocOptions, {
stripTrailingHardline: true,
}),
printClosingTagSuffix(node, options),
];
return [
breakParent,
printOpeningTagPrefix(node, options),
await textToDoc(value, textToDocOptions, {
stripTrailingHardline: true,
}),
printClosingTagSuffix(node, options),
];
};
}
} else if (node.parent.type === "interpolation") {
const textToDocOptions = {
__isInHtmlInterpolation: true, // to avoid unexpected `}}`
__embeddedInHtml: true,
return async (textToDoc) => {
const textToDocOptions = {
__isInHtmlInterpolation: true, // to avoid unexpected `}}`
__embeddedInHtml: true,
};
if (options.parser === "angular") {
textToDocOptions.parser = "__ng_interpolation";
textToDocOptions.trailingComma = "none";
} else if (options.parser === "vue") {
textToDocOptions.parser =
options.__should_parse_vue_template_with_ts
? "__vue_ts_expression"
: "__vue_expression";
} else {
textToDocOptions.parser = "__js_expression";
}
return [
indent([
line,
await textToDoc(node.value, textToDocOptions, {
stripTrailingHardline: true,
}),
]),
node.parent.next &&
needsToBorrowPrevClosingTagEndMarker(node.parent.next)
? " "
: line,
];
};
if (options.parser === "angular") {
textToDocOptions.parser = "__ng_interpolation";
textToDocOptions.trailingComma = "none";
} else if (options.parser === "vue") {
textToDocOptions.parser = options.__should_parse_vue_template_with_ts
? "__vue_ts_expression"
: "__vue_expression";
} else {
textToDocOptions.parser = "__js_expression";
}
return [
indent([
line,
textToDoc(node.value, textToDocOptions, {
stripTrailingHardline: true,
}),
]),
node.parent.next &&
needsToBorrowPrevClosingTagEndMarker(node.parent.next)
? " "
: line,
];
}
break;
}
@@ -378,33 +387,34 @@ function embed(path, print, textToDoc, options) {
}
}
const embeddedAttributeValueDoc = printEmbeddedAttributeValue(
node,
(code, opts) =>
// strictly prefer single quote to avoid unnecessary html entity escape
textToDoc(
code,
{ __isInHtmlAttribute: true, __embeddedInHtml: true, ...opts },
{ stripTrailingHardline: true }
),
options
);
if (embeddedAttributeValueDoc) {
return [
node.rawName,
'="',
group(
mapDoc(embeddedAttributeValueDoc, (doc) =>
typeof doc === "string" ? doc.replace(/"/g, "&quot;") : doc
)
),
'"',
];
}
break;
return async (textToDoc) => {
const embeddedAttributeValueDoc = await printEmbeddedAttributeValue(
node,
(code, opts) =>
// strictly prefer single quote to avoid unnecessary html entity escape
textToDoc(
code,
{ __isInHtmlAttribute: true, __embeddedInHtml: true, ...opts },
{ stripTrailingHardline: true }
),
options
);
if (embeddedAttributeValueDoc) {
return [
node.rawName,
'="',
group(
mapDoc(embeddedAttributeValueDoc, (doc) =>
typeof doc === "string" ? doc.replace(/"/g, "&quot;") : doc
)
),
'"',
];
}
};
}
case "front-matter":
return printFrontMatter(node, textToDoc);
return (textToDoc) => printFrontMatter(node, textToDoc);
}
}
+5 -9
View File
@@ -6,11 +6,11 @@ import { group } from "../document/builders.js";
* v-for="(..., ...) in ..."
* v-for="(..., ...) of ..."
*/
function printVueFor(value, textToDoc) {
async function printVueFor(value, attributeTextToDoc) {
const { left, operator, right } = parseVueFor(value);
return [
group(
textToDoc(`function _(${left}) {}`, {
await attributeTextToDoc(`function _(${left}) {}`, {
parser: "babel",
__isVueForBindingLeft: true,
})
@@ -18,11 +18,7 @@ function printVueFor(value, textToDoc) {
" ",
operator,
" ",
textToDoc(
right,
{ parser: "__js_expression" },
{ stripTrailingHardline: true }
),
await attributeTextToDoc(right, { parser: "__js_expression" }),
];
}
@@ -72,8 +68,8 @@ function parseVueFor(value) {
};
}
function printVueBindings(value, textToDoc) {
return textToDoc(`function _(${value}) {}`, {
function printVueBindings(value, attributeTextToDoc) {
return attributeTextToDoc(`function _(${value}) {}`, {
parser: "babel",
__isVueBindings: true,
});
+17 -40
View File
@@ -6,36 +6,9 @@ import {
import formatMarkdown from "./embed/markdown.js";
import formatCss from "./embed/css.js";
import formatGraphql from "./embed/graphql.js";
import formatHtml from "./embed/html.js";
import { formatHtml, formatAngular } from "./embed/html.js";
function getLanguage(path) {
if (
isStyledJsx(path) ||
isStyledComponents(path) ||
isCssProp(path) ||
isAngularComponentStyles(path)
) {
return "css";
}
if (isGraphQL(path)) {
return "graphql";
}
if (isHtml(path)) {
return "html";
}
if (isAngularComponentTemplate(path)) {
return "angular";
}
if (isMarkdown(path)) {
return "markdown";
}
}
function embed(path, print, textToDoc, options) {
function embed(path) {
const node = path.getValue();
if (
@@ -47,25 +20,29 @@ function embed(path, print, textToDoc, options) {
return;
}
const language = getLanguage(path);
if (!language) {
return;
if (
isStyledJsx(path) ||
isStyledComponents(path) ||
isCssProp(path) ||
isAngularComponentStyles(path)
) {
return formatCss;
}
if (language === "markdown") {
return formatMarkdown(path, print, textToDoc);
if (isGraphQL(path)) {
return formatGraphql;
}
if (language === "css") {
return formatCss(path, print, textToDoc);
if (isHtml(path)) {
return formatHtml;
}
if (language === "graphql") {
return formatGraphql(path, print, textToDoc);
if (isAngularComponentTemplate(path)) {
return formatAngular;
}
if (language === "html" || language === "angular") {
return formatHtml(path, print, textToDoc, options, { parser: language });
if (isMarkdown(path)) {
return formatMarkdown;
}
}
+2 -2
View File
@@ -3,7 +3,7 @@ import { indent, hardline, softline } from "../../document/builders.js";
import { mapDoc, replaceEndOfLine, cleanDoc } from "../../document/utils.js";
import { printTemplateExpressions } from "../print/template-literal.js";
function format(path, print, textToDoc) {
async function format(textToDoc, print, path /*, options*/) {
const node = path.getValue();
// Get full template literal with expressions replaced by placeholders
@@ -20,7 +20,7 @@ function format(path, print, textToDoc) {
currVal,
""
);
const doc = textToDoc(
const doc = await textToDoc(
text,
{ parser: "scss" },
{ stripTrailingHardline: true }
+2 -2
View File
@@ -4,7 +4,7 @@ import {
printTemplateExpressions,
} from "../print/template-literal.js";
function format(path, print, textToDoc) {
async function format(textToDoc, print, path /*, options*/) {
const node = path.getValue();
const numQuasis = node.quasis.length;
@@ -46,7 +46,7 @@ function format(path, print, textToDoc) {
if (commentsAndWhitespaceOnly) {
doc = printGraphqlComments(lines);
} else {
doc = textToDoc(
doc = await textToDoc(
text,
{ parser: "graphql" },
{ stripTrailingHardline: true }
+6 -3
View File
@@ -7,7 +7,7 @@ import {
// The counter is needed to distinguish nested embeds.
let htmlTemplateLiteralCounter = 0;
function format(path, print, textToDoc, options, { parser }) {
async function format(textToDoc, print, path, options, parser) {
const node = path.getValue();
const counter = htmlTemplateLiteralCounter;
htmlTemplateLiteralCounter = (htmlTemplateLiteralCounter + 1) >>> 0;
@@ -30,7 +30,7 @@ function format(path, print, textToDoc, options, { parser }) {
const placeholderRegex = new RegExp(composePlaceholder("(\\d+)"), "g");
let topLevelCount = 0;
const doc = textToDoc(
const doc = await textToDoc(
text,
{
parser,
@@ -93,4 +93,7 @@ function format(path, print, textToDoc, options, { parser }) {
]);
}
export default format;
export const formatHtml = (textToDoc, print, path, options) =>
format(textToDoc, print, path, options, "html");
export const formatAngular = (textToDoc, print, path, options) =>
format(textToDoc, print, path, options, "angular");
+2 -2
View File
@@ -6,7 +6,7 @@ import {
} from "../../document/builders.js";
import { escapeTemplateCharacters } from "../print/template-literal.js";
function format(path, print, textToDoc) {
async function format(textToDoc, print, path /*, options*/) {
const node = path.getValue();
let text = node.quasis[0].value.raw.replace(
/((?:\\\\)*)\\`/g,
@@ -18,7 +18,7 @@ function format(path, print, textToDoc) {
text = text.replace(new RegExp(`^${indentation}`, "gm"), "");
}
const doc = escapeTemplateCharacters(
textToDoc(
await textToDoc(
text,
{ parser: "markdown", __inJsTemplate: true },
{ stripTrailingHardline: true }
+1
View File
@@ -866,6 +866,7 @@ const printer = {
remaining: handleComments.handleRemainingComment,
},
getCommentChildNodes: handleComments.getCommentChildNodes,
isNode: (node) => typeof node.type === "string",
};
export default printer;
+39 -34
View File
@@ -7,45 +7,49 @@ import { replaceEndOfLine } from "../document/utils.js";
import printFrontMatter from "../utils/front-matter/print.js";
import { getFencedCodeBlockValue } from "./utils.js";
function embed(path, print, textToDoc, options) {
function embed(path, options) {
const node = path.getValue();
if (node.type === "code" && node.lang !== null) {
const parser = inferParserByLanguage(node.lang, options);
if (parser) {
const styleUnit = options.__inJsTemplate ? "~" : "`";
const style = styleUnit.repeat(
Math.max(3, getMaxContinuousCount(node.value, styleUnit) + 1)
);
const newOptions = { parser };
if (node.lang === "tsx") {
newOptions.filepath = "dummy.tsx";
}
const doc = textToDoc(
getFencedCodeBlockValue(node, options.originalText),
newOptions,
{ stripTrailingHardline: true }
);
return markAsRoot([
style,
node.lang,
node.meta ? " " + node.meta : "",
hardline,
replaceEndOfLine(doc),
hardline,
style,
]);
return async (textToDoc) => {
const styleUnit = options.__inJsTemplate ? "~" : "`";
const style = styleUnit.repeat(
Math.max(3, getMaxContinuousCount(node.value, styleUnit) + 1)
);
const newOptions = { parser };
if (node.lang === "tsx") {
newOptions.filepath = "dummy.tsx";
}
const doc = await textToDoc(
getFencedCodeBlockValue(node, options.originalText),
newOptions,
{ stripTrailingHardline: true }
);
return markAsRoot([
style,
node.lang,
node.meta ? " " + node.meta : "",
hardline,
replaceEndOfLine(doc),
hardline,
style,
]);
};
}
}
switch (node.type) {
case "front-matter":
return printFrontMatter(node, textToDoc);
return (textToDoc) => printFrontMatter(node, textToDoc);
// MDX
case "importExport":
return [
textToDoc(
return async (textToDoc) => [
await textToDoc(
node.value,
{ parser: "babel" },
{ stripTrailingHardline: true }
@@ -53,14 +57,15 @@ function embed(path, print, textToDoc, options) {
hardline,
];
case "jsx":
return textToDoc(
`<$>${node.value}</$>`,
{
parser: "__js_expression",
rootMarker: "mdx",
},
{ stripTrailingHardline: true }
);
return (textToDoc) =>
textToDoc(
`<$>${node.value}</$>`,
{
parser: "__js_expression",
rootMarker: "mdx",
},
{ stripTrailingHardline: true }
);
}
return null;
+1 -1
View File
@@ -40,7 +40,7 @@ function createParse({ isMDX }) {
.use(isMDX ? htmlToJsx : identity)
.use(wikiLink)
.use(looseItems);
return processor.runSync(processor.parse(text));
return processor.run(processor.parse(text));
};
}
@@ -916,6 +916,7 @@ const printer = {
massageAstNode: clean,
hasPrettierIgnore,
insertPragma,
isNode: (obj) => Boolean(obj.type),
};
export default printer;
+3 -2
View File
@@ -1,4 +1,4 @@
function embed(path, print, textToDoc, options) {
function embed(path, options) {
const node = path.getValue();
// Try to format `.prettierrc`, `.stylelintrc`, and `.lintstagedrc` as `json` first
@@ -7,7 +7,8 @@ function embed(path, print, textToDoc, options) {
options.filepath &&
/(?:[/\\]|^)\.(?:prettier|stylelint|lintstaged)rc$/.test(options.filepath)
) {
return textToDoc(options.originalText, { ...options, parser: "json" });
return (textToDoc) =>
textToDoc(options.originalText, { ...options, parser: "json" });
}
}
+20 -22
View File
@@ -2,7 +2,7 @@ import AstPath from "../common/ast-path.js";
import { hardline, addAlignmentToDoc } from "../document/builders.js";
import { propagateBreaks } from "../document/utils.js";
import { printComments } from "./comments.js";
import { printSubtree } from "./multiparser.js";
import { printEmbeddedLanguages } from "./multiparser.js";
/**
* Takes an abstract syntax tree (AST) and recursively converts it to a
@@ -25,17 +25,28 @@ import { printSubtree } from "./multiparser.js";
* state of the recursion. It is called "path", because it represents
* the path to the current node through the Abstract Syntax Tree.
*/
function printAstToDoc(ast, options, alignmentSize = 0) {
async function printAstToDoc(ast, options, alignmentSize = 0) {
const { printer } = options;
if (printer.preprocess) {
ast = printer.preprocess(ast, options);
ast = await printer.preprocess(ast, options);
}
const cache = new Map();
const path = new AstPath(ast);
let doc = mainPrint();
const embeds = new Map();
await printEmbeddedLanguages(path, mainPrint, options, printAstToDoc, embeds);
// Only the root call of the print method is awaited.
// This is done to make things simpler for plugins that don't use recursive printing.
let doc = await callPluginPrintFunction(
path,
options,
mainPrint,
undefined,
embeds
);
if (alignmentSize > 0) {
// Add a hardline to make the indents take effect
@@ -69,7 +80,7 @@ function printAstToDoc(ast, options, alignmentSize = 0) {
return cache.get(value);
}
const doc = callPluginPrintFunction(path, options, mainPrint, args);
const doc = callPluginPrintFunction(path, options, mainPrint, args, embeds);
if (shouldCache) {
cache.set(value, doc);
@@ -101,7 +112,7 @@ function printPrettierIgnoredNode(node, options) {
return { doc: originalText.slice(start, end), printedComments };
}
function callPluginPrintFunction(path, options, printPath, args) {
function callPluginPrintFunction(path, options, printPath, args, embeds) {
const node = path.getValue();
const { printer } = options;
@@ -111,23 +122,10 @@ function callPluginPrintFunction(path, options, printPath, args) {
// Escape hatch
if (printer.hasPrettierIgnore && printer.hasPrettierIgnore(path)) {
({ doc, printedComments } = printPrettierIgnoredNode(node, options));
} else if (embeds.has(node)) {
doc = embeds.get(node);
} else {
if (node) {
try {
// Potentially switch to a different parser
doc = printSubtree(path, printPath, options, printAstToDoc);
} catch (error) {
/* istanbul ignore if */
if (process.env.PRETTIER_DEBUG) {
throw error;
}
// Continue with current parser
}
}
if (!doc) {
doc = printer.print(path, options, printPath, args);
}
doc = printer.print(path, options, printPath, args);
}
// We let JSXElement print its comments itself because it adds () around
+3 -3
View File
@@ -47,7 +47,7 @@ async function coreFormat(originalText, opts, addAlignmentSize = 0) {
}
const astComments = attachComments(text, ast, opts);
const doc = printAstToDoc(ast, opts, addAlignmentSize);
const doc = await printAstToDoc(ast, opts, addAlignmentSize);
const result = printDocToString(doc, opts);
@@ -334,9 +334,9 @@ const prettier = {
return parsed;
},
formatAST(ast, options) {
async formatAST(ast, options) {
options = normalizeOptions(options);
const doc = printAstToDoc(ast, options);
const doc = await printAstToDoc(ast, options);
return printDocToString(doc, options);
},
+110 -23
View File
@@ -1,27 +1,118 @@
import { stripTrailingHardline } from "../document/utils.js";
import { normalize } from "./options.js";
import { ensureAllCommentsPrinted, attach } from "./comments.js";
import { parseSync } from "./parser.js";
import { parse } from "./parser.js";
function printSubtree(path, print, options, printAstToDoc) {
if (options.printer.embed && options.embeddedLanguageFormatting === "auto") {
return options.printer.embed(
path,
print,
(text, partialNextOptions, textToDocOptions) =>
textToDoc(
text,
partialNextOptions,
options,
printAstToDoc,
textToDocOptions
),
options
async function printEmbeddedLanguages(
/** @type {import("../common/ast-path").default} */ path,
genericPrint,
options,
printAstToDoc,
embeds
) {
const { printer } = options;
if (!printer.embed || options.embeddedLanguageFormatting !== "auto") {
return;
}
if (printer.embed.length > 2) {
throw new Error(
"printer.embed has too many parameters. The API changed in Prettier v3. Please update your plugin. See https://prettier.io/docs/en/plugins.html#optional-embed"
);
}
const isNode =
(printer.isNode ?? printer.canAttachComment)?.bind(printer) ?? (() => true);
const hasPrettierIgnore =
printer.hasPrettierIgnore?.bind(printer) ?? (() => false);
const ignoredProperties = printer.massageAstNode?.ignoredProperties;
const embedCallResults = [];
recurse();
const originalPathStack = path.stack;
for (const { print, node, pathStack } of embedCallResults) {
try {
path.stack = pathStack;
const doc = await print(textToDocForEmbed, genericPrint, path, options);
if (doc) {
embeds.set(node, doc);
}
} catch (error) {
/* istanbul ignore if */
if (process.env.PRETTIER_DEBUG) {
throw error;
}
}
}
path.stack = originalPathStack;
function textToDocForEmbed(text, partialNextOptions, textToDocOptions) {
return textToDoc(
text,
partialNextOptions,
options,
printAstToDoc,
textToDocOptions
);
}
function recurse() {
const node = path.getValue();
if (
node === null ||
typeof node !== "object" ||
!isNode(node) ||
hasPrettierIgnore(path)
) {
return;
}
for (const key in node) {
if (
Object.prototype.hasOwnProperty.call(node, key) &&
!ignoredProperties?.has(key)
) {
if (Array.isArray(node[key])) {
path.each(recurse, key);
} else {
path.call(recurse, key);
}
}
}
const result = printer.embed(path, options);
if (!result) {
return;
}
if (typeof result === "function") {
embedCallResults.push({
print: result,
node,
pathStack: [...path.stack],
});
return;
}
if (process.env.PRETTIER_DEBUG && typeof result.then === "function") {
throw new Error(
"`embed` should return an async function instead of Promise."
);
}
embeds.set(node, result);
}
}
function textToDoc(
async function textToDoc(
text,
partialNextOptions,
parentOptions,
@@ -39,13 +130,9 @@ function textToDoc(
{ passThrough: true }
);
const result = parseSync(text, nextOptions);
const result = await parse(text, nextOptions);
const { ast } = result;
if (typeof ast?.then === "function") {
throw new TypeError("async parse is not supported in embed");
}
text = result.text;
const astComments = ast.comments;
@@ -56,7 +143,7 @@ function textToDoc(
// @ts-expect-error -- Casting to `unique symbol` isn't allowed in JSDoc comment
nextOptions[Symbol.for("tokens")] = ast.tokens || [];
const doc = printAstToDoc(ast, nextOptions);
const doc = await printAstToDoc(ast, nextOptions);
ensureAllCommentsPrinted(astComments);
if (shouldStripTrailingHardline) {
@@ -72,4 +159,4 @@ function textToDoc(
return doc;
}
export { printSubtree };
export { printEmbeddedLanguages };
+1 -37
View File
@@ -102,40 +102,4 @@ function handleParseError(error, text) {
throw error;
}
// TODO: Remove this when we allow async parser in embed
function parseSync(originalText, opts) {
const parsers = getParsers(opts);
// Create a new object {parserName: parseFn}. Uses defineProperty() to only call
// the parsers getters when actually calling the parser `parse` function.
const parsersForCustomParserApi = Object.defineProperties(
{},
Object.fromEntries(
Object.keys(parsers).map((parserName) => [
parserName,
{
enumerable: true,
get() {
return parsers[parserName].parse;
},
},
])
)
);
const parser = resolveParser(opts, parsers);
const text = parser.preprocess
? parser.preprocess(originalText, opts)
: originalText;
let ast;
try {
ast = parser.parse(text, parsersForCustomParserApi, opts);
} catch (error) {
handleParseError(error, originalText);
}
return { text, ast };
}
export { parse, parseSync, resolveParser };
export { parse, resolveParser };
+6 -2
View File
@@ -1,10 +1,14 @@
import { hardline, markAsRoot } from "../../document/builders.js";
function print(node, textToDoc) {
async function print(node, textToDoc) {
if (node.lang === "yaml") {
const value = node.value.trim();
const doc = value
? textToDoc(value, { parser: "yaml" }, { stripTrailingHardline: true })
? await textToDoc(
value,
{ parser: "yaml" },
{ stripTrailingHardline: true }
)
: "";
return markAsRoot([
node.startDelimiter,
@@ -1,34 +0,0 @@
"use strict";
const name = "async-parser";
module.exports = {
languages: [
{
name,
parsers: [name],
},
],
parsers: {
[name]: {
parse: (text) => Promise.resolve({ text, isAsyncParserPluginAst: true }),
astFormat: name,
locStart() {},
locEnd() {},
},
},
printers: {
[name]: {
print(path) {
if (!path.getNode().isAsyncParserPluginAst) {
throw new Error("Unexpected parse result.");
}
return "Formatted by async-parser plugin";
},
massageAstNode() {
return { text: "AST text value placeholder" };
},
},
},
};
@@ -0,0 +1,43 @@
"use strict";
const name = "async-printer";
module.exports = {
languages: [
{
name,
parsers: [name],
},
],
parsers: {
[name]: {
parse: (text) => Promise.resolve(JSON.parse(text)),
astFormat: name,
locStart() {},
locEnd() {},
},
},
printers: {
[name]: {
preprocess(ast) {
return Promise.resolve({
...ast,
preprocess: ast.preprocess?.toLowerCase() ?? "",
});
},
print(path) {
const ast = path.getValue();
// print may be async if not used recursively
return Promise.resolve(
JSON.stringify({
...ast,
print: ast.print?.replace(/\s+/g, " ") ?? "",
})
);
},
massageAstNode() {
return { text: "AST text value placeholder" };
},
},
},
};
@@ -1,6 +1,6 @@
{
"private": true,
"name": "prettier-plugin-async-parser",
"name": "prettier-plugin-async-printer",
"version": "1.0.0",
"main": "./index.cjs"
}
@@ -1 +0,0 @@
A text will format as "Formatted by async-parser plugin".
@@ -1,14 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`async-parser.txt format 1`] = `
exports[`async-printer.txt format 1`] = `
====================================options=====================================
parsers: ["async-parser"]
parsers: ["async-printer"]
printWidth: 80
| printWidth
=====================================input======================================
A text will format as "Formatted by async-parser plugin".
{ "preprocess": "Lorem Ipsum", "print": "Lorem Ipsum" }
=====================================output=====================================
Formatted by async-parser plugin
{"preprocess":"lorem ipsum","print":"Lorem Ipsum"}
================================================================================
`;
@@ -0,0 +1 @@
{ "preprocess": "Lorem Ipsum", "print": "Lorem Ipsum" }
@@ -3,7 +3,7 @@ import createEsmUtils from "esm-utils";
const { require } = createEsmUtils(import.meta);
const plugins = [
require("../../../../config/prettier-plugins/prettier-plugin-async-parser/index.cjs"),
require("../../../../config/prettier-plugins/prettier-plugin-async-printer/index.cjs"),
];
run_spec(import.meta, ["async-parser"], { plugins });
run_spec(import.meta, ["async-printer"], { plugins });
@@ -1,40 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`async-parser.txt format 1`] = `
====================================options=====================================
parsers: ["async-parser"]
printWidth: 80
| printWidth
=====================================input======================================
A text will format as "Formatted by async-parser plugin".
=====================================output=====================================
Formatted by async-parser plugin
================================================================================
`;
exports[`with-aync-parser-code-block.md format 1`] = `
====================================options=====================================
parsers: ["markdown"]
printWidth: 80
| printWidth
=====================================input======================================
\`\`\`async-parser
Since we don't allow async parser in embed format, so this will not be changed.
\`\`\`
\`\`\`uppercase-rocks
This text should be uppercased.
\`\`\`
=====================================output=====================================
\`\`\`async-parser
Since we don't allow async parser in embed format, so this will not be changed.
\`\`\`
\`\`\`uppercase-rocks
THIS TEXT SHOULD BE UPPERCASED.
\`\`\`
================================================================================
`;
@@ -1,7 +0,0 @@
```async-parser
Since we don't allow async parser in embed format, so this will not be changed.
```
```uppercase-rocks
This text should be uppercased.
```
@@ -0,0 +1,30 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`with-aync-printer-code-block.md format 1`] = `
====================================options=====================================
parsers: ["markdown"]
printWidth: 80
| printWidth
=====================================input======================================
\`\`\`async-printer
{
"preprocess": "This text will be lowercased.",
"print": "This text will be whitespace-normalized."
}
\`\`\`
\`\`\`uppercase-rocks
This text should be uppercased.
\`\`\`
=====================================output=====================================
\`\`\`async-printer
{"preprocess":"this text will be lowercased.","print":"This text will be whitespace-normalized."}
\`\`\`
\`\`\`uppercase-rocks
THIS TEXT SHOULD BE UPPERCASED.
\`\`\`
================================================================================
`;
@@ -1,6 +1,6 @@
const plugins = await Promise.all(
[
"../../../../config/prettier-plugins/prettier-plugin-async-parser/index.cjs",
"../../../../config/prettier-plugins/prettier-plugin-async-printer/index.cjs",
"../../../../config/prettier-plugins/prettier-plugin-uppercase-rocks/index.js",
].map(async (plugin) => (await import(plugin)).default)
);
@@ -0,0 +1,10 @@
```async-printer
{
"preprocess": "This text will be lowercased.",
"print": "This text will be whitespace-normalized."
}
```
```uppercase-rocks
This text should be uppercased.
```
@@ -0,0 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`embed API in plugins with new signature should work 1`] = `
"abcdef
{ "foo": "bar" }
1234
"
`;
exports[`embed API in plugins with outdated signature should cause error 1`] = `"printer.embed has too many parameters. The API changed in Prettier v3. Please update your plugin. See https://prettier.io/docs/en/plugins.html#optional-embed"`;
@@ -0,0 +1,87 @@
import prettier from "../../config/prettier-entry.js";
const {
doc: {
builders: { hardline },
},
format,
} = prettier;
function makePlugin(withOutdatedApi = false) {
return {
parsers: {
// parsers leak across tests, so the name needs to be unique
[withOutdatedApi ? "baz-parser-outdated" : "baz-parser"]: {
parse: (text) => ({
type: "root",
lines: text
.trim()
.split("\n")
.map((line) => ({
type: line.startsWith("{") ? "json" : "plain",
text: line,
})),
}),
astFormat: "baz-ast",
},
},
printers: {
"baz-ast": {
print: (path, options, print) => {
const { type, text } = path.getValue();
switch (type) {
case "root":
return path.map(print, "lines");
case "json":
case "plain":
return [text, hardline];
}
},
embed: withOutdatedApi
? (path, print, textToDoc) => {
const { type, text } = path.getValue();
if (type === "json") {
return textToDoc(text, {
parser: "json",
printWidth: Number.POSITIVE_INFINITY,
});
}
}
: (path) => {
const { type, text } = path.getValue();
if (type === "json") {
return (textToDoc) =>
textToDoc(text, {
parser: "json",
printWidth: Number.POSITIVE_INFINITY,
});
}
},
},
},
};
}
const input = `abcdef
{ "foo": "bar"}
1234`;
describe("embed API in plugins", () => {
test("with new signature should work", async () => {
expect(
await format(input, {
plugins: [makePlugin()],
parser: "baz-parser",
})
).toMatchSnapshot();
});
test("with outdated signature should cause error", async () => {
await expect(
format(input, {
plugins: [makePlugin(true)],
parser: "baz-parser-outdated",
})
).rejects.toThrowErrorMatchingSnapshot();
});
});