在 Prettier 内置的 Markdown 插件基础上,重写部分逻辑,实现 Markdown 表格的「紧凑布局」替换内置的「宽度对齐布局」。
Prettier 是什么
Prettier 是一个代码格式化工具,主要支持了一些编程/标记语言,比如 JS/TS、JSX/TSX、JSON、CSS,以及前端框架 Vue、Angular 等。Prettier 从系统设计上并不局限于某些语言,通过插件可以支持更多语言。
它自称是一个「有态度的代码格式化器」,这里的态度主要体现在默认约定上。关于代码风格的争论永远不会停止,以 JavaScript 为例,缩进是 Tab or 4 空格 or 2 空格、语句之间是否应该使用分号进行分割、字符串优先使用单引号还是双引号等等。
这些问题由于争论过大,在版本初期不得不成为 Prettier 的选项。但总的来说 Prettier 非常排斥这种选项,这意味着在更多的场景上,它仅提供一种方案。「大家都别吵,我说了算」。
发现问题
Prettier 内置的 Markdown 插件在对表格数据进行格式化时,默认会生成一种「宽度对齐布局」,保证每一列的宽度相同。效果如下:
# 处理前
| Title | Description |
| --- | --- |
| A | B |
| CCC | D |
# 处理后
| Title | Description |
| ----- | ----------- |
| A | B |
| CCC | D |
看起来很完美,美观易读,这样有什么不好吗?
不幸的是,我的项目里有大量的 API 表格,它们通常有三列或者四列,表格平均行数在 15+,其中每一列可能是一个超长(20+ 汉字)或者超短的文本。
如果仅修改表格中某一行的某一个数据,在被 Prettier 格式化之后,有可能整个表格每行都发生了变化(宽度对齐导致了额外的空格出现),这严重「污染」了 git 提交记录,同时 PR 的 reviewer 也难以快速识别出实际修改了哪些内容。
为了避免这个问题,我更倾向于另一种对 Markdown 表格的格式化方案,即去掉宽度对齐的约束,保证每一个表格数据的文本两侧有且仅有一个空格。这样不管怎么修改内容,git 记录与实际修改的文本永远一致。
不妨称它为「紧凑布局」,效果如下:
# 处理前
| Title | Description |
| --- | --- |
| A | B |
| CCC | D |
# 处理后
| Title | Description |
| --- | --- |
| A | B |
| CCC | D |
简单了解 Prettier 插件
一个 Prettier 插件通常是一个导出的 js 模块,它可以包含以下参数:
- languages:要处理的语言,比如 md
- parsers:解析器,输入文件源码字符串,输出 AST
- printers:打印机,输入 AST,输出满足 Prettier 约定格式的数据中间态
- options:本插件自身对外提供的配置
- defaultOptions:覆盖 Prettier 的一些选项
这里有一个 ASTExplorer 网站可以查看一些常见语言的源码与 AST 对比。
Prettier 内置的 Markdown 插件使用了 remark-parse 作为 parser,输出格式为 mdast。
动手编写插件
我们要解决的问题是输出另一种布局的表格,仅涉及到 printers 的部分,因此其余模块可直接使用 Prettier 内置 Markdown 插件上的方法,最终整个插件的框架像这样:
import linguistLanguages from 'linguist-languages';
import { printers as MarkdownPrinter, parsers as MarkdownParsers } from 'prettier/plugins/markdown';
export const languages = [
{
...linguistLanguages.Markdown,
parsers: ['markdown'], // 使用内置的 markdown parser
}
];
export const parsers = {
markdown: MarkdownParsers.markdown
};
export const printers = {
mdast: {
...MarkdownPrinter.mdast,
print: pluginPrint, // 仅对打印机方法进行重写
}
};
下面考虑 pluginPrint 方法的编写,它应该是一个递归处理 Markdown AST 的函数。
与上面的代码逻辑类似,对于非 Table 类型的节点输入,直接使用内置插件的打印机方法即可,我们只需要对 Table 类型节点的输出方法进行重写。
function pluginPrint(path, options, print) {
const node = path.getValue(); // 通过 path 获取到对应的节点
if (node.type == "table") {
// 对于 Table 类型节点,使用自定义的打印机方法
return printTable(path, options, print);
}
return MarkdownPrinter.mdast.print(path, options, print);
}
Markdown 中的 Table 格式为:
| Title1 | Title2 | Title3 |
| ------ | ------ | ------ |
| val1 | val2 | val3 |
| val1 | val2 | val3 |
...
设想中表格「紧凑布局」的规则如下:
- 对于第二行数据,需要将每一个元素中的 - 的数量格式化为 3;
- 对于其他行数据,应该保证文本与两侧的分割符之间有且仅有一个空格;
完整的 printTable 方法如下:
function printTable(path, options, print) {
// 提取整个表格中所有行的元素
const contents = path.map(
() =>
path.map(() => {
const text = print().flat(Infinity).join("");
return { text };
}, "children"),
"children"
);
const alignedTable = printTableContents();
return [alignedTable];
function printTableContents() {
// 依次格式化第一行、第二行、其他行
const parts = [printRow(contents[0]), printAlign()];
if (contents.length > 1) {
for (let i = 1; i < contents.length - 1; i++) {
parts.push(printRow(contents[i]));
}
parts.push(printRow(contents[contents.length - 1], true)); // 标记结束,末尾不再添加换行符
}
return parts;
}
function printAlign() {
// 对于第二行,忽略所有的数据,一律使用 --- 进行替换
const align = contents[0].map(() => {
return `---`;
});
return `| ${align.join(" | ")} |\n`;
}
function printRow(rowContents, end = false) {
const columns = rowContents.map(({ text }) => {
return ` ${text} `;
});
return end ? `|${columns.join("|")}|` : `|${columns.join("|")}|\n`;
}
}
插件运行流程
- 1、编写代码后,执行
git commit
操作 - 2、提交操作会触发 husky 插件的 pre-commit 钩子
- 3、pre-commit 钩子中运行 nano-staged 插件(依赖更简洁轻量的 lint-staged)
- 4、nano-staged 插件执行
prettier --write
命令,对所有提交的文件进行格式化
参考链接
1、Prettier 源码中的 printTable 方法:github.com/prettier/prettier
2、Prettier 文档:prettier.io