pangu.hs

Posted on December 15, 2025
Tags:
pangu.js 是给兼有中英文字符的文本中插入空格的工具。

我觉得这种工具很有用。这个网站曾经使用一个简化版本 。 不过 js 版本的工具会导致整个网页加载很慢,如果能在编译的时候运行就会变得更好。

最简单的办法其实是在 hakyll 生成 html 之后运行一下 pangu。 有人会想把 pangu 也变成一个 pandoc filter。在 pandoc AST 上判断中英文字符连接情况有点复杂,不过没有你想的那么复杂。 pangu 在每个段落上的工作是独立的,不会需要知道别的段落的信息,在 pandoc AST 当中,每个段落都是一个 Block,constructor 是 Para [Inline]。而 Inline 包含了所有需要考虑的其他元素。 所以只需要在一个 Inline list 当中检查每个 element 内部是否需要空格,以及 list 两个元素之间是否需要空格。我们并不关心遍历 AST 的时候上一个 leaf 是什么。

1 Python → Haskell

在元旦假期终于开始写。计划是从 python 版本迁移过来,因为 vinta 实现 pangu 的那些语言里我只看得懂 python。

我发现貌似作者自己也没总结出什么中英文排版加空格、换全角或中文标点的规则(也可能是因为 ww 和大陆习惯也不同),而且正则表达式里漏洞很多。

pangu.hs 需要根据一些规则来替换字符和删减空格,所以应该把这些规则写下来然后弄一个存放规则的 list,程序根据这个 list 里的规则来调整文本,同时用户见到自己不想要的 rule 也可以直接 comment 它。

我没有在 Haskell 中使用正则表达式的经验,于是求助 Gemini 和 ChatGPT。相比于 regex 他们更推荐用 megaparsec。 如果要 find and replace,就需要用streamEdit. 这函数会找所有不重叠的 section,完成替换任务。
type Parser = Parsec Void Text
type Rule = Parser Text
type RuleSet = [Rule]

applyUntilFixed :: Rule -> Text -> Text
applyUntilFixed rule =
  fix
    ( \loop current ->
        let next = streamEdit (try rule) id current
         in if next == current then next else loop next
    )

applyRulesRecursively :: RuleSet -> Text -> Text
applyRulesRecursively rules input = 
  foldl (flip applyUntilFixed) input rules

applyRules :: RuleSet -> Text -> Text
applyRules rules input = foldl (flip applyOnce) input rules
  where
    applyOnce rule = streamEdit (try rule) id
然后即可这样选择要启用哪些Rule
recursiveRules :: RuleSet
recursiveRules =
  [ fullwidthCJKsymCJK,
    fullwidthCJKsym
  ]

onepassRules :: RuleSet
onepassRules =
  [ dotsCJK,
    fixCJKcolAN,
    cjkquote,
    quoteCJK,
    fixQuote,
    cjkpossessivequote,
    -- singlequoteCJK,
    fixPossessivequote,
    hashANSCJKhash,
    cjkhash,
    -- hashcjk,
    anscjk,
    cjkans,
    empty -- a dummy rule
  ]

pangu :: Text -> Text
pangu input = 
  applyRules onepassRules $ applyRulesRecursively recursiveRules input

完整代码在 https://github.com/sxlxc/pangu.hs 可以看到。 功能并不和 pangu.py 完全一致

2 Pandoc filter

现在想要在 hakyll 里使用这个包。 这部分很简单,只要实现上文说的想法就好了。 pandoc-types 提供了很多方便写 filter 的函数,我们实现好处理 [Inline] 的 filter 就好了。
panguFilter :: Pandoc -> Pandoc
panguFilter = walk transformBlocks
  where
    transformBlocks :: Block -> Block
    transformBlocks (Para inlines) = Para (panguInlines inlines)
    transformBlocks x = x

完整的实现可以看这个 commit

3 CSS text-autospace

一月九号才在知乎看到 CSS 也支持类似的功能: text-autospace

2025 年九月份就已经在主流浏览器上可用。