Skip to content

写个插件(一)Prettier

Published:

在 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 模块,它可以包含以下参数:

这里有一个 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   |
...

设想中表格「紧凑布局」的规则如下:

完整的 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、Prettier 源码中的 printTable 方法:github.com/prettier/prettier

2、Prettier 文档:prettier.io