4 分钟阅读
-- 次浏览
中英文之间总差口气? 让 pangu 帮你自动加空格

先看一眼没有空格的世界

随便打开一篇技术博客, 大概率会看到这样的句子:

我们使用React18的Concurrent Mode来优化首屏渲染, 配合Suspense实现了流式SSR

读起来像什么? 像一堆字被人用胶水粘在了一起. React18的Concurrent 这几个字符连成一片, 眼睛要反复扫才能把中文和英文拆开. 写的人可能觉得无所谓, 但读的人每多停顿一次, 就多消耗一点耐心.

加上空格之后:

我们使用 React 18 的 Concurrent Mode 来优化首屏渲染, 配合 Suspense 实现了流式 SSR.

舒服多了. 中文和英文各自有了呼吸的空间, 一眼扫过去就能分清楚哪些是术语, 哪些是描述.

pangu.js 在干什么

pangu.js 做的事情很纯粹: 在 CJK ? 字符和半角字符 (字母, 数字, 符号) 之间插一个空格. 不多不少, 就一个空格.

名字来自盘古开天辟地 — 在混沌的文本里劈开一道缝隙.

它的规则大致是这样:

场景处理前处理后
中文 + 英文中文English中文 English
中文 + 数字第3章第 3 章
中文 + 符号使用(括号)使用 (括号)
纯中文这是中文这是中文
纯英文pure Englishpure 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 授权协议。
转载请注明来源!