26

[译] Prettier 插件开发文档

 3 years ago
source link: https://mp.weixin.qq.com/s/GBVxrt9MXBrsB1KXBzk52g
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文翻译自: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参数:

  prettier --write main.foo --plugin-search-dir=./dir-with-plugins --plugin=./foo-plugin

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

或者API的方式,通过设置plugins 及 pluginSearchDirs的方式:

prettier.format("code",{

parser:"foo",

pluginSearchDirs:["./dir-with-plugins"],

plugins:["./foo-plugin"],

});

Prettier期望每一个pluginSearchDirs 都包含 node_modules 子目录,这样@prettier/plugin- , @ /prettier-plugin-* 以及 prettier-plugin-*会在node_modules文件夹中被查询。比如,pluginSearchDirs 可以是你的项目目录或者是全局的npm模块的位置。

如果 --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。

exportconst 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)。

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

exportconst printers ={

"dance-ast":{

print,

embed,

preprocess,

insertPragma,

canAttachComment,

isBlockComment,

printComment,

handleComments:{

ownLine,

endOfLine,

remaining,

},

},

};

打印过程

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

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 函数中,该函数的结构是:

functionprint(

// Path to the AST node to print

path:FastPath,

options:object,

// Recursively print a child node

print:(path:FastPath)=>Doc

):Doc;

print 函数被传入一个 path 对象,可以通过path.getValue()来获取AST中的节点信息。还被传递了一个持久化的 options 对象(其中包含全局的options,插件可能会改变该该配置项),以及一个 print 用来做递归调用。一个基本的print 函数可能像下面的代码所示:

const{ builders }=require("prettier").doc;

functionprint(path, options,print){

const node = path.getValue();

if(Array.isArray(node)){

return builders.concat(path.map(print));

}

return node.value;

}

可以看下 prettier-python's printer 来了解一些可能的例子。

(可选) 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"});

}

returnfalse;

}

(可选)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[ ].locStart/locEnd 函数来检查每一个注释应该依附的AST节点。然后在打印的过程中注释被附加到这些对应的节点上,修改AST,并从AST根目录中删除Comments属性。*Comment函数被用来调整Prettier的算法。一旦注释被附加到了AST上,Prettier会自动调用 printComment(path, options): Doc 函数,并将返回的doc插入到(期望的)正确的位置上。

(可选)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);

returntrue;

}

returnfalse;

}

带有注释的节点应具有包含注释数组的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:["."],}

);

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK