RIME 脚本食用方法举隅:以输入苏州码为例

Category 碎碎念

RIME 或称中州韵输入法,另一个更风行的名字是小狼毫输入法,当然这并不准确,因为只有 Windows 平台上的 RIME 才称为小狼毫。不过也无妨,作为一款开源输入法,RIME 可以部署在 Windows、MacOS、Linux、Android 等多个平台上,实现大同小异的功能,大部分配置文件也都通用,用不着很仔细区分。

我很早就听说了 RIME,作为开源输入法,用户可以自己构建码表、输入方案,因而一问世就很受方言、汉字、打字爱好者的青睐。方言爱好者用 RIME 实现各种方言输入方案,汉字爱好者用来输入扩展区汉字,打字爱好者则是用来改进各种音码、形码方案,不一而足。

但早年间 RIME 的 bug 比较多,入门的门槛高,一直只在小圈子内流行。经过数次版本迭代后,现而今的 RIME 可以说是非常好用,哪怕是仅追求不窃取用户资料的「圈外人」也可以轻松体验。

网络上关于配置 RIME 的入门教程很多,我不在此赘言。这篇文章主要谈谈如何用 RIME 的 Lua 脚本实现一些高级输入,也是我最近折腾 RIME 的一些心得。

苏州码

苏州码也称苏州码子、花码等,是中国传统的记数符号,对照如下表所示:

0 1 2 3 4 5 6 7 8 9

在表示数字时,苏州码用一个符号表示一位数,从左向右书写,这与阿拉伯数字的计数方式相同。

苏州码还有一条规则,当「〡」「〢」「〣」中任意两者相邻时,首个用竖式,次一个用横式,再次一个又用回竖式,如此循环。仅「〡」「〢」「〣」三个数字具有横式苏州码,其所谓横式就是汉字的「一」「二」「三」,可以想知这是为了避免「〡」「〢」粘连成「〣」。

知道以上的规则就会识读苏州码了,例如:

  • 18590 ➔ 〡〨〥〩〇
  • 51203 ➔ 〥〡二〇〣
  • 72132 ➔ 〧〢一〣二

再来看几个加上单位的具体例子:

癸亥年更流部

实际使用时,还会将最大数位用汉字着于最高位数字下方,数量单位着于个位数字下方。可以看出,苏州码完美兼容中文直排的书写传统,阅读时从左至右逐列读出即可。在遇到大数时,这种能直接呼读的优势更为明显,例如

〡〨〥〣〤〦〥
万   块

可以直接读「一万八千五百三十四块六五」。由于排版不便,苏州码在互联网时代已经难觅踪迹了,但似乎在民间手写的场合还有孑余。

手写的苏州码

RIME

言归正传,一个个复制输入苏州码太不现实,那么如何优雅地用 RIME 输入苏州码呢?

挂载一个输入方案

从头构建输入方案太过复杂,我们可以通过修改现成的输入方案实现我们的想法。在 RIME 的官方仓库中就能找到很多输入方案,可以下载一个最熟悉的。

以 Windows 平台为例,正确安装 RIME 后,在右下角的任务栏中理应出现 RIME 图标。

  1. 右击 RIME 图标,选择 用户文件夹,将下载的输入方案移入该文件夹中,文件夹中应具有许多 .yaml 文件;
  2. 右击 RIME 图标,选择 重新部署
  3. 再右击 RIME 图标,选择 输入法设定,就能找到下载的输入方案了。

深入输入方案

输入方案最基本的两个文件是 *.schema.yaml*.dict.yaml

  • *.schema.yaml 用于实现输入功能,例如模糊音、中英文混打等功能都通过它实现;
  • *.dict.yaml 是码表文件,用户一般不需要动它。

打开输入方案的 *.schema.yaml,可以看到里面有一个名为 translators 的模块,该模块决定了打字时击入的编码如何转化为候选词。

我们要通过 Lua 脚本将输入的数字转为苏州码,在该模块下添加一项 lua_translator@number_translatorlua_translator 告诉 RIME 我们要使用 Lua 生成候选词,number_translator 是函数名称。我修改后的 translators 模块为

translators:
  - punct_translator
  - table_translator@custom_phrase
  - reverse_lookup_translator
  - script_translator
  - lua_translator@number_translator

 Warning YAML 文件对缩进敏感,一定要检查缩进是否正确。

Lua 脚本

接着在用户文件夹,即 *.schema.yaml 所在文件夹中新建一个名为 rime.lua 的文件,写入

number_translator = require("number")

上述代码将 number.lua 脚本注册为 number_translator 函数。rime.lua 文件管理着接入 RIME 的所有 Lua 脚本,将相应脚本注释去,其功能就被禁用。

在用户文件夹中新建名为 lua 的文件夹,所有 Lua 脚本就存放在该目录下,在该目录中新建一个 number.lua 文件。如果仅列举关键文件,文件结构应为

RIME
├─*.dict.yaml
├─*.schema.yaml
├─lua
│  └─number.lua
└─rime.lua

number.lua 写入将数字字符串转为苏州码的核心函数:

local function contains(array, element)
    for _, value in pairs(array) do
        if value == element then
            return true
        end
    end
    return false
end

local function num2suzhou(num)
    local suzhou = {"〇", "〡", "〢", "〣", "〤", "〥", "〦", "〧", "〨", "〩"}
    local horizontalSuzhou = {"一", "二", "三"}
    local oneTwoThree = {table.unpack(suzhou, 2, 4)}  -- {"〡", "〢", "〣"}
    local result = ""
    if num == nil then return "" end
    -- 遍历整个字符串
    for pos = 1, string.len(num) do
        -- 将每个字符转为数字
        digit = tonumber(string.sub(num, pos, pos))
        if pos > 1 then
            -- 数字若为 {"〡", "〢", "〣"}
            if digit > 0 and digit < 4 then
                -- 且前一个字符也为 {"〡", "〢", "〣"}
                -- `-3` 即取末一个汉字,utf-8 中一个汉字 3 字节
                if contains(oneTwoThree, string.sub(result, -3)) then
                    -- 就使用横式的 {"一", "二", "三"}
                    result = result .. horizontalSuzhou[digit]
                    goto continue
                end
            end
        end
        -- 其他情况或其他数字都使用竖式
        result = result .. suzhou[digit + 1]
        ::continue::
    end
    return result
end

num2suzhou() 实现了前文提到的数字与苏州码映射和横竖式转换两个规则,接下来要将封装成 RIME 的接口:

-- 若输入数字带有小数,将其切分为整数、小数点、小数 3 个部分
local function splitNumPart(str)
    local part = {}
    part.int, part.dot, part.dec = string.match(str, "^(%d*)(%.?)(%d*)")
    return part
end

-- 字符串处理流程
function numberTranslatorFunc(num)
    -- 切分小数
    local numberPart = splitNumPart(num)
    local result = {}
    -- 整数和小数部分分别用 num2suzhou() 转换,再将整数、小数点、小数三者连起来
    -- 最后将结果存入 result
    table.insert(
        result,
        {
            -- 候选结果
            num2suzhou(numberPart.int) .. numberPart.dot .. num2suzhou(numberPart.dec),
            -- 候选备注
            "〔蘇州碼〕"
        }
    )
    return result
end

-- 接入 RIME 引擎
function translator(input, seg)
    local str, num, numberPart
    -- 匹配 "S + 数字 + 小数点(可有可无) + 数字(可有可无)" 的模版
    if string.match(input, "^(S%d+)(%.?)(%d*)$") ~= nil then
        -- 去除字符串首的字母
        str = string.gsub(input, "^(%a+)", "")
        numberPart = numberTranslatorFunc(str)
        if #numberPart > 0 then
            for i = 1, #numberPart do
                -- numberTranslatorFunc()
                yield(
                    Candidate(
                        input,
                        seg.start,
                        seg._end,
                        numberPart[i][1],   -- 候选结果
                        numberPart[i][2]    -- 候选备注
                    )
                )
            end
        end
    end
end

return translator

处理字符串的过程都写在注释中了,这里仅具体说一下接入 RIME 的 translator() 函数。

translator(input, seg) 接受两个参数,input 为用户击入的字符,seg 推测是分词信息,一般用不到,可以当作固定模版。

正则 "^(S%d+)(%.?)(%d*)$" 用于匹配用户的 input

  • S 匹配大写字母「S」,作用类似于快捷键,也可以改为自己喜欢的键位;
  • %d+ 匹配一至多个数字;
  • ^ 表示匹配句首,^(S%d+) 就表示只有以「S」和若干数字开头时才会转换;
  • %. 匹配字符「.」,%.? 表示「.」可有可无;
  • %d* 匹配零至多个数字。

用户输入的字符符合匹配规则,字符串经处理后用 yield(Candidate()) 生成候选词。Candidate() 需要填入 5 个参数,不过其实也只用更改后两个参数就好。

完成后仍然要重新部署一下,就可以试试输入效果了~

demo

了解在 RIME 上套用 Lua 脚本的方法后,相信编写自己的脚本也不觉得困难了,参考模版就能实现自己的奇思妙想。 librime-lua 提供了许多 Lua 脚本,已经实现了很多有意思的想法,供额外参考。


References