如何优雅地升级ruby标签:正则表达式指南
在支持HTML的环境(包括不少现代的Markdown编辑器1)使用<ruby>
标签为汉字等表意文字添加注音(如拼音或振假名),是一个非常实用的功能。然而,一个常见的痛点是,在不支持该标签的纯文本渲染环境下,内容的可读性会大大降低。
例如,一个标准的<ruby>
标签:
<ruby>大学<rt>だいがく</rt></ruby>
在纯文本的渲染环境中会显示为大学だいがく
,这显然不够理想。2
优雅降级的方案:<rp>
标签
HTML标准为此提供了一个优雅的解决方案:<rp>
(ruby parenthesis)标签。它的作用是包裹“备用字符”(通常是括号),这些字符只在不支持<ruby>
的环境中显示。
理想的格式应该是这样:
<ruby>大学<rp>(</rp><rt>だいがく</rt><rp>)</rp></ruby>
这样可以实现两全其美的效果:
- 在支持的环境3:正常显示注音样式,括号被隐藏。
- 在不支持的环境(纯文本):显示为
大学(だいがく)
,未尝有损可读性。
那么,如何将手上的文字(譬如说电子笔记库)中所有旧格式的<ruby>
标签批量升级到这种新格式呢?自然会想到的是全局搜索与正则表达式。
第一版方案:简单替换
(警告:请不要急着一行行跟着以下方案批量替换,建议先看看下文再做决定。另外,任何时候都请记得做好备份,再批量修改。)
这里用可以直接管理包括.markdown
文件在内的笔记文档的VS Code来举例。我们最直接的想法是找到所有的注音标签,然后给它们加上括号。
- 查找:
<rt>(.*?)<\/rt>
- 替换:
<rp>(</rp><rt>$1</rt><rp>)</rp>
这个方案可以处理最简单的情况。但很快我们就会发现第一个问题。
边界情况:一个词组内的多个注音
当遇到像日语“地じ震しん”这样的词,其源代码可能是:
<ruby>地<rt>じ</rt>震<rt>しん</rt></ruby>
上面的简单方案能正确工作吗?是的。它会独立地将<rt>じ</rt>
替换为<rp>(</rp><rt>じ</rt><rp>)</rp>
,并将<rt>しん</rt>
替换为<rp>(</rp><rt>しん</rt><rp>)</rp>
,最终得到正确的结果:
<ruby>地<rp>(</rp><rt>じ</rt><rp>)</rp>震<rp>(</rp><rt>しん</rt><rp>)</rp></ruby>
到目前为止,一切似乎都很顺利。但一个更隐蔽的“定时炸弹”埋藏在这个方案中。
第二版方案:解决重复执行的bug
问题在于,如果我们不小心再次运行4这个替换脚本,会发生什么?
它会找到已经修复好的<rp>(</rp><rt>じ</rt><rp>)</rp>
中的<rt>じ</rt>
,然后再次为它套上括号,导致灾难性的结果:
<rp>(</rp><rp>(</rp><rt>じ</rt><rp>)</rp><rp>)</rp>
这破坏了幂等性5原则。一个健壮的脚本必须能够安全地重复执行而不产生副作用。
因此,我们需要一个最终的、更智能的查找逻辑:只查找那些尚未被<rp>(</rp>
和<rp>)</rp>
包裹的<rt>
标签。
终极解决方案:使用反向否定查找
正则表达式的“反向否定查找”(negative lookbehind),写作(?<!...)
,是解决这个问题的完美工具。它允许我们匹配一个模式,但前提是它的前面不是某个特定的字符串。
最终的查找与替换规则
Ctrl+Shift+H
或 Cmd+Shift+H
)。.*
图标)。(?<!<rp>\(<\/rp>)<rt>(.*?)<\/rt>
<rp>(</rp><rt>$1</rt><rp>)</rp>
工作原理解析
- 查找:
(?<!<rp>\(<\/rp>)<rt>(.*?)<\/rt>
(?<!<rp>\(<\/rp>)
:这是关键。它是一个反向否定查找,意思是:“从当前位置往前看,前面的文本不能是<rp>(</rp>
”。注意,为了匹配字面上的括号(
,我们用\(
对其进行了转义。<rt>(.*?)<\/rt>
:这部分和之前一样,用于匹配<rt>
标签及其内容。
这个查找表达式现在变得非常智能:
- 它能匹配
<ruby>地<rt>じ</rt>...
中的<rt>じ</rt>
,因为它的前面是字符地
,不是<rp>(</rp>
。 - 它不能匹配
<ruby>地<rp>(</rp><rt>じ</rt>...
中的<rt>じ</rt>
,因为它前面的文本正好是我们排除了的<rp>(</rp>
。
这样,无论对文件执行多少次这个替换操作,它都只会影响那些尚未被修复的旧标签,而对已经符合新格式的标签秋毫无犯,完美地保证了操作的安全性和幂等性6。
通过这个逐步优化的过程,我们最终得到了一个强大、安全且一劳永逸的解决方案,可以放心地用它来优化整个笔记库。
尾注
<ruby>
标签并不是标准的Markdown语法——从“<
”与“>
”就能看出它的XML色彩,但是不少现代的Markdown编辑器——例如Obsidian之类——都会尽量支持HTML语法,<ruby>
标签也就包含在内了。 ↩︎<rp>
的<ruby>
标签了。 ↩︎