先看一眼没有空格的世界
随便打开一篇技术博客, 大概率会看到这样的句子:
我们使用React18的Concurrent Mode来优化首屏渲染, 配合Suspense实现了流式SSR
读起来像什么? 像一堆字被人用胶水粘在了一起. React18的Concurrent 这几个字符连成一片, 眼睛要反复扫才能把中文和英文拆开. 写的人可能觉得无所谓, 但读的人每多停顿一次, 就多消耗一点耐心.
加上空格之后:
我们使用 React 18 的 Concurrent Mode 来优化首屏渲染, 配合 Suspense 实现了流式 SSR.
舒服多了. 中文和英文各自有了呼吸的空间, 一眼扫过去就能分清楚哪些是术语, 哪些是描述.
pangu.js 在干什么
pangu.js 做的事情很纯粹: 在 CJK ? 字符和半角字符 (字母, 数字, 符号) 之间插一个空格. 不多不少, 就一个空格.
名字来自盘古开天辟地 — 在混沌的文本里劈开一道缝隙.
它的规则大致是这样:
| 场景 | 处理前 | 处理后 |
|---|---|---|
| 中文 + 英文 | 中文English | 中文 English |
| 中文 + 数字 | 第3章 | 第 3 章 |
| 中文 + 符号 | 使用(括号) | 使用 (括号) |
| 纯中文 | 这是中文 | 这是中文 |
| 纯英文 | pure English | pure English |
不碰纯中文, 不碰纯英文, 只在两种文字交界的地方动手. 而且它是幂等的 ? — 跑一遍和跑十遍结果相同, 不用担心空格越加越多.
接入 Astro 博客
pangu.js 本身是个字符串处理库, 要让它在 Markdown 构建阶段自动生效, 需要包一层 remark 插件.
npm 上有现成的 remark-pangu, 但上次更新是 2020 年, 和当前 remark 生态的兼容性存疑. 好在自己写一个也就十来行的事:
// src/plugins/remark-pangu.mjs
import { createRequire } from "node:module";
import { visit } from "unist-util-visit";
const require = createRequire(import.meta.url);
const { pangu } = require("pangu");
export default function remarkPangu() {
return (tree) => {
visit(tree, "text", (node) => {
node.value = pangu.spacingText(node.value);
});
};
}有个小坑: pangu 的 npm 包不支持 ESM 直接 import, 需要用 createRequire 走 CJS 方式加载. API 也不是直觉上的 pangu.spacing(), 而是从模块里解构出 pangu 对象, 再调 spacingText().
然后在 astro.config.mjs 里注册:
import remarkPangu from "./src/plugins/remark-pangu.mjs";
export default defineConfig({
markdown: {
remarkPlugins: [remarkAlert, remarkMermaid, remarkPangu],
},
});如果管道里还有其他会把文本节点转成 HTML 的插件, pangu 要排在它们前面. 因为 pangu 只处理纯文本节点, 一旦文本变成了 HTML 节点, 它就碰不到了.
构建时 vs 运行时
pangu.js 其实有浏览器版本, 可以在页面加载后遍历 DOM 节点做处理. 但对于静态博客来说, 构建时处理是更好的选择:
- 零运行时开销, 页面加载后不需要再跑一遍 JS
- 不会出现 “先显示没空格的文本, 闪一下变成有空格的” 这种闪烁
- 构建产物就是最终形态, 所见即所得
唯一的代价是每次构建会多几毫秒 — 对一个静态博客来说完全可以忽略.
和 smartypants 的冲突
Astro 默认开启了 smartypants, 它会把直引号 " 转成排版引号 " ". 这个转换发生在自定义 remark 插件之前, 所以 pangu 拿到的文本里, 引号已经被替换过了.
大多数情况下这不影响 pangu 的工作 — 它只关心 CJK 和半角字符的边界, 不在意引号长什么样. 但如果你的管道里有其他插件需要从文本节点里提取内容放进 HTML 属性 (比如 data-*), 排版引号可能会造成显示异常. 遇到这种情况, 在提取时做一次还原就行:
function normalizeSmartyQuotes(value) {
return value
.replaceAll("\u201C", '"')
.replaceAll("\u201D", '"')
.replaceAll("\u2018", "'")
.replaceAll("\u2019", "'");
}该不该手动加空格
有人会问: 既然有自动工具, 写 Markdown 的时候还需要手动加空格吗?
我的建议是 — 写的时候随意, 不用刻意. pangu 会在构建时兜底, 漏掉的空格自动补上, 多余的也不会变成双空格. 养成加空格的习惯当然好, 但不用为此焦虑, 工具存在的意义就是让你少操心这种事.
NOTE
文章作者: Catluo
文章链接: 中英文之间总差口气? 让 pangu 帮你自动加空格
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 授权协议。
转载请注明来源!