Prettier 如何对齐含 CJK 字符的 Markdown 表格从宽度计算到源码实现【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier导读Markdown 表格在源码中如何书写、格式化后如何对齐是 Prettier 处理 Markdown 时的经典问题。中文、日文、韩文等 CJK 字符是典型的“全角/宽字符”一个字符在终端和渲染器中通常占两个 ASCII 字符的宽度若 Prettier 仍按1 字符 1 宽度的朴素方式计算表格列宽就会错位、对不齐。本篇以仓库中的测试用例 tests/format/markdown/table/cjk.md 为起点结合 table.js 与 get-string-width.js 的源码实现讲清 Prettier 对齐 CJK 表格的完整原理、边界规则与验证方法。读完你既能理解“为什么 第一欄 后面有 4 个空格”也能在遇到其他全角/emoji 场景时举一反三。一个最小的 CJK 表格测试用例仓库中的 tests/format/markdown/table/cjk.md 是一份极简的输入样例全文只有三行| abc | def | ghi | | --- | --- | --- | | 第一欄 | 第二欄 | 第三欄 |它由表头行、分隔行delimiter row和一行数据组成数据行的内容是繁体中文“第一欄 / 第二欄 / 第三欄”每格恰好 3 个 CJK 字符。这个测试由 format.test.js 驱动以proseWrap: always选项运行runFormatTest(import.meta, [markdown], { proseWrap: always });对应的期望输出记录在快照文件 tests/format/markdown/table/snapshots/format.test.js.snap 中| abc | def | ghi | | ------ | ------ | ------ | | 第一欄 | 第二欄 | 第三欄 |请观察两个关键细节数据行的第一欄没有补空格但表头与分隔行被拉宽abc后多了 4 个空格---变成了------这样三列在“视觉宽度”上完全对齐——每一列含两侧空格的总显示宽度均为 10第一欄3 个全角字符 × 2 左右各 2 个空格。等宽代码列与显示列不同若按纯字符数计算第一欄只有 3 个字符abc也是 3 个字符根本不需要扩展之所以拉宽是因为 Prettier 计算的是“显示宽度”而非“字符个数”。这正是本测试用例要锁定回归的核心行为Markdown 表格的对齐必须以每个单元格的显示宽度为准CJK 全角字符按 2 个宽度计。表格对齐的源码实现先量宽度再画空格Prettier 的 Markdown 打印器在 src/language-markdown/print/table.js 中实现了printTable函数其工作流程可以拆成三步。第一步把每个单元格渲染成纯文本并测量宽度在printTable中Prettier 先遍历所有行和单元格将每个单元格的 doc 用printDocToString渲染成纯文本并调用getStringWidth(text)计算其显示宽度const text printDocToString(print(), { ...options, printWidth: Number.POSITIVE_INFINITY, endOfLine: lf, }).formatted; const width getStringWidth(text);注意这里把printWidth设为Number.POSITIVE_INFINITY即单元格内部不因行宽而被换行或压缩保证测出的宽度是单元格内容的完整宽度。第二步统计每列的最大宽度每一列的宽度取该列所有单元格的最大值且有一个最小宽度 3对应分隔行的---、:--、--:或:-:等形式columnMaxWidths[columnIndex] Math.max( columnMaxWidths[columnIndex] ?? 3, // minimum width 3 (---, :--, :-:, --:) width, );第三步按对齐方式填充空格printRow根据列对齐方式node.align[columnIndex]取自分隔行计算左右两侧各补多少空格const spaces columnMaxWidths[columnIndex] - width; let before 0; if (align right) { before spaces; } else if (align center) { before Math.floor(spaces / 2); } const after spaces - before; return ${ .repeat(before)}${text}${ .repeat(after)};分隔行则由printAlign根据对齐符号生成对应长度的-与:组合。最终每行以| ${columns.join( | )} |拼出。佐证同一目录下的对齐与 emoji 测试cjk.md 并非孤例同一目录下的 align.md|a|b|c|、:--|:-:|--:的左/中/右对齐、emoji.md、issue-15572.md✔/✘全角符号、table.md含中文表头的学号/姓名/分数大表等都在验证同一套宽度计算逻辑在不同字符类型下的行为并全部固化在 format.test.js.snap 中。其中table.md快照里中文表格的期望输出为| 学号 | 姓名 | 分数 | | ---- | ---- | ---- | | 小明 | 男 | 75 |注意男与75的对齐方式男是全角字符占 2 宽度75是两个半角字符也占 2 宽度因此它们能恰好对齐——这正是“以显示宽度计算”才能得到的效果。宽度计算的底层getStringWidth 的字符分类规则对齐效果的源头在 src/utilities/get-string-width.js 的getStringWidth。它的核心规则是纯 ASCII 快速路径若文本不包含任何[^\x20-\x7F]字符直接返回text.length避免无谓的正则与分配开销。emoji 先行处理用emoji-regex匹配出所有 emoji通过narrow-emojis判断是否为窄字符宽 emoji 计 2、窄 emoji 计 1并从文本中剔除后再逐字符统计。控制字符忽略0x1F以下及0x7F–0x9F的控制字符不计宽度。零宽字符忽略\p{Nonspacing_Mark}与\p{Enclosing_Mark}组合用附加符号、变体选择符等不产生水平宽度不计数。全角/宽字符计 2核心一行是width isFullwidth(codePoint) || isWide(codePoint) ? 2 : 1;其中isFullwidth与isWide来自get-east-asian-width依赖见 package.json 中声明的get-east-asian-width: 1.7.0。中文、日文假名、韩文谚文等被归为 East Asian Wide/Fullwidth 的字符宽度为 2当字符为“歧义宽度”Ambiguous时Prettier 始终按窄字符 1 计这一点与 string-width 库的策略一致源码注释明确写着“always treat ambiguous width characters as having narrow width”。回看 cjk.md 的用例第一欄中的每个汉字都属于全角字符getStringWidth(第一欄)的结果是 6而abc是 3因此列宽被拉宽到 6abc之后便补出 3 个空格| abc |中abc到|之间共 4 个空格含固定间隔 1 个最终形成视觉对齐。特殊模式proseWrap 与紧凑表格printTable中还有一段值得注意的分支逻辑const alignedTable printTableContents(/* isCompact */ false); if (options.proseWrap ! never) { return [breakParent, alignedTable]; } // Only if the --prose-wrap never is set and it exceeds the print width. const compactTable printTableContents(/* isCompact */ true); return [breakParent, group(ifBreak(compactTable, alignedTable))];默认及proseWrap: always即本测试场景下表格总是输出为“对齐模式”aligned这也是 cjk.md 快照所见的行为仅当用户显式设置--prose-wrap never且紧凑模式仍超出打印宽度时才会退化为“紧凑模式”compact单元格间只保留单个空格、分隔行缩短为---样式否则仍输出对齐表格。紧凑模式同样复用printRow/printAlign只是分隔行的中间部分固定为-而非按列宽重复-const middle isCompact ? - : -.repeat(width - 2);如何验证与复现仓库对 markdown 表格的所有测试统一由 tests/format/markdown/table/format.test.js 运行快照结果集中在 format.test.js.snap。若你修改了表格相关源码或想验证某个新字符类型如新的全角符号、组合 emoji的对齐行为可以在tests/format/markdown/table/下新建或修改*.md输入样例格式仿照 cjk.md第一行| abc | def | ghi |这种未规范化的书写即可运行 markdown 表格目录的测试并更新快照例如yarn jest tests/format/markdown/table --updateSnapshot检查快照中每列的总显示宽度是否一致对于含全角字符的列宽度按每字符 2 计算这也是 cjk.md 测试用例存在的意义——防止未来对宽度计算逻辑的改动破坏中文表格对齐。小结以 cjk.md 为代表的测试用例锁定了 Prettier 对 Markdown 表格“按显示宽度对齐”的核心行为CJK 全角字符宽度为 2emoji 与全角符号同理对齐实现位于 src/language-markdown/print/table.js先测量每格宽度、统计列最大宽度、再按对齐方式补空格宽度计算依赖 src/utilities/get-string-width.js通过get-east-asian-width判断全角/宽字符并忽略控制字符与零宽附加符号歧义宽度按窄计proseWrap: never且超出打印宽度时才使用紧凑表格模式其余情况一律输出对齐表格。理解了这条从“字符宽度分类”到“列宽计算”再到“空格填充”的完整链路你不仅能解释第一欄后 4 个空格的来历也能自行推断任何新字符新全角符号、区域旗帜 emoji、变体选择符组合等在 Prettier 表格中的对齐表现。【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考