优化中文 Markdown 软换行显示效果
涛叔Markdown 支持所谓的软换行(soft break)。连续输入的两个换行符表示新起一段,段落之间会有一个空行。如果只输入一个换行符,那就表示软换行。说它软是因为它只会影响到 Markdown 源文,转成 HTML 后不会变成<br>
,而是对应空格。这在英文环境中没有问题,但在中文环境中却很不方便。今天就分享如何优化中文 Markdown 中软换行的显示效果。
感谢读者 mjoycarry 分享,其实 pandoc 内置 east_asian_line_breaks1 扩展支持处理中文软换行效果。建议大家直接使用该扩展。我的博客系统已经改用这个扩展了。但本文依然可以用作是 lua-filter 的入门阅读材料😄
我的博客使用 pandoc 转换 Markdown 文件。pandoc 支持通过 lua-filter 进行扩展。我之前就分享过通过 lua-filter 实现统计文章字数和修复相对引用。今天继续使用 lua-filter 来优化转换行的显示效果。
先给出具体的例子,让大家直观地感受一下这个问题。比如我们有如下 Markdown 原文:
hello
world
世界 你好
它们转成 HTML 会显示为:
hello world
世界 你好
注意世界跟你好之间有个空格,hello 跟 world 之间同样有空格。这个空格对英语来说是必须的,但对汉语来说却很多余。
以前没有办法,写中文内容的时候尽量不换行。一段写成一行,无论是移动还是修改都不方便。而且这种折衷的办法还有一个缺点,那就是在不同的屏幕上展示的效果不一样。如果屏幕比较窄也还可以,如果是很宽的屏幕,那直就是一段一行了。
最近我在翻译Go语言垃圾回收指南的时候实在是忍无可忍,因为每一段的内容都很长😂所以下决心彻底解决这个问题。
解决思路也很简单,就是在生成 HTML 之前扫描所有的软换行,如果换行前后都是汉字就把它强制删除。这个功能可以通过 lua-filter 轻松实现。
考虑到判断是否为汉字略微麻烦,而判断是否为 ASCII 字符比较简单。所以我这边要求,只有在软换行两边都不是 ASCII 字符的时候才会删除。
在 lua 中判断 ASCII 字符的函数如下:
function is_ascii(char)
local ascii_code = string.byte(char)
return ascii_code >= 0 and ascii_code <= 127
end
其实 lua 也支持比较字符,比如char >= '!' and char <= '~'
也可以判断。但上面的是 ChatGPT 给出的版本,又不是不能用😄
接下来就是要找到 Markdown 中的软换行。查一下官方文档发现有一个 SoftBreak 对象。但说实话我是后来才看到的😂因为之前不知道有它,只能通过调试的办法来看。
最简单的 lua-filter 结构如下:
return {
{
Para = function (para)
print(para)
return para
end,
}
}
代码需要返回一个 table 对象。在 lua 中 table 对象即可以用作数组,也可以用作字典。大家有个概念就行,我也不太清楚🤪列表的每一个元素都是一个 filter,它的键是对应的 Markdown 语法对象,值是一个回调函数。
pandoc 会为每个段落生成一个Para
对象,然后会依次调用 lua filter 的回调函数加以处理。在上面的例子,我是简单打印出Para
的结构。
运行 lua filter 也非常简单:
pandoc --lua-filter filter.lua demo.md
假如 Markdown 的内容如下:
n 好
那么 pandoc 输出的内容就是:
Para [Str "n",SoftBreak,Str "\22909"]
<p>n 好</p>
第一行是我们在 lua filter 中输出的内容,第二行是对应的 HTML 输出。
从输出结果来看,这个段落里三个部分,中间是SoftBreak
,另外两个是Str
。最后的Str
感觉像是乱码,应该就是原文中的「你」。
接下来就有思路了。我们应该想办法遍历Para
的每个部分,如果发现是SoftBreak
就再看看前后的Str
是不是中文,如果都是中文那么就想办法把当前的SoftBreak
替换掉。
在 lua 中遍历对象使用 for 循环:
for k, v in ipairs(para) do
print(k, v)
end
但试了一个发现得到的结果不符合预期,什么都没有输出😂没辙了,只能先看看Para
的官方文档。原来实际的子节点保存在content
里面,所以正确的遍历姿势为:
local cs = para.content
for k, v in ipairs(cs) do
print(k, v)
end
然后就是在遇到SoftBreak
时检查前后的字符类型:
if v.t == 'SoftBreak' then
local p = cs[k-1].text
local n = cs[k+1].text
if p ~= nil and n ~= nil then
if (not is_ascii(p:sub(-1))) and (not is_ascii(n:sub(1,1))) then
-- to do
end
end
end
lua 的 for 循环不支持 continue 也是醉了,只能多套几层判断。因为软换行前后可能是 Code
等类型,直接读取text
属性可能返回nil
,所以需要事先检查。不然可能报错。
另外在 lua 中,字符串可以调用sub
函数提取子串,sub(-1)
是提取最后一个字符,而sub(1,1)
是提取最前面的字符。
最后一步就是想办法删除SoftBreak
。理论上我们可以直接修改Para
的content
列表。但在遍历过程中直接修改会引发意想不到的问题2,但如果复制一份再按条件过虑,又可能引起性能问题。所以最简单的办法就是替换!
我们直接把SoftBreak
替换成一个空的字符串,对应代码为:
para.content[k] = pandoc.Str("")
这样一来,最终输出结果就变成:
hello world
世界你好
是不是很方便😄现在就可以尽情地换行,随心所欲地换行。嗯,是自由的味道。
https://pandoc.org/MANUAL.html#extension-east_asian_line_breaks↩︎
lua 支持这种操作,删除后所有元素的下标会重新排列,处理起来比较麻烦。↩︎