Support async parser for embedded languages (#13211)
Co-authored-by: fisker Cheung <lionkay@gmail.com>
This commit is contained in:
1 parent
31cdcd0487
commit
4d5b645e0e
35 files changed
+588
-413
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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: ["."],
|
||||
});
|
||||
|
||||
@@ -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
@@ -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, """) : 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, """) : doc
|
||||
)
|
||||
),
|
||||
'"',
|
||||
];
|
||||
}
|
||||
};
|
||||
}
|
||||
case "front-matter":
|
||||
return printFrontMatter(node, textToDoc);
|
||||
return (textToDoc) => printFrontMatter(node, textToDoc);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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");
|
||||
@@ -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 }
|
||||
|
||||
@@ -866,6 +866,7 @@ const printer = {
|
||||
remaining: handleComments.handleRemainingComment,
|
||||
},
|
||||
getCommentChildNodes: handleComments.getCommentChildNodes,
|
||||
isNode: (node) => typeof node.type === "string",
|
||||
};
|
||||
|
||||
export default printer;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 };
|
||||
@@ -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
-1
@@ -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".
|
||||
+4
-4
@@ -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" }
|
||||
+2
-2
@@ -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
-1
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user