本文翻译自:https://prettier.io/docs/en/plugins.html

Prettier的插件机制用于对新增语言实现格式化。Prettier目前自己实现的对所有语言的格式化都是基于插件API实现的。prettier的核心包内置了JavaScript以及其他web主流语言的格式化实现。对于其他语言的格式化支持你需要安装对应的插件。

使用插件

如果插件安装在prettier所在的同一个node modules目录中,插件会被自动加载。插件包的命名必须以“@prettier/plugin-”或“prettier-plugin-”或者“@/prettier-plugin-”打头。

> 记得要被替换为一个其他的名字,更多信息可以参考 > NPM scope> .

当插件不能被自动发现的时候,你可以以下面的方式加载它:

  • cli的方式,通过—plugin 以及 —plugin-search-dir参数:
    1. prettier --write main.foo --plugin-search-dir=./dir-with-plugins --plugin=./foo-plugin

注意:你可以多次设置> —plugin> 或 > —plugin-search-dir> 选项

  • 或者API的方式,通过设置plugins 及 pluginSearchDirs的方式:
    1. prettier.format("code", {
    2. parser: "foo",
    3. pluginSearchDirs: ["./dir-with-plugins"],
    4. plugins: ["./foo-plugin"],
    5. });
    Prettier期望每一个pluginSearchDirs 都包含 node_modules 子目录,这样@prettier/plugin-, @/prettier-plugin- 以及 prettier-plugin-会在node_modules文件夹中被查询。比如,pluginSearchDirs 可以是你的项目目录或者是全局的npm模块的位置。

如果 —plugin-search-dir/pluginSearchDirs 中的至少一个被提供了参数,就会停止自动从默认的目录加载插件(比如, prettier 下的 node_modules 目录)

官方插件

社区插件

开发插件

Prettier的插件就是常规的JavaScript模块,有下面的5个导出项:

  • languages
  • parsers
  • printers
  • options
  • defaultOptions

    languages

    languages是你的插件即将为Prettier贡献的语言定义的数组。它可以包含在 prettier.getSupportInfo()中指定的所有字段。

数组中的每一项必须包含 name 以及 parsers。

  1. export const languages = [
  2. {
  3. // The language name
  4. name: "InterpretedDanceScript",
  5. // Parsers that can parse this language.
  6. // This can be built-in parsers, or parsers you have contributed via this plugin.
  7. parsers: ["dance-parse"],
  8. },
  9. ];

parsers

Parsers负责将代码字符串转为AST(Abstract Syntax Tree)。parseres的key必须跟 languages数组里某一项的 parsers数组里的名字匹配。值包含一个parse函数,一个AST格式化的名字,两个位置提取函数 (locStart and locEnd)。

  1. export const parsers = {
  2. "dance-parse": {
  3. parse,
  4. // The name of the AST that
  5. astFormat: "dance-ast",
  6. hasPragma,
  7. locStart,
  8. locEnd,
  9. preprocess,
  10. },
  11. };

parse 函数的格式为:

  1. function parse(text: string, parsers: object, options: object): AST;

位置抽取函数 (locStart and locEnd) 返回一个给定AST节点的开始及结束位置:

  1. function locStart(node: object): number;

(可选)pragma检测函数 (hasPragma) 应该返回代码字符串是否包含注释。

  1. function hasPragma(text: string): boolean;

(可选)预处理(preprocess)函数可以在执行parse函数之前预处理输入文本。

  1. 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, 等等)都是可选的。

  1. export const printers = {
  2. "dance-ast": {
  3. print,
  4. embed,
  5. preprocess,
  6. insertPragma,
  7. canAttachComment,
  8. isBlockComment,
  9. printComment,
  10. handleComments: {
  11. ownLine,
  12. endOfLine,
  13. remaining,
  14. },
  15. },
  16. };

打印过程

Prettier使用一个被称作Doc的中间表示,然后将该中间表示转化为一个字符串(依赖于 printWidth等的选项)。一个printer的工作是解析 parsers[].parse生成的AST并返回一个Doc。Doc使用 builder commands构建:

  1. const { concat, join, line, ifBreak, group } = require("prettier").doc.builders;

打印的过程(将AST转为Doc)如下:

  1. preprocess(ast: AST, options: object): AST, 存在于Parsers中,如果配置了的话,将会被调用。会被传入parser生成的AST作为参数。被 preprocess 返回的AST将会被Prettier使用。如果 preprocess 没有被配置,parser返回的AST将会被直接使用。
  2. 注释节点会被附加到AST上(可以参考 在一个printer中处理注释 来查看更多细节)
  3. Doc是从AST递归构造的。
    1. embed(path: FastPath, print, textToDoc, options: object): Doc | null 在每一个AST节点被会被调用。如果 embed 返回了一个Doc,该Doc就会被使用。
    2. 如果 embed 没有被定义或者返回了一个falsy的值, print(path: FastPath, options: object, print): Doc会在每一个AST节点上被调用。

      print

      一个插件的printer大部分的工作都将发生在其 print 函数中,该函数的结构是:
      1. function print(
      2. // Path to the AST node to print
      3. path: FastPath,
      4. options: object,
      5. // Recursively print a child node
      6. print: (path: FastPath) => Doc
      7. ): Doc;
      print 函数被传入一个 path 对象,可以通过path.getValue()来获取AST中的节点信息。还被传递了一个持久化的 options 对象(其中包含全局的options,插件可能会改变该该配置项),以及一个 print 用来做递归调用。一个基本的print 函数可能像下面的代码所示:
      1. const { builders } = require("prettier").doc;
      2. function print(path, options, print) {
      3. const node = path.getValue();
      4. if (Array.isArray(node)) {
      5. return builders.concat(path.map(print));
      6. }
      7. return node.value;
      8. }
      可以看下 prettier-python’s printer 来了解一些可能的例子。

(可选) embed

embed 函数在插件需要打印一个嵌套在另一种语言中的语言的时候被调用。例如打印css-in-js或在Markdown中打印fenced代码块。其签名为:

  1. function embed(
  2. // Path to the current AST node
  3. path: FastPath,
  4. // Print a node with the current printer
  5. print: (path: FastPath) => Doc,
  6. // Parse and print some text using a different parser.
  7. // You should set `options.parser` to specify which parser to use.
  8. textToDoc: (text: string, options: object) => Doc,
  9. // Current options
  10. options: object
  11. ): Doc | null;

embed 函数的行为类似于 print ,除了它会被额外传递一个 textToDoc参数,该参数可以被用来使用不同的插件来渲染一个Doc。 embed 函数返回一个Doc或者一个falsy值。如果一个falsy值被返回,载当前 path下 print 函数会被执行。如果返回了一个Doc,该Doc会在打印中被使用, print 将不会再被调用。

比如,一个具有嵌入JavaScript节点的插件可能具有以下 embed 函数:

  1. function embed(path, print, textToDoc, options) {
  2. const node = path.getValue();
  3. if (node.type === "javascript") {
  4. return textToDoc(node.javaScriptText, { ...options, parser: "babel" });
  5. }
  6. return false;
  7. }

(可选)preprocess

preprocess函数可以在AST被传入到 print 函数中之前提前进入到parser的AST。
function preprocess(ast: AST, options: object): AST;

(可选) insertPragma

在 insertPragma 函数中使用 —insert-pragma 选项的时候,一个插件可以实现将一个注释代码插入到最终的结果字符串中。它的签名是:

  1. function insertPragma(text: string): string;

在printer中处理注释

注释通常并不会作为一们语言的AST的一部分,这对prettier的打印器来说是一项挑战。一个Prettier的插件既可以在它自身的 print 函数中打印出注释,也可以依赖于Prettier的注释算法。

默认的,如果AST的顶层(根目录)拥有 comments 属性,Prettier就会假定该 comments 属性是包含有注释节点的数组。然后Prettier会使用提供的 parsers[].locStart/locEnd 函数来检查每一个注释应该依附的AST节点。然后在打印的过程中注释被附加到这些对应的节点上,修改AST,并从AST根目录中删除Comments属性。 *Comment函数被用来调整Prettier的算法。一旦注释被附加到了AST上,Prettier会自动调用 printComment(path, options): Doc 函数,并将返回的doc插入到(期望的)正确的位置上。

(可选)printComment

当一个注释节点需要被打印时会被调用。签名为:

  1. function printComment(
  2. // Path to the current comment node
  3. commentPath: FastPath,
  4. // Current options
  5. options: object
  6. ): Doc;

(可选)canAttachComment

  1. function canAttachComment(node: AST): boolean;

该函数被用于判断是否能将一个注释节点附加到某一个AST节点上。默认的,会遍历所有的AST属性,来搜索可以附加注释的节点。该函数被用于阻止注释节点被附加到某一个特定的节点上。典型的实现如下:

  1. function canAttachComment(node) {return node.type && node.type !== "comment";}

(可选)isBlockComment

function isBlockComment(node: AST): boolean;

判断AST节点是否是一个块注释节点。

(可选)handleComments

handleComments 对象包含三个可选的函数,每一个函数都具有下面的签名:

  1. function(
  2. // The AST node corresponding to the comment
  3. comment: AST,
  4. // The full source code text
  5. text: string,
  6. // The global options object
  7. options: object,
  8. // The AST
  9. ast: AST,
  10. // Whether this comment is the last comment
  11. isLastComment: boolean
  12. ): 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 函数的例子可以确保注释不跟在“标点符号”节点(为演示目的而编写)后面,可能如下所示:

  1. const { util } = require("prettier");
  2. function ownLine(comment, text, options, ast, isLastComment) {
  3. const { precedingNode } = comment;
  4. if (precedingNode && precedingNode.type === "punctuation") {
  5. util.addTrailingComment(precedingNode, comment);
  6. return true;
  7. }
  8. return false;
  9. }

带有注释的节点应具有包含注释数组的comments属性。每一个注释都应该包含下面的属性: leading, trailing, printed。

上面的例子使用了 util.addTrailingComment,该方法会自动的设置comment.leading/trailing/printed 为适当的值,并将注释添加到AST节点的 comments 数组中。

options

options是一个包含插件支持的自定义选项的对象。一个可能的例子是:

  1. options: {
  2. openingBraceNewLine: {
  3. type: "boolean",
  4. category: "Global",
  5. default: true,
  6. description: "Move open brace for code blocks onto new line."
  7. }
  8. }

defaultOptions

如果您的插件要求Prettier的一些核心选项有不同的默认值,您可以在 defaultOptions 中指定它们:

  1. defaultOptions: {
  2. tabWidth: 4
  3. }

工具函数

来自Prettier core的 util 模块被认为是一个私有API,并不打算被插件消费。作为替代, util-shared 模块为插件提供了以下有限的工具函数集合:

  1. type Quote = '"' | "'";
  2. type SkipOptions = { backwards?: boolean };
  3. function getMaxContinuousCount(str: string, target: string): number;
  4. function getStringWidth(text: string): number;
  5. function getAlignmentSize(value: string, tabWidth: number, startIndex?: number): number;
  6. function getIndentSize(value: string, tabWidth: number): number;
  7. function skip(chars: string | RegExp): (text: string, index: number | false, opts?: SkipOptions) => number | false;
  8. function skipWhitespace(text: string, index: number | false, opts?: SkipOptions): number | false;
  9. function skipSpaces(text: string, index: number | false, opts?: SkipOptions): number | false;
  10. function skipToLineEnd(text: string, index: number | false, opts?: SkipOptions): number | false;
  11. function skipEverythingButNewLine(text: string, index: number | false, opts?: SkipOptions): number | false;
  12. function skipInlineComment(text: string, index: number | false): number | false;
  13. function skipTrailingComment(text: string, index: number | false): number | false;
  14. function skipNewline(text: string, index: number | false, opts?: SkipOptions): number | false;
  15. function hasNewline(text: string, index: number, opts?: SkipOptions): boolean;
  16. function hasNewlineInRange(text: string, start: number, end: number): boolean;
  17. function hasSpaces(text: string, index: number, opts?: SkipOptions): boolean;
  18. function makeString(rawContent: string, enclosingQuote: Quote, unescapeUnnecessaryEscapes?: boolean): string;
  19. function getNextNonSpaceNonCommentCharacterIndex<N>(text: string, node: N, locEnd: (node: N) => number): number | false;
  20. function isNextLineEmptyAfterIndex(text: string, index: number): boolean;
  21. function isNextLineEmpty<N>(text: string, node: N, locEnd: (node: N) => number): boolean;
  22. function isPreviousLineEmpty<N>(text: string, node: N, locStart: (node: N) => number): boolean;

教程

测试插件

由于插件可以使用相对路径进行解析,因此在处理一个插件时可以这样做:

  1. const prettier = require("prettier");
  2. const code = "(add 1 2)";
  3. prettier.format(code, {
  4. parser: "lisp",
  5. plugins: ["."],}
  6. );

上面的代码会加载位于当前工作目录下的一个插件。