本文翻译自:https://prettier.io/docs/en/plugins.html
Prettier的插件机制用于对新增语言实现格式化。Prettier目前自己实现的对所有语言的格式化都是基于插件API实现的。prettier的核心包内置了JavaScript以及其他web主流语言的格式化实现。对于其他语言的格式化支持你需要安装对应的插件。
使用插件
如果插件安装在prettier所在的同一个node modules目录中,插件会被自动加载。插件包的命名必须以“@prettier/plugin-”或“prettier-plugin-”或者“@
> 记得要被替换为一个其他的名字,更多信息可以参考 > NPM scope> .
当插件不能被自动发现的时候,你可以以下面的方式加载它:
- cli的方式,通过—plugin 以及 —plugin-search-dir参数:
prettier --write main.foo --plugin-search-dir=./dir-with-plugins --plugin=./foo-plugin
注意:你可以多次设置> —plugin> 或 > —plugin-search-dir> 选项
- 或者API的方式,通过设置plugins 及 pluginSearchDirs的方式:
Prettier期望每一个pluginSearchDirs 都包含 node_modules 子目录,这样@prettier/plugin-, @/prettier-plugin- 以及 prettier-plugin-会在node_modules文件夹中被查询。比如,pluginSearchDirs 可以是你的项目目录或者是全局的npm模块的位置。prettier.format("code", {
parser: "foo",
pluginSearchDirs: ["./dir-with-plugins"],
plugins: ["./foo-plugin"],
});
如果 —plugin-search-dir/pluginSearchDirs 中的至少一个被提供了参数,就会停止自动从默认的目录加载插件(比如, prettier 下的 node_modules 目录)
官方插件
- @prettier/plugin-php
- @prettier/plugin-pug by @Shinigami92
- @prettier/plugin-ruby
- @prettier/plugin-swift
- @prettier/plugin-xml
社区插件
- prettier-plugin-apex by @dangmai
- prettier-plugin-elm by @giCentre
- prettier-plugin-java by @JHipster
- prettier-plugin-kotlin by @Angry-Potato
- prettier-plugin-package by @shellscape
- prettier-plugin-packagejson by @matzkoh
- prettier-plugin-pg by @benjie
- prettier-plugin-properties by @eemeli
- prettier-plugin-solidity by @mattiaerre
- prettier-plugin-svelte by @UnwrittenFun
- prettier-plugin-toml by @bd82
- prettier-plugin-organize-imports by @simonhaenisch
- prettier-plugin-pkg by @JounQin
- prettier-plugin-sh by @JounQin
开发插件
Prettier的插件就是常规的JavaScript模块,有下面的5个导出项:
- 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
Parsers负责将代码字符串转为AST(Abstract Syntax Tree)。parseres的key必须跟 languages数组里某一项的 parsers数组里的名字匹配。值包含一个parse函数,一个AST格式化的名字,两个位置提取函数 (locStart and locEnd)。
export const parsers = {
"dance-parse": {
parse,
// The name of the AST that
astFormat: "dance-ast",
hasPragma,
locStart,
locEnd,
preprocess,
},
};
parse 函数的格式为:
function parse(text: string, parsers: object, options: object): AST;
位置抽取函数 (locStart and locEnd) 返回一个给定AST节点的开始及结束位置:
function locStart(node: object): number;
(可选)pragma检测函数 (hasPragma) 应该返回代码字符串是否包含注释。
function hasPragma(text: string): boolean;
(可选)预处理(preprocess)函数可以在执行parse函数之前预处理输入文本。
function preprocess(text: string, options: object): string;
printers
Printers负责将所有的AST转化为一种Prettier的中间表示,也被称作:Doc。
printers的key值必须与上面的parsers中parser的 astFormat 相匹配(比如上面:dance-parse的astFormat值为:’dance-ast’)。值是一个包含一个 print函数的对象,所有的其他属性 (embed, preprocess, 等等)都是可选的。
export const printers = {
"dance-ast": {
print,
embed,
preprocess,
insertPragma,
canAttachComment,
isBlockComment,
printComment,
handleComments: {
ownLine,
endOfLine,
remaining,
},
},
};
打印过程
Prettier使用一个被称作Doc的中间表示,然后将该中间表示转化为一个字符串(依赖于 printWidth等的选项)。一个printer的工作是解析 parsers[
const { concat, join, line, ifBreak, group } = require("prettier").doc.builders;
打印的过程(将AST转为Doc)如下:
- preprocess(ast: AST, options: object): AST, 存在于Parsers中,如果配置了的话,将会被调用。会被传入parser生成的AST作为参数。被 preprocess 返回的AST将会被Prettier使用。如果 preprocess 没有被配置,parser返回的AST将会被直接使用。
- 注释节点会被附加到AST上(可以参考 在一个printer中处理注释 来查看更多细节)
- Doc是从AST递归构造的。
- embed(path: FastPath, print, textToDoc, options: object): Doc | null 在每一个AST节点被会被调用。如果 embed 返回了一个Doc,该Doc就会被使用。
- 如果 embed 没有被定义或者返回了一个falsy的值, print(path: FastPath, options: object, print): Doc会在每一个AST节点上被调用。
print
一个插件的printer大部分的工作都将发生在其 print 函数中,该函数的结构是:
print 函数被传入一个 path 对象,可以通过path.getValue()来获取AST中的节点信息。还被传递了一个持久化的 options 对象(其中包含全局的options,插件可能会改变该该配置项),以及一个 print 用来做递归调用。一个基本的print 函数可能像下面的代码所示:function print(
// Path to the AST node to print
path: FastPath,
options: object,
// Recursively print a child node
print: (path: FastPath) => Doc
): Doc;
可以看下 prettier-python’s printer 来了解一些可能的例子。const { builders } = require("prettier").doc;
function print(path, options, print) {
const node = path.getValue();
if (Array.isArray(node)) {
return builders.concat(path.map(print));
}
return node.value;
}
(可选) embed
embed 函数在插件需要打印一个嵌套在另一种语言中的语言的时候被调用。例如打印css-in-js或在Markdown中打印fenced代码块。其签名为:
function embed(
// Path to the current AST node
path: FastPath,
// Print a node with the current printer
print: (path: FastPath) => 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;
embed 函数的行为类似于 print ,除了它会被额外传递一个 textToDoc参数,该参数可以被用来使用不同的插件来渲染一个Doc。 embed 函数返回一个Doc或者一个falsy值。如果一个falsy值被返回,载当前 path下 print 函数会被执行。如果返回了一个Doc,该Doc会在打印中被使用, print 将不会再被调用。
比如,一个具有嵌入JavaScript节点的插件可能具有以下 embed 函数:
function embed(path, print, textToDoc, options) {
const node = path.getValue();
if (node.type === "javascript") {
return textToDoc(node.javaScriptText, { ...options, parser: "babel" });
}
return false;
}
(可选)preprocess
preprocess函数可以在AST被传入到 print 函数中之前提前进入到parser的AST。function preprocess(ast: AST, options: object): AST;
(可选) insertPragma
在 insertPragma 函数中使用 —insert-pragma 选项的时候,一个插件可以实现将一个注释代码插入到最终的结果字符串中。它的签名是:
function insertPragma(text: string): string;
在printer中处理注释
注释通常并不会作为一们语言的AST的一部分,这对prettier的打印器来说是一项挑战。一个Prettier的插件既可以在它自身的 print 函数中打印出注释,也可以依赖于Prettier的注释算法。
默认的,如果AST的顶层(根目录)拥有 comments 属性,Prettier就会假定该 comments 属性是包含有注释节点的数组。然后Prettier会使用提供的 parsers[
(可选)printComment
当一个注释节点需要被打印时会被调用。签名为:
function printComment(
// Path to the current comment node
commentPath: FastPath,
// 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 对象包含三个可选的函数,每一个函数都具有下面的签名:
function(
// 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调度的时候,Prettier将至少使用 enclosingNode, precedingNode, 或者 followingNode中的一个方法对每一个AST comment节点(如:新创建的属性)进行注释。这可以用来帮助插件做决策(当然,为了做出更复杂的决定,整个AST和原始文本也会被传递进来)。
手动附加注释
util.addTrailingComment/addLeadingComment/addDanglingComment 函数可以被用来手动的为AST节点添加注释。一个 ownLine 函数的例子可以确保注释不跟在“标点符号”节点(为演示目的而编写)后面,可能如下所示:
const { util } = require("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 数组中。
options
options是一个包含插件支持的自定义选项的对象。一个可能的例子是:
options: {
openingBraceNewLine: {
type: "boolean",
category: "Global",
default: true,
description: "Move open brace for code blocks onto new line."
}
}
defaultOptions
如果您的插件要求Prettier的一些核心选项有不同的默认值,您可以在 defaultOptions 中指定它们:
defaultOptions: {
tabWidth: 4
}
工具函数
来自Prettier core的 util 模块被认为是一个私有API,并不打算被插件消费。作为替代, util-shared 模块为插件提供了以下有限的工具函数集合:
type Quote = '"' | "'";
type SkipOptions = { backwards?: boolean };
function getMaxContinuousCount(str: string, target: string): number;
function getStringWidth(text: string): number;
function getAlignmentSize(value: string, tabWidth: number, startIndex?: number): number;
function getIndentSize(value: string, tabWidth: number): number;
function skip(chars: string | RegExp): (text: string, index: number | false, opts?: SkipOptions) => number | false;
function skipWhitespace(text: string, index: number | false, opts?: SkipOptions): number | false;
function skipSpaces(text: string, index: number | false, opts?: SkipOptions): number | false;
function skipToLineEnd(text: string, index: number | false, opts?: SkipOptions): number | false;
function skipEverythingButNewLine(text: string, index: number | false, opts?: SkipOptions): number | false;
function skipInlineComment(text: string, index: number | false): number | false;
function skipTrailingComment(text: string, index: number | false): number | false;
function skipNewline(text: string, index: number | false, opts?: SkipOptions): number | false;
function hasNewline(text: string, index: number, opts?: SkipOptions): boolean;
function hasNewlineInRange(text: string, start: number, end: number): boolean;
function hasSpaces(text: string, index: number, opts?: SkipOptions): boolean;
function makeString(rawContent: string, enclosingQuote: Quote, unescapeUnnecessaryEscapes?: boolean): string;
function getNextNonSpaceNonCommentCharacterIndex<N>(text: string, node: N, locEnd: (node: N) => number): number | false;
function isNextLineEmptyAfterIndex(text: string, index: number): boolean;
function isNextLineEmpty<N>(text: string, node: N, locEnd: (node: N) => number): boolean;
function isPreviousLineEmpty<N>(text: string, node: N, locStart: (node: N) => number): boolean;
教程
- How to write a plugin for Prettier:教你如何为TOML编写一个非常基本的Prettier插件 或者访问这个地址
测试插件
由于插件可以使用相对路径进行解析,因此在处理一个插件时可以这样做:
const prettier = require("prettier");
const code = "(add 1 2)";
prettier.format(code, {
parser: "lisp",
plugins: ["."],}
);
上面的代码会加载位于当前工作目录下的一个插件。