插件
插件是向 Prettier 添加新语言或格式化规则的方法。Prettier 对所有语言的实现都是使用插件 API 表达的。核心 prettier
包内置了 JavaScript 和其他面向 Web 的语言。对于其他语言,您需要安装插件。
使用插件
您可以使用以下方式加载插件:
通过
--plugin
选项使用 命令行工具prettier --write main.foo --plugin=prettier-plugin-foo
提示:您可以多次设置
--plugin
选项。通过
plugins
选项使用 APIawait prettier.format("code", { parser: "foo", plugins: ["prettier-plugin-foo"], });
通过 配置文件
{ "plugins": ["prettier-plugin-foo"] }
提供给 plugins
的字符串最终会被传递给 import()
表达式,因此您可以提供模块/包名称、路径或 import()
接受的任何其他内容。
官方插件
@prettier/plugin-php
@prettier/plugin-pug
由 @Shinigami92 维护@prettier/plugin-ruby
@prettier/plugin-xml
社区插件
prettier-plugin-apex
由 @dangmai 维护prettier-plugin-astro
由 @withastro 贡献者 维护prettier-plugin-elm
由 @giCentre 维护prettier-plugin-erb
由 @adamzapasnik 维护prettier-plugin-gherkin
由 @mapado 维护prettier-plugin-glsl
由 @NaridaL 维护prettier-plugin-go-template
由 @NiklasPor 维护prettier-plugin-java
由 @JHipster 维护prettier-plugin-jinja-template
由 @davidodenwald 维护prettier-plugin-jsonata
由 @Stedi 维护prettier-plugin-kotlin
由 @Angry-Potato 维护prettier-plugin-motoko
由 @dfinity 维护prettier-plugin-nginx
由 @joedeandev 维护prettier-plugin-prisma
由 @umidbekk 维护prettier-plugin-properties
由 @eemeli 维护prettier-plugin-rust
由 @jinxdash 维护prettier-plugin-sh
由 @JounQin 维护prettier-plugin-sql
由 @JounQin 维护prettier-plugin-sql-cst
由 @nene 维护prettier-plugin-solidity
由 @mattiaerre 维护prettier-plugin-svelte
由 @sveltejs 维护prettier-plugin-toml
由 @bd82 维护
开发插件
Prettier 插件是普通的 JavaScript 模块,具有以下五个导出或默认导出,以及以下属性:
languages
parsers
printers
options
defaultOptions
languages
languages
是一个语言定义数组,您的插件将将其贡献给 Prettier。它可以包含 prettier.getSupportInfo()
中指定的所有字段。
它**必须**包含 name
和 parsers
。
export const languages = [
{
// The language name
name: "InterpretedDanceScript",
// Parsers that can parse this language.
// This can be built-in parsers, or parsers you have contributed via this plugin.
parsers: ["dance-parse"],
},
];
parsers
解析器将代码作为字符串转换为 AST(抽象语法树)。
键必须与 languages
中 parsers
数组中的名称匹配。值包含一个解析函数、一个 AST 格式名称和两个位置提取函数(locStart
和 locEnd
)。
export const parsers = {
"dance-parse": {
parse,
// The name of the AST that the parser produces.
astFormat: "dance-ast",
hasPragma,
locStart,
locEnd,
preprocess,
},
};
parse
函数的签名为:
function parse(text: string, options: object): Promise<AST> | AST;
位置提取函数(locStart
和 locEnd
)返回给定 AST 节点的起始和结束位置。
function locStart(node: object): number;
(可选) hasPragma
函数应返回文本是否包含 pragma 注释。
function hasPragma(text: string): boolean;
(可选) preprocess
函数可以在将输入文本传递给 parse
函数之前对其进行处理。
function preprocess(text: string, options: object): string;
printers
打印器将 AST 转换为 Prettier 中间表示形式,也称为 Doc。
键必须与解析器生成的 astFormat
匹配。值包含一个具有 print
函数的对象。所有其他属性(embed
、preprocess
等)都是可选的。
export const printers = {
"dance-ast": {
print,
embed,
preprocess,
getVisitorKeys,
insertPragma,
canAttachComment,
isBlockComment,
printComment,
getCommentChildNodes,
handleComments: {
ownLine,
endOfLine,
remaining,
},
},
};
打印过程
Prettier 使用一个中间表示形式,称为 Doc,然后 Prettier 会将其转换为字符串(基于 printWidth
等选项)。打印器 的作用是获取 parsers[<parser name>].parse
生成的 AST 并返回一个 Doc。Doc 使用 构建器命令 构建。
import { doc } from "prettier";
const { join, line, ifBreak, group } = doc.builders;
打印过程包括以下步骤:
- AST 预处理(可选)。请参阅
preprocess
。 - 注释附加(可选)。请参阅 在打印器中处理注释。
- 处理嵌入式语言(可选)。如果定义了
embed
方法,则会对每个节点进行深度优先调用。出于性能原因,递归本身是同步的,但embed
可能会返回异步函数,这些函数可以调用其他解析器和打印器来组合嵌入语法(如 CSS-in-JS)的文档。这些返回的函数会在下一步之前排队并依次执行。 - 递归打印。从 AST 递归构建 Doc。从根节点开始。
- 如果从步骤 3 中,当前节点关联了一个嵌入式语言文档,则使用此文档。
- 否则,将调用
print(path, options, print): Doc
方法。它会为当前节点组合一个 Doc,通常是通过使用print
回调打印子节点来实现。
print
插件的打印机大部分工作将在其print
函数中进行,其签名为
function print(
// Path to the AST node to print
path: AstPath,
options: object,
// Recursively print a child node
print: (selector?: string | number | Array<string | number> | AstPath) => Doc,
): Doc;
print
函数传递以下参数
path
:一个对象,可用于访问 AST 中的节点。它是一个类似栈的数据结构,维护着递归的当前状态。之所以称为“path”(路径),是因为它表示从 AST 的根节点到当前节点的路径。当前节点由path.node
返回。options
:一个持久化对象,包含全局选项,插件可以修改它来存储上下文数据。print
:用于打印子节点的回调函数。此函数包含核心打印逻辑,该逻辑由插件提供的步骤组成。特别是,它调用打印机的print
函数并将自身传递给它。因此,核心中的两个print
函数和插件中的一个print
函数在递归向下遍历 AST 时相互调用。
以下是一个简化的示例,以便了解典型print
实现的样貌
import { doc } from "prettier";
const { group, indent, join, line, softline } = doc.builders;
function print(path, options, print) {
const node = path.node;
switch (node.type) {
case "list":
return group([
"(",
indent([softline, join(line, path.map(print, "elements"))]),
softline,
")",
]);
case "pair":
return group([
"(",
indent([softline, print("left"), line, ". ", print("right")]),
softline,
")",
]);
case "symbol":
return node.name;
}
throw new Error(`Unknown node type: ${node.type}`);
}
查看prettier-python 的打印机,了解一些可能的示例。
embed
(可选) 打印机可以拥有embed
方法来在一个语言中打印另一种语言。例如,在 Markdown 中打印 CSS-in-JS 或围栏代码块。签名为
function embed(
// Path to the current AST node
path: AstPath,
// Current options
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;
embed
方法类似于print
方法,它将 AST 节点映射到文档,但与print
不同的是,它可以通过返回异步函数来执行异步工作。该函数的第一个参数,textToDoc
异步函数,可用于使用不同的插件呈现文档。
如果从embed
返回的函数返回一个文档或解析为文档的 Promise,则该文档将用于打印,并且不会为该节点调用print
方法。在某些情况下,也可能并且方便地直接从embed
同步返回文档,但是在这种情况下,textToDoc
和print
回调不可用。返回一个函数来获取它们。
如果embed
返回undefined
,或者它返回的函数返回undefined
或解析为undefined
的 Promise,则该节点将使用print
方法正常打印。如果返回的函数抛出错误或返回拒绝的 Promise(例如,如果发生解析错误),也会发生这种情况。如果希望 Prettier 重新抛出这些错误,请将PRETTIER_DEBUG
环境变量设置为非空值。
例如,一个插件,其节点包含嵌入的 JavaScript,可能具有以下embed
方法
function embed(path, options) {
const node = path.node;
if (node.type === "javascript") {
return async (textToDoc) => {
return [
"<script>",
hardline,
await textToDoc(node.javaScriptCode, { parser: "babel" }),
hardline,
"</script>",
];
};
}
}
如果--embedded-language-formatting
选项设置为off
,则嵌入步骤将完全跳过,不会调用embed
,并且所有节点都将使用print
方法打印。
preprocess
(可选) preprocess
方法可以在将 AST 传递到print
方法之前处理来自解析器的 AST。
function preprocess(ast: AST, options: Options): AST | Promise<AST>;
getVisitorKeys
(可选) 如果插件使用注释附加或嵌入语言,此属性可能派上用场。这些功能遍历 AST,迭代每个节点(从根节点开始)的所有自己的可枚举属性。如果 AST 具有循环,则此类遍历最终会导致无限循环。此外,节点可能包含非节点对象(例如,位置数据),迭代这些对象会浪费资源。为了解决这些问题,打印机可以定义一个函数来返回应遍历的属性名称。
其签名为
function getVisitorKeys(node, nonTraversableKeys: Set<string>): string[];
默认的getVisitorKeys
function getVisitorKeys(node, nonTraversableKeys) {
return Object.keys(node).filter((key) => !nonTraversableKeys.has(key));
}
第二个参数nonTraversableKeys
是一组通用键和 Prettier 内部使用的键。
如果您拥有完整的访问者键列表
const visitorKeys = {
Program: ["body"],
Identifier: [],
// ...
};
function getVisitorKeys(node /* , nonTraversableKeys*/) {
// Return `[]` for unknown node to prevent Prettier fallback to use `Object.keys()`
return visitorKeys[node.type] ?? [];
}
如果您只需要排除一小部分键
const ignoredKeys = new Set(["prev", "next", "range"]);
function getVisitorKeys(node, nonTraversableKeys) {
return Object.keys(node).filter(
(key) => !nonTraversableKeys.has(key) && !ignoredKeys.has(key),
);
}
insertPragma
(可选) 当使用--insert-pragma
选项时,插件可以在insertPragma
函数中实现如何在生成的代码中插入pragma注释。其签名为
function insertPragma(text: string): string;
在打印机中处理注释
注释通常不是语言 AST 的一部分,对漂亮打印机来说是一个挑战。Prettier 插件可以在其print
函数中自行打印注释,或者依赖 Prettier 的注释算法。
默认情况下,如果 AST 具有顶级comments
属性,则 Prettier 假设comments
存储注释节点的数组。然后,Prettier 将使用提供的parsers[<plugin>].locStart
/locEnd
函数来搜索每个注释“属于”的 AST 节点。然后将注释附加到这些节点(在此过程中**修改 AST**),并且从 AST 根节点删除comments
属性。*Comment
函数用于调整 Prettier 的算法。将注释附加到 AST 后,Prettier 将自动调用printComment(path, options): Doc
函数并将返回的文档插入到(希望是)正确的位置。
getCommentChildNodes
(可选) 默认情况下,Prettier 递归搜索每个节点的所有对象属性(除了几个预定义的属性)。可以提供此函数来覆盖此行为。其签名为
function getCommentChildNodes(
// The node whose children should be returned.
node: AST,
// Current options
options: object,
): AST[] | undefined;
如果节点没有子节点,则返回[]
,或者返回undefined
以回退到默认行为。
printComment
(可选) 每当需要打印注释节点时调用。其签名为
function printComment(
// Path to the current comment node
commentPath: AstPath,
// Current options
options: object,
): Doc;
canAttachComment
(可选) function canAttachComment(node: AST): boolean;
此函数用于确定是否可以将注释附加到特定 AST 节点。默认情况下,遍历**所有** AST 属性以搜索可以附加注释的节点。此函数用于防止将注释附加到特定节点。典型的实现如下所示
function canAttachComment(node) {
return node.type && node.type !== "comment";
}
isBlockComment
(可选) function isBlockComment(node: AST): boolean;
返回 AST 节点是否为块注释。
handleComments
(可选) handleComments
对象包含三个可选函数,每个函数的签名为
(
// The AST node corresponding to the comment
comment: AST,
// The full source code text
text: string,
// The global options object
options: object,
// The AST
ast: AST,
// Whether this comment is the last comment
isLastComment: boolean,
) => boolean;
这些函数用于覆盖 Prettier 的默认注释附加算法。ownLine
/endOfLine
/remaining
预期要么手动将注释附加到节点并返回 true
,要么返回 false
并让 Prettier 附加注释。
根据注释节点周围的文本,Prettier 会分派
- 如果注释前面只有空格并且后面有换行符,则使用
ownLine
, - 如果注释后面有换行符,但前面有一些非空格字符,则使用
endOfLine
, - 在所有其他情况下使用
remaining
。
在分派时,Prettier 将会在每个 AST 注释节点上添加注释(即,创建新的属性),至少包含 enclosingNode
、precedingNode
或 followingNode
之一。这些可用于辅助插件的决策过程(当然,完整的 AST 和原始文本也会被传入以进行更复杂的决策)。
手动附加注释
util.addTrailingComment
/addLeadingComment
/addDanglingComment
函数可用于手动将注释附加到 AST 节点。一个确保注释不跟随“标点符号”节点(为演示目的而虚构)的 ownLine
函数示例可能如下所示
import { util } from "prettier";
function ownLine(comment, text, options, ast, isLastComment) {
const { precedingNode } = comment;
if (precedingNode && precedingNode.type === "punctuation") {
util.addTrailingComment(precedingNode, comment);
return true;
}
return false;
}
带有注释的节点预期具有一个 comments
属性,该属性包含一个注释数组。每个注释都预期具有以下属性:leading
、trailing
、printed
。
上面的示例使用了 util.addTrailingComment
,它会自动将 comment.leading
/trailing
/printed
设置为适当的值,并将注释添加到 AST 节点的 comments
数组中。
--debug-print-comments
CLI 标志可以帮助调试注释附加问题。它会打印一个详细的注释列表,其中包含有关每个注释如何分类(ownLine
/endOfLine
/remaining
、leading
/trailing
/dangling
)以及附加到哪个节点的信息。对于 Prettier 的内置语言,此信息也可在 Playground 中获得(调试部分中的“显示注释”复选框)。
options
options
是一个包含插件支持的自定义选项的对象。
示例
export default {
// ... plugin implementation
options: {
openingBraceNewLine: {
type: "boolean",
category: "Global",
default: true,
description: "Move open brace for code blocks onto new line.",
},
},
};
defaultOptions
如果您的插件需要为某些 Prettier 的核心选项指定不同的默认值,您可以在 defaultOptions
中指定它们。
export default {
// ... plugin implementation
defaultOptions: {
tabWidth: 4,
},
};
实用程序函数
来自 Prettier 核心的 util
模块被视为私有 API,插件不应使用它。相反,util-shared
模块为插件提供以下有限的实用程序函数集
type Quote = '"' | "'";
type SkipOptions = { backwards?: boolean };
function getMaxContinuousCount(text: string, searchString: string): number;
function getStringWidth(text: string): number;
function getAlignmentSize(
text: string,
tabWidth: number,
startIndex?: number,
): number;
function getIndentSize(value: string, tabWidth: number): number;
function skip(
characters: string | RegExp,
): (
text: string,
startIndex: number | false,
options?: SkipOptions,
) => number | false;
function skipWhitespace(
text: string,
startIndex: number | false,
options?: SkipOptions,
): number | false;
function skipSpaces(
text: string,
startIndex: number | false,
options?: SkipOptions,
): number | false;
function skipToLineEnd(
text: string,
startIndex: number | false,
options?: SkipOptions,
): number | false;
function skipEverythingButNewLine(
text: string,
startIndex: number | false,
options?: SkipOptions,
): number | false;
function skipInlineComment(
text: string,
startIndex: number | false,
): number | false;
function skipTrailingComment(
text: string,
startIndex: number | false,
): number | false;
function skipNewline(
text: string,
startIndex: number | false,
options?: SkipOptions,
): number | false;
function hasNewline(
text: string,
startIndex: number,
options?: SkipOptions,
): boolean;
function hasNewlineInRange(
text: string,
startIndex: number,
startIndex: number,
): boolean;
function hasSpaces(
text: string,
startIndex: number,
options?: SkipOptions,
): boolean;
function makeString(
rawText: string,
enclosingQuote: Quote,
unescapeUnnecessaryEscapes?: boolean,
): string;
function getNextNonSpaceNonCommentCharacter(
text: string,
startIndex: number,
): string;
function getNextNonSpaceNonCommentCharacterIndex(
text: string,
startIndex: number,
): number | false;
function isNextLineEmpty(text: string, startIndex: number): boolean;
function isPreviousLineEmpty(text: string, startIndex: number): boolean;
教程
- 如何为 Prettier 编写插件:教你如何为 TOML 编写一个非常基本的 Prettier 插件。
测试插件
由于可以使用相对路径解析插件,因此在处理一个插件时,您可以执行以下操作
import * as prettier from "prettier";
const code = "(add 1 2)";
await prettier.format(code, {
parser: "lisp",
plugins: ["."],
});
这将解析相对于当前工作目录的插件。