chore:新的根节点

This commit is contained in:
amzxyz
2026-01-21 17:43:36 +08:00
commit 274c2e8d00
94 changed files with 3245054 additions and 0 deletions

231
lua/auto_phrase.lua Normal file
View File

@@ -0,0 +1,231 @@
-- @amzxyz https://github.com/amzxyz/rime_wanxiang
-- 自动造词
local AP = {}
-- 注释缓存text -> comment只给中文造词用
local comment_cache = {}
-- 工具是否纯英文ASCII 且至少 1 个字母)
local function is_ascii_word(text)
if not text or text == "" then
return false
end
local has_alpha = false
for i = 1, #text do
local b = text:byte(i)
if b > 127 then
return false
end
if (b >= 65 and b <= 90) or (b >= 97 and b <= 122) then
has_alpha = true
end
end
return has_alpha
end
-- 判断字符是否为汉字(原逻辑)
function AP.is_chinese_only(text)
local non_chinese_pattern = "[%w%p]"
if not text or text == "" then
return false
end
if text:match(non_chinese_pattern) then
return false
end
for _, cp in utf8.codes(text) do
-- 常用汉字区 + 扩展 A/B/C/D/E/F/G
if not (
(cp >= 0x4E00 and cp <= 0x9FFF) or -- CJK Unified Ideographs
(cp >= 0x3400 and cp <= 0x4DBF) or -- CJK Ext-A
(cp >= 0x20000 and cp <= 0x2EBEF) -- CJK Ext-B~G
) then
return false
end
end
return true
end
function AP.init(env)
local config = env.engine.schema.config
local ctx = env.engine.context
-- 中文自动造词的开关(只控制 add_user_dict
local enable_auto_phrase =
config:get_bool("add_user_dict/enable_auto_phrase") or false
local enable_user_dict =
config:get_bool("add_user_dict/enable_user_dict") or false
-- 中文add_user_dict受 add_* 开关影响)
if enable_auto_phrase and enable_user_dict then
env.memory = Memory(env.engine, env.engine.schema, "add_user_dict")
else
env.memory = nil
end
-- 英文enuser不受 add_* 开关影响,始终尝试启用)
env.en_memory = Memory(env.engine, env.engine.schema, "wanxiang_mixedcode")
-- 只要有一边需要,就挂上 commit/delete 通知
if env.en_memory or env.memory then
env._commit_conn = ctx.commit_notifier:connect(function(c)
AP.commit_handler(c, env)
end)
env._delete_conn = ctx.delete_notifier:connect(function(_)
comment_cache = {}
end)
end
end
function AP.fini(env)
if env._commit_conn then
env._commit_conn:disconnect()
env._commit_conn = nil
end
if env._delete_conn then
env._delete_conn:disconnect()
env._delete_conn = nil
end
if env.memory then
env.memory:disconnect()
env.memory = nil
end
if env.en_memory then
env.en_memory:disconnect()
env.en_memory = nil
end
end
function AP.save_comment_cache(cand)
local comment = cand.comment
local comment_text = cand.text
if comment_text and comment_text ~= "" and comment and comment ~= "" then
comment_cache[comment_text] = comment
end
end
-- 入口lua_filter
function AP.func(input, env)
local config = env.engine.schema.config
local context = env.engine.context
local use_comment_cache = env.memory ~= nil -- 只有中文造词才需要缓存注释
for cand in input:iter() do
local genuine_cand = cand:get_genuine()
local preedit = genuine_cand.preedit or ""
local initial_comment = genuine_cand.comment
if use_comment_cache then
AP.save_comment_cache(cand)
end
yield(cand)
end
end
-- 造词(原逻辑 + 新增 '\' 英文造词)
function AP.commit_handler(ctx, env)
if not ctx or not ctx.composition then
comment_cache = {}
return
end
local segments = ctx.composition:toSegmentation():get_segments()
local segments_count = #segments
local commit_text = ctx:get_commit_text() or ""
local raw_input = ctx.input or ""
---------------------------------------------------
-- ① 英文 + '\' 造词 —— 始终启用,只依赖 env.en_memory
-- 条件:
-- - raw_input 末尾为 '\'
-- - commit_text 为“ASCII 且至少 1 字母”的英文
-- 行为:
-- - text = commit_text
-- - custom_code = 编码去掉末尾 '\' + 空格
---------------------------------------------------
if raw_input ~= "" and raw_input:sub(-1) == "\\" and is_ascii_word(commit_text) then
local code_body = raw_input:gsub("\\+$", "") -- 去掉末尾连续 '\'
code_body = code_body:gsub("%s+$", "") -- 去掉尾部空白
if code_body ~= "" and env.en_memory then
local entry = DictEntry()
entry.text = commit_text -- 上屏英文本身
entry.weight = 1
entry.custom_code = code_body .. " " -- 真实编码(无 '\') + 空格
env.en_memory:update_userdict(entry, 1, "")
-- log.info(string.format("[auto_phrase] EN 造词:[%s], code=[%s]", entry.text, entry.custom_code))
end
comment_cache = {}
return
end
---------------------------------------------------
-- ② 中文自动造词:只在 env.memory 存在时工作
---------------------------------------------------
if not env.memory then
-- 中文造词功能被关掉时,直接跳过这一段
comment_cache = {}
return
end
-- 检查是否符合最小造词单元要求
if segments_count <= 1 or utf8.len(commit_text) <= 1 then
comment_cache = {}
return
end
-- 检查是否符合造词内容要求
if not AP.is_chinese_only(commit_text) or comment_cache[commit_text] then
comment_cache = {}
return
end
local preedits_table = {}
local config = env.engine.schema.config
local delimiter = config:get_string("speller/delimiter") or " '"
local escaped_delimiter =
utf8.char(utf8.codepoint(delimiter)):gsub("(%W)", "%%%1")
for i = 1, segments_count do
local seg = segments[i]
local cand = seg:get_selected_candidate()
if cand then
local cand_text = cand.text
local preedit = comment_cache[cand_text]
if preedit and preedit ~= "" then
for part in preedit:gmatch("[^" .. escaped_delimiter .. "]+") do
table.insert(preedits_table, part)
end
end
end
end
if #preedits_table == 0 then
comment_cache = {}
return
end
local dictEntry = DictEntry()
dictEntry.text = commit_text
dictEntry.weight = 1
dictEntry.custom_code = table.concat(preedits_table, " ") .. " "
env.memory:update_userdict(dictEntry, 1, "")
comment_cache = {}
end
return AP

55
lua/backspace_limit.lua Normal file
View File

@@ -0,0 +1,55 @@
-- backspace_limiter.lua
-- 防止连续 Backspace 在编码为空时删除已上屏内容虽然我更推荐拍下esc。
-- 这个功能依赖按键事件的处理,运行逻辑的问题在手机上无法得到好的效果,其中macOS特非常特殊,它的按键事件等同于手机逻辑,因此手机和Mac都屏蔽了这一功能
-- @author amzxyz
local M = {}
local ACCEPT, PASS = 1, 2
-- 引入移动设备检测模块
local wanxiang = require("wanxiang")
-- 状态标志说明:
-- env.prev_input_len: 上一次按键前的输入长度
-- env.bs_sequence: 当前是否处于连续 Backspace 序列中
function M.init(env)
env.prev_input_len = -1 -- 初始化为无效值
env.bs_sequence = false
end
function M.func(key, env)
local ctx = env.engine.context
local kc = key.keycode
-- 非 Backspace 键或按键释放事件:重置状态
if kc ~= 0xFF08 or key:release() then
env.bs_sequence = false
env.prev_input_len = -1
return PASS
end
-- 获取当前输入长度
local current_len = #ctx.input
-- 处于连续 Backspace 序列中
if env.bs_sequence then
-- 移动设备由于运行逻辑的问题不能实现友好的逻辑
if wanxiang.is_mobile_device() then
return PASS -- 直接放行
-- PC设备保持原有逻辑长度1变0时拦截
else
if env.prev_input_len == 1 and current_len == 0 then
return ACCEPT -- 拦截PC设备上从1变为0的情况
end
end
-- 更新状态
env.prev_input_len = current_len
return PASS
end
-- 开始新的 Backspace 序列
env.bs_sequence = true
env.prev_input_len = current_len
-- 首次按键总是允许
return PASS
end
return M

BIN
lua/charset.bin Normal file

Binary file not shown.

262
lua/input_statistics.lua Normal file
View File

@@ -0,0 +1,262 @@
-- github.com/amzxyz
-- 一个用于统计输入字数和其他时间维度的统计。先搭起一个框架,有志之士看看如何优化,统计什么数据,什么维度,构建一个有效的统计信息
-- 硬编码输入方案信息
local schema_name = "万象拼音"
local software_name = rime_api.get_distribution_code_name()
local software_version = rime_api.get_distribution_version()
-- 初始化统计表(若未加载)
input_stats = input_stats or {
daily = {count = 0, length = 0, fastest = 0, ts = 0},
weekly = {count = 0, length = 0, fastest = 0, ts = 0},
monthly = {count = 0, length = 0, fastest = 0, ts = 0},
yearly = {count = 0, length = 0, fastest = 0, ts = 0},
lengths = {},
daily_max = 0,
recent = {}
}
-- 时间戳工具函数
local function start_of_day(t)
return os.time{year=t.year, month=t.month, day=t.day, hour=0}
end
local function start_of_week(t)
local d = t.wday == 1 and 6 or (t.wday - 2)
return os.time{year=t.year, month=t.month, day=t.day - d, hour=0}
end
local function start_of_month(t)
return os.time{year=t.year, month=t.month, day=1, hour=0}
end
local function start_of_year(t)
return os.time{year=t.year, month=1, day=1, hour=0}
end
-- 判断是否是统计命令
local function is_summary_command(text)
return text == "/rtj" or text == "/ztj" or text == "/ytj" or text == "/ntj" or text == "/tj"
end
-- 更新统计数据
local function update_stats(input_length)
local now = os.date("*t")
local now_ts = os.time(now)
local day_ts = start_of_day(now)
local week_ts = start_of_week(now)
local month_ts = start_of_month(now)
local year_ts = start_of_year(now)
if input_stats.daily.ts ~= day_ts then
input_stats.daily = {count = 0, length = 0, fastest = 0, ts = day_ts}
input_stats.daily_max = 0
input_stats.recent = {}
end
if input_stats.weekly.ts ~= week_ts then
input_stats.weekly = {count = 0, length = 0, fastest = 0, ts = week_ts}
end
if input_stats.monthly.ts ~= month_ts then
input_stats.monthly = {count = 0, length = 0, fastest = 0, ts = month_ts}
end
if input_stats.yearly.ts ~= year_ts then
input_stats.yearly = {count = 0, length = 0, fastest = 0, ts = year_ts}
end
-- 更新记录
local update = function(stat)
stat.count = stat.count + 1
stat.length = stat.length + input_length
end
update(input_stats.daily)
update(input_stats.weekly)
update(input_stats.monthly)
update(input_stats.yearly)
if input_length > input_stats.daily_max then
input_stats.daily_max = input_length
end
input_stats.lengths[input_length] = (input_stats.lengths[input_length] or 0) + 1
-- 最近一分钟统计
local ts = os.time()
table.insert(input_stats.recent, {ts = ts, len = input_length})
local threshold = ts - 60
local total = 0
local new_recent = {}
for _, item in ipairs(input_stats.recent) do
if item.ts >= threshold then
total = total + item.len
table.insert(new_recent, item)
end
end
input_stats.recent = new_recent
if total > input_stats.daily.fastest then input_stats.daily.fastest = total end
if total > input_stats.weekly.fastest then input_stats.weekly.fastest = total end
if total > input_stats.monthly.fastest then input_stats.monthly.fastest = total end
if total > input_stats.yearly.fastest then input_stats.yearly.fastest = total end
end
-- 表序列化工具(请自行根据实际添加到环境中)
table.serialize = function(tbl)
local lines = {"{"}
for k, v in pairs(tbl) do
local key = (type(k) == "string") and ("[\"" .. k .. "\"]") or ("[" .. k .. "]")
local val
if type(v) == "table" then
val = table.serialize(v)
elseif type(v) == "string" then
val = '"' .. v .. '"'
else
val = tostring(v)
end
table.insert(lines, string.format(" %s = %s,", key, val))
end
table.insert(lines, "}")
return table.concat(lines, "\n")
end
-- 保存至文件
local function save_stats()
local path = rime_api.get_user_data_dir() .. "/lua/input_stats.lua"
local file = io.open(path, "w")
if not file then return end
file:write("input_stats = " .. table.serialize(input_stats) .. "\n")
file:close()
end
-- 显示函数(以日统计为例)
local function format_daily_summary()
local s = input_stats.daily
if s.count == 0 then return "※ 今天没有任何记录。" end
return string.format(
"※ 今天的统计:\n%s\n◉ 今天\n共上屏[%d]次\n共输入[%d]字\n最快一分钟输入了[%d]字\n%s\n◉ 方案:%s\n◉ 平台:%s %s\n%s",
string.rep("", 14), s.count, s.length, s.fastest,
string.rep("", 14), schema_name, software_name, software_version,
string.rep("", 14))
end
-- 显示函数(周统计)
local function format_weekly_summary()
local s = input_stats.weekly
if s.count == 0 then return "※ 本周没有任何记录。" end
return string.format(
"※ 本周的统计:\n%s\n◉ 本周共上屏[%d]次\n共输入[%d]字\n最快一分钟输入了[%d]字\n周内单日最多一次输入[%d]字\n%s\n◉ 方案:%s\n◉ 平台:%s %s\n%s",
string.rep("", 14), s.count, s.length, s.fastest, input_stats.daily_max,
string.rep("", 14), schema_name, software_name, software_version,
string.rep("", 14))
end
-- 显示函数(月统计)
local function format_monthly_summary()
local s = input_stats.monthly
if s.count == 0 then return "※ 本月没有任何记录。" end
return string.format(
"※ 本月的统计:\n%s\n◉ 本月共上屏[%d]次\n共输入[%d]字\n最快一分钟输入了[%d]字\n%s\n◉ 方案:%s\n◉ 平台:%s %s\n%s",
string.rep("", 14), s.count, s.length, s.fastest,
string.rep("", 14), schema_name, software_name, software_version,
string.rep("", 14))
end
-- 显示函数(年统计)
local function format_yearly_summary()
local s = input_stats.yearly
if s.count == 0 then return "※ 本年没有任何记录。" end
local length_counts = {}
for length, count in pairs(input_stats.lengths) do
table.insert(length_counts, {length = length, count = count})
end
table.sort(length_counts, function(a, b) return a.count > b.count end)
local fav = length_counts[1] and length_counts[1].length or 0
return string.format(
"※ 本年的统计:\n%s\n◉ 本年共上屏[%d]次\n共输入[%d]字\n最快一分钟输入了[%d]字\n您最常输入长度为[%d]的词组\n%s\n◉ 方案:%s\n◉ 平台:%s %s\n%s",
string.rep("", 14), s.count, s.length, s.fastest, fav,
string.rep("", 14), schema_name, software_name, software_version,
string.rep("", 14))
end
-- 转换器函数:处理命令 /rtj /ztj /ytj /ntj
local function translator(input, seg, env)
if input:sub(1, 1) ~= "/" then return end
local summary = ""
if input == "/rtj" then
summary = format_daily_summary()
elseif input == "/ztj" then
summary = format_weekly_summary()
elseif input == "/ytj" then
summary = format_monthly_summary()
elseif input == "/ntj" then
summary = format_yearly_summary()
elseif input == "/tj" then
summary = format_daily_summary() .. "\n\n" .. format_weekly_summary() .. "\n\n" .. format_monthly_summary() .. "\n\n" .. format_yearly_summary()
elseif input == "/tjql" then
input_stats = {
daily = {count = 0, length = 0, fastest = 0, ts = 0},
weekly = {count = 0, length = 0, fastest = 0, ts = 0},
monthly = {count = 0, length = 0, fastest = 0, ts = 0},
yearly = {count = 0, length = 0, fastest = 0, ts = 0},
lengths = {},
daily_max = 0,
recent = {}
}
save_stats()
summary = "※ 所有统计数据已清空。"
end
if summary ~= "" then
yield(Candidate("stat", seg.start, seg._end, summary, ""))
end
end
-- 加载保存的统计数据input_stats.lua
local function load_stats_from_lua_file()
local path = rime_api.get_user_data_dir() .. "/lua/input_stats.lua"
local ok, result = pcall(function()
local env = {}
local f = loadfile(path, "t", env)
if f then f() end
return env.input_stats
end)
if ok and type(result) == "table" then
input_stats = result
else
-- 保底初始化,防止错误
input_stats = {
daily = {count = 0, length = 0, fastest = 0, ts = 0},
weekly = {count = 0, length = 0, fastest = 0, ts = 0},
monthly = {count = 0, length = 0, fastest = 0, ts = 0},
yearly = {count = 0, length = 0, fastest = 0, ts = 0},
lengths = {},
daily_max = 0,
recent = {}
}
end
end
local function init(env)
local ctx = env.engine.context
-- 加载历史统计数据
load_stats_from_lua_file()
-- 注册提交通知回调
ctx.commit_notifier:connect(function()
local commit_text = ctx:get_commit_text()
if not commit_text or commit_text == "" then return end
-- 排除统计命令(如 /rtj、/tj 等)
if is_summary_command(commit_text) then return end
-- 排除统计候选上屏内容(例如 "※ 今天..." 或 "◉ 本年..."
if commit_text:match("^[※◉]") then return end
-- 排除我们自己生成的统计候选comment 是 "input_stats_summary"
-- local cand = ctx:get_selected_candidate()
-- if cand and cand.comment == "input_stats_summary" then return end
-- 保存最近一次 commit 内容
env.last_commit_text = commit_text
-- 统计长度
local input_length = utf8.len(commit_text) or string.len(commit_text)
update_stats(input_length)
save_stats()
end)
end
return { init = init, func = translator }

92
lua/key_binder.lua Normal file
View File

@@ -0,0 +1,92 @@
-- 正则按键绑定处理器
-- 本处理器在 Rime 标准库的按键绑定处理器key_binder的基础上增加了用正则表达式判断当前输入的编码的功能
-- 也即,在输入编码不同时,可以将按键绑定到不同的功能
-- RIME_PROCESS_RESULTS 定义在 wanxiang.lua 中,这里需要引入才能使用
local wanxiang = require("wanxiang")
local this = {}
---@class KeyBinderEnv: Env
---@field redirecting boolean
---@field bindings Binding[]
---@class Binding
---element
---@field match string
---@field accept KeyEvent
---@field send_sequence KeySequence
---解析配置文件中的按键绑定配置
---@param value ConfigMap
---@return Binding | nil
local function parse(value)
local match = value:get_value("match")
local accept = value:get_value("accept")
local send_sequence = value:get_value("send_sequence")
if not match or not accept or not send_sequence then
return nil
end
local key_event = KeyEvent(accept:get_string())
local sequence = KeySequence(send_sequence:get_string())
local binding = { match = match:get_string(), accept = key_event, send_sequence = sequence }
return binding
end
---@param env KeyBinderEnv
function this.init(env)
env.redirecting = false
---@type Binding[]
env.bindings = {}
local bindings = env.engine.schema.config:get_list("key_binder/bindings")
if not bindings then
return
end
for i = 1, bindings.size do
local item = bindings:get_at(i - 1)
if not item then goto continue end
local value = item:get_map()
if not value then goto continue end
local binding = parse(value)
if not binding then goto continue end
table.insert(env.bindings, binding)
::continue::
end
end
---@param key_event KeyEvent
---@param env KeyBinderEnv
---@return ProcessResult
function this.func(key_event, env)
-- local input = rime.current(env.engine.context)
local input = env.engine.context.input
-- log.info("key_binder"..input)
if env.redirecting then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
if not input then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
if env.engine.context == nil or env.engine.context.composition == nil or env.engine.context.composition:back() == nil then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
if not env.engine.context.composition:back():has_tag("abc") then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
for _, binding in ipairs(env.bindings) do
-- 只有当按键和当前输入的模式都匹配的时候,才起作用
if key_event:eq(binding.accept) and rime_api.regex_match(input, binding.match) then
env.redirecting = true
for _, event in ipairs(binding.send_sequence:toKeyEvent()) do
env.engine:process_key(event)
end
env.redirecting = false
return wanxiang.RIME_PROCESS_RESULTS.kAccepted
end
end
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
return this

256
lua/kp_number_processor.lua Normal file
View File

@@ -0,0 +1,256 @@
-- https://github.com/amzxyz/rime_wanxiang
-- 万象家族 lua小键盘行为控制
-- - 小键盘数字:根据 kp_number_mode 决定 “参与编码 / 直接上屏”
-- - 主键盘数字:在有候选菜单时,用于选第 n 个候选
--
-- 用法示例schema.yaml
-- engine:
-- processors:
-- - lua_processor@*kp_number_processor
-- # 小键盘模式(可省略,默认 auto
-- # auto : 空闲时直接上屏,输入中参与编码
-- # compose : 无论是否在输入中,小键盘都参与编码(不直接上屏)
-- kp_number_mode: auto
local wanxiang = require("wanxiang")
-- 小键盘键码映射
local KP = {
[0xFFB1] = 1, -- KP_1
[0xFFB2] = 2,
[0xFFB3] = 3,
[0xFFB4] = 4,
[0xFFB5] = 5,
[0xFFB6] = 6,
[0xFFB7] = 7,
[0xFFB8] = 8,
[0xFFB9] = 9,
[0xFFB0] = 0, -- KP_0
}
local P = {}
-- 从 schema 读取 kp_number/patterns 列表
local function load_function_patterns(config)
local patterns = {}
local ok_list, list = pcall(function()
return config:get_list("kp_number/patterns")
end)
if ok_list and list and list.size and list.size > 0 then
for i = 0, list.size - 1 do
local item = list:get_value_at(i)
if item then
local pat = item:get_string()
if pat and pat ~= "" then
table.insert(patterns, pat)
end
end
end
end
-- 如果用户没配,给一份保底的默认集合(等价你现在用的那些)
if #patterns == 0 then
patterns = {
"^/[0-9]$", "^/10$", "^/[A-Za-z]+$",
"^`[A-Za-z]*$",
"^``[A-Za-z/`']*$",
"^U[%da-f]+$",
"^R[0-9]+%.?[0-9]*$",
"^N0[1-9]?0?[1-9]?$",
"^N1[02]?0?[1-9]?$",
"^N0[1-9]?[1-2]?[1-9]?$",
"^N1[02]?[1-2]?[1-9]?$",
"^N0[1-9]?3?[01]?$",
"^N1[02]?3?[01]?$",
"^N19?[0-9]?[0-9]?[01]?[0-2]?[0-3]?[0-9]?$",
"^N20?[0-9]?[0-9]?[01]?[0-2]?[0-3]?[0-9]?$",
"^V.*$",
}
end
return patterns
end
-- 根据“当前编码 + 这次按下的数字字符”判断是否属于命令模式
local function is_function_code_after_digit(env, context, digit_char)
if not context or not digit_char or digit_char == "" then
return false
end
local code = context.input or ""
local s = code .. digit_char
local pats = env.function_patterns
if not pats or #pats == 0 then
return false
end
for _, pat in ipairs(pats) do
-- 这里 pat 必须是 Lua pattern 语法
if s:match(pat) then
return true
end
end
return false
end
---@param env Env
function P.init(env)
local engine = env.engine
local config = engine.schema.config
local context = engine.context
-- 读数字选词个数
env.page_size = config:get_int("menu/page_size") or 6
-- 读小键盘模式auto / compose默认 auto
local m = config:get_string("kp_number/kp_number_mode") or "auto"
if m ~= "auto" and m ~= "compose" then
m = "auto"
end
env.kp_mode = m
-- 初始化状态快照
env.context = context
env.is_composing = context:is_composing()
env.has_menu = context:has_menu()
-- 读取命令模式 Lua pattern 集合
env.function_patterns = load_function_patterns(config)
-- 用 update_notifier 同步 context / is_composing / has_menu
env.kp_update_connection = context.update_notifier:connect(function(ctx)
env.context = ctx
env.is_composing = ctx:is_composing()
env.has_menu = ctx:has_menu()
end)
end
---@param env Env
function P.fini(env)
if env.kp_update_connection then
env.kp_update_connection:disconnect()
env.kp_update_connection = nil
end
env.context = nil
env.is_composing = nil
env.has_menu = nil
env.function_patterns = nil
end
---@param key KeyEvent
---@param env Env
---@return ProcessResult
function P.func(key, env)
-- 只处理按下
if key:release() then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
local engine = env.engine
local context = env.context or engine.context
local mode = env.kp_mode or "auto"
local page_sz = env.page_size
local is_composing = env.is_composing
local has_menu = env.has_menu
------------------------------------------------------------------
-- 1) 小键盘数字auto / compose
-- 如果“加上本次数字后”还匹配某个命令模式 pattern
-- 只作为编码输入,不 commit、不选词。
------------------------------------------------------------------
local kp_num = KP[key.keycode]
if kp_num ~= nil then
local ch = tostring(kp_num) -- "0".."9"
if is_function_code_after_digit(env, context, ch) then
if context then
if context.push_input then
context:push_input(ch)
else
context.input = (context.input or "") .. ch
end
end
return wanxiang.RIME_PROCESS_RESULTS.kAccepted
end
if mode == "auto" then
-- 输入中:参与编码;空闲:直接上屏
if is_composing then
if context.push_input then
context:push_input(ch)
else
context.input = (context.input or "") .. ch
end
else
engine:commit_text(ch)
end
else
-- compose始终参与编码
if context.push_input then
context:push_input(ch)
else
context.input = (context.input or "") .. ch
end
end
return wanxiang.RIME_PROCESS_RESULTS.kAccepted
end
------------------------------------------------------------------
-- 2) 主键盘数字:
-- 2.1 若“加上本次数字后”匹配命令模式 → 只当编码输入
-- 2.2 否则:
-- 有菜单时:选第 n 个候选
-- 空闲时:直接上屏
------------------------------------------------------------------
local r = key:repr() or ""
if r:match("^[0-9]$") then
-- 命令模式:只作为编码输入
if is_function_code_after_digit(env, context, r) then
if context then
if context.push_input then
context:push_input(r)
else
context.input = (context.input or "") .. r
end
end
return wanxiang.RIME_PROCESS_RESULTS.kAccepted
end
-- 有候选菜单时,用数字选「当前页」的第 n 个候选
if has_menu then
local d = tonumber(r)
if d and d >= 1 and d <= page_sz then
local composition = context and context.composition
if composition and not composition:empty() then
local seg = composition:back() -- 当前正在编辑的 segment
local menu = seg and seg.menu
if menu and not menu:empty() then
-- 当前高亮候选的全局下标0 开始)
local sel_index = seg.selected_index or 0
local page_size = page_sz
-- 当前页号 = 高亮索引 / 每页大小
local page_no = math.floor(sel_index / page_size)
local page_start = page_no * page_size
-- 当前页第 n 个候选的全局下标
local index = page_start + (d - 1)
-- 防止越界(最后一页候选不足一整页)
if index < menu:candidate_count() then
if context:select(index) then
return wanxiang.RIME_PROCESS_RESULTS.kAccepted
end
end
end
end
end
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
end
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
return P

89
lua/letter_selector.lua Normal file
View File

@@ -0,0 +1,89 @@
-- @amzxyz https://github.com/amzxyz/rime_wanxiang
-- 功能仅在特定前缀或者tag模式下按 qwertyuio 选择第 1~9 个候选
local wanxiang = require("wanxiang")
local M = {}
-- 键码映射q w e r t y u i o → 1..9
local KEY2IDX = {
[0x71] = 1, -- q
[0x77] = 2, -- w
[0x65] = 3, -- e
[0x72] = 4, -- r
[0x74] = 5, -- t
[0x79] = 6, -- y
[0x75] = 7, -- u
[0x69] = 8, -- i
[0x6F] = 9, -- o
}
-- 判断是否在命令模式
local function is_function_mode_active(context)
if not context or not context.composition or context.composition:empty() then
return false
end
local seg = context.composition:back()
if not seg then return false end
return seg:has_tag("number") or seg:has_tag("Ndate")
end
-- 缓存命令模式的状态,避免每次按键都计算
local function on_update(env, ctx)
env._fn_active = is_function_mode_active(ctx)
end
function M.init(env)
env._fn_active = false
env._upd_conn = env.engine.context.update_notifier:connect(function(ctx)
on_update(env, ctx)
end)
end
function M.fini(env)
if env._upd_conn then
env._upd_conn:disconnect()
env._upd_conn = nil
end
end
local function handle_key(key_event, env)
-- 只处理按下;有修饰键则忽略
if key_event:release() or key_event:ctrl() or key_event:alt() or key_event:super() then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
local idx = KEY2IDX[key_event.keycode]
if not idx then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
local context = env.engine.context
if not env._fn_active then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
if not context or not context.composition or context.composition:empty() then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
local seg = context.composition:back()
if not seg or not seg.menu then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
-- 准备最多 9 个候选
local count = seg.menu:prepare(9)
if idx < 1 or idx > count then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
-- 选择:候选索引从 0 开始
context:select(idx - 1)
return wanxiang.RIME_PROCESS_RESULTS.kAccepted
end
function M.func(key_event, env)
return handle_key(key_event, env)
end
return M

68
lua/lib/bit.lua Normal file
View File

@@ -0,0 +1,68 @@
local bit_ok, bit_ = pcall(require, "bit") -- LuaJIT 内置 bit 库
local bit32_ok, bit32_ = pcall(require, "bit32") -- Lua 5.2 内置 bit32 库
---@alias fn_band fun(a: integer, b: integer): integer
---@alias fn_bxor fun(a: integer, b: integer): integer
---@type nil | { band: fn_band, bxor: fn_bxor }
local bit53_ = nil -- Lua 5.3 引入的原生位运算操作符
---@diagnostic disable-next-line: deprecated
local load_func = load or loadstring
if load_func then
---将新语法放入字符串中,避免在旧版 Lua 中导致语法错误
local bit53_func, bit53_err = load_func("return {" ..
"band = function(a, b) return a & b end," ..
"bxor = function(a, b) return a ~ b end," ..
"}")
if bit53_func and not bit53_err then
bit53_ = bit53_func()
end
end
local bit = {}
---@return integer
function bit.bxor(a, b)
if bit_ok then
return bit_.bxor(a, b)
elseif bit32_ok then
return bit32_.bxor(a, b)
elseif bit53_ then
return bit53_.bxor(a, b)
end
local p, c = 1, 0
while a > 0 and b > 0 do
local ra, rb = a % 2, b % 2
if ra ~= rb then c = c + p end
a, b, p = (a - ra) / 2, (b - rb) / 2, p * 2
end
if a < b then a = b end
while a > 0 do
local ra = a % 2
if ra > 0 then c = c + p end
a, p = (a - ra) / 2, p * 2
end
return c
end
---@return integer
function bit.band(a, b)
if bit_ok then
return bit_.band(a, b)
elseif bit32_ok then
return bit32_.band(a, b)
elseif bit53_ then
return bit53_.band(a, b)
end
local p, c = 1, 0
while a > 0 and b > 0 do
local ra, rb = a % 2, b % 2
if ra + rb > 1 then c = c + p end
a, b, p = (a - ra) / 2, (b - rb) / 2, p * 2
end
return c
end
return bit

116
lua/lib/userdb.lua Normal file
View File

@@ -0,0 +1,116 @@
local META_KEY_PREFIX = "\001" .. "/"
-- UserDb 缓存,使用弱引用表,不阻止垃圾回收并能自动清理
local db_pool = setmetatable({}, { __mode = "v" })
---@class WrappedUserDb: UserDb
---@field meta_query fun(self: self, prefix: string): DbAccessor
---@field meta_fetch fun(self: self, key: string): string|nil
---@field meta_update fun(self: self, key: string, value: string): boolean
---@field meta_erase fun(self: self, key: string): boolean
---@field query_with fun(self: self, prefix: string, handler: fun(key: string, value: string))
---@field empty fun(self: self, include_metafield?: boolean) -- 清空数据库
-- 用于存放包装器对象的自定义方法
local extends = {}
--- @param key string
--- @return string|nil
function extends:meta_fetch(key)
return self._db:fetch(META_KEY_PREFIX .. key)
end
--- @param key string
--- @param value string
--- @return boolean
function extends:meta_update(key, value)
return self._db:update(META_KEY_PREFIX .. key, value)
end
--- @param key string
--- @return boolean
function extends:meta_erase(key)
return self._db:erase(META_KEY_PREFIX .. key)
end
--- @param prefix string
--- @return DbAccessor
function extends:meta_query(prefix)
return self._db:query(META_KEY_PREFIX .. prefix)
end
function extends:query_with(prefix, handler)
local da = self._db:query(prefix)
if da then
for key, value in da:iter() do
handler(key, value)
end
end
da = nil
collectgarbage()
end
--- @param include_metafield boolean 是否也清理元数据。
function extends:empty(include_metafield)
self:query_with("", function(key, _)
local is_metafield = key:find(META_KEY_PREFIX, 1, true) == 1
if include_metafield or not is_metafield then
self._db:erase(key)
end
end)
end
local mt = {
__index = function(wrapper, key)
-- 优先使用自定义方法
if extends[key] then
return extends[key]
end
-- 不是自定义方法,委托给真实的 UserDb 对象
local real_db = wrapper._db
local value = real_db[key]
if type(value) == "function" then
return function(_, ...)
return value(real_db, ...)
end
end
return value
end,
}
local userdb = {}
--- @param db_name string
--- @param db_class "userdb" | "plain_userdb" | nil
--- @return WrappedUserDb
function userdb.UserDb(db_name, db_class)
db_class = db_class or "userdb"
local key = db_name .. "." .. db_class
---@type UserDb
local db = db_pool[key]
if not db then
db = UserDb(db_name, db_class)
db_pool[key] = db
end
local wrapper = {
_db = db,
_pool_key = key,
}
return setmetatable(wrapper, mt)
end
function userdb.LevelDb(db_name)
return userdb.UserDb(db_name, "userdb")
end
function userdb.TableDb(db_name)
return userdb.UserDb(db_name, "plain_userdb")
end
return userdb

722
lua/librime.lua Normal file
View File

@@ -0,0 +1,722 @@
-- librime-lua 官方类型提示
-- ⚠️ 仅用于类型提升,请勿直接 require 使用
-- from https://github.com/hchunhui/librime-lua/blob/master/contrib/librime.lua
-- Last Change: LTS
---@meta rime
--- 全局对象
---@class RimeAPI
---@field get_rime_version fun(): string
---@field get_shared_data_dir fun(): string
---@field get_user_data_dir fun(): string
---@field get_sync_dir fun(): string
---@field get_distribution_name fun(): string
---@field get_distribution_code_name fun(): string
---@field get_distribution_version fun(): string
---@field get_user_id fun(): string
---@field get_time_ms fun(): number
---@field regex_match fun(input: string, pattern: string): boolean
---@field regex_search fun(input: string, pattern: string): string[] | nil
---@field regex_replace fun(input: string, pattern: string, fmt: string): string
rime_api = {}
---@class Log
---@field info fun(string)
---@field warning fun(string)
---@field error fun(string)
log = {}
---@param cand Candidate
function yield(cand) end
--- 常量
---@enum ConfigType
local config_types = {
kNull = "kNull",
kScalar = "kScalar",
kList = "kList",
kMap = "kMap",
}
---@enum SegmentType
local segment_types = {
kVoid = "kVoid",
kGuess = "kGuess",
kSelected = "kSelected",
kConfirmed = "kConfirmed",
}
---@enum CandidateDynamicType
local candidate_dynamic_types = {
kSentence = "Sentence",
kPhrase = "Phrase",
kSimple = "Simple",
kShadow = "Shadow",
kUniquified = "Uniquified",
kOther = "Other",
}
---@enum ProcessResult
local process_results = {
kRejected = 0,
kAccepted = 1,
kNoop = 2,
}
---@enum ModifierMask
local modifier_masks = {
kShift = 0x1,
kLock = 0x2,
kControl = 0x4,
kAlt = 0x8,
}
--- 工具
---@class Set
---@field empty fun(self: self): boolean
---@field __index function
---@field __add function
---@field __sub function
---@field __mul function
---@field __set function
---@param values any[]
---@return Set
function Set(values) end
--- 对象接口及构造函数
---@class Env
---@field engine Engine
---@field name_space string
---@class Engine
---@field schema Schema
---@field context Context
---@field active_engine Engine
---@field process_key fun(self: self, key_event: KeyEvent): boolean
---@field compose fun(self: self, ctx: Context)
---@field commit_text fun(self: self, text: string)
---@field apply_schema fun(self: self, schema: Schema)
---@class Context
---@field composition Composition
---@field input string
---@field caret_pos integer
---@field commit_notifier Notifier
---@field select_notifier Notifier
---@field update_notifier Notifier
---@field delete_notifier Notifier
---@field option_update_notifier OptionUpdateNotifier
---@field property_update_notifier PropertyUpdateNotifier
---@field unhandled_key_notifier KeyEventNotifier
---@field commit_history CommitHistory
---@field commit fun(self: self)
---@field get_commit_text fun(self: self): string
---@field get_script_text fun(self: self): string
---@field get_preedit fun(self: self): Preedit
---@field is_composing fun(self: self): boolean
---@field has_menu fun(self: self): boolean
---@field get_selected_candidate fun(self: self): Candidate
---@field push_input fun(self: self, text: string)
---@field pop_input fun(self: self, len: integer): boolean
---@field delete_input fun(self: self, len: integer): boolean
---@field clear fun(self: self)
---@field select fun(self: self, index: integer): boolean
---@field highlight fun(self: self, index: integer): boolean
---@field confirm_current_selection fun(self: self): boolean
---@field delete_current_selection fun(self: self): boolean
---@field confirm_previous_selection fun(self: self): boolean
---@field reopen_previous_selection fun(self: self): boolean
---@field clear_previous_segment fun(self: self): boolean
---@field reopen_previous_segment fun(self: self): boolean
---@field clear_non_confirmed_composition fun(self: self): boolean
---@field refresh_non_confirmed_composition fun(self: self): boolean
---@field set_option fun(self: self, name: string, value: boolean)
---@field get_option fun(self: self, name: string): boolean
---@field set_property fun(self: self, key: string, value: string)
---@field get_property fun(self: self, key: string): string
---@field clear_transient_options fun(self: self)
---@class Preedit
---@field text string
---@field caret_pos integer
---@field sel_start integer
---@field sel_end integer
---@class Composition
---@field empty fun(self: self): boolean
---@field back fun(self: self): Segment
---@field pop_back fun(self: self)
---@field push_back fun(self: self)
---@field has_finished_composition fun(self: self): boolean
---@field get_prompt fun(self: self): string
---@field toSegmentation fun(self: self): Segmentation
---@field spans fun(self: self): Spans
---@class Segmentation
---@field input string
---@field size integer
---@field empty fun(self: self): boolean
---@field back fun(self: self): Segment | nil
---@field pop_back fun(self: self)
---@field reset_length fun(self: self, length: integer)
---@field add_segment fun(self: self, seg: Segment): boolean
---@field forward fun(self: self): boolean
---@field trim fun(self: self): boolean
---@field has_finished_segmentation fun(self: self): boolean
---@field get_current_start_position fun(self: self): integer
---@field get_current_end_position fun(self: self): integer
---@field get_current_segment_length fun(self: self): integer
---@field get_confirmed_position fun(self: self): integer
---@field get_segments fun(self: self): Segment[]
---@field get_at fun(self: self, index: integer): Segment
---@class Segment
---@field status SegmentType
---@field start integer
---@field _start integer
---@field _end integer
---@field length integer
---@field tags Set
---@field menu Menu
---@field selected_index integer
---@field prompt string
---@field clear fun(self: self)
---@field close fun(self: self)
---@field reopen fun(self: self, caret_pos: integer)
---@field has_tag fun(self: self, tag: string): boolean
---@field get_candidate_at fun(self: self, index: integer): Candidate
---@field get_selected_candidate fun(self: self): Candidate
---@field active_text fun(self: self, text: string): string
---@field spans fun(self: self): Spans
---@param start_pos integer
---@param end_pos integer
---@return Segment
function Segment(start_pos, end_pos) end
---@class Spans
---@field _start integer
---@field _end integer
---@field count integer
---@field vertices integer[]
---@field add_span fun(self: self, start: integer, end: integer)
---@field add_spans fun(self: self, spans: Spans)
---@field add_vertex fun(self: self, vertex: integer)
---@field previous_stop fun(self: self, caret_pos: integer): integer
---@field next_stop fun(self: self, caret_pos: integer): integer
---@field has_vertex fun(self: self, vertex: integer): boolean
---@field count_between fun(self: self, start: integer, end: integer): integer
---@field clear fun(self: self)
---@return Spans
function Spans() end
---@class Schema
---@field schema_id string
---@field schema_name string
---@field config Config
---@field page_size integer
---@field select_keys string
---@param schema_id string
---@return Schema
function Schema(schema_id) end
---@class Config
---@field load_from_file fun(self: self, filename: string): boolean
---@field save_to_file fun(self: self, filename: string): boolean
---@field is_null fun(self: self, conf_path: string): boolean
---@field is_value fun(self: self, conf_path: string): boolean
---@field is_list fun(self: self, conf_path: string): boolean
---@field is_map fun(self: self, conf_path: string): boolean
---@field get_bool fun(self: self, conf_path: string): boolean|nil
---@field set_bool fun(self: self, conf_path: string, b: boolean): boolean
---@field get_int fun(self: self, conf_path: string): integer|nil
---@field set_int fun(self: self, conf_path: string, i: integer): boolean
---@field get_double fun(self: self, conf_path: string): number|nil
---@field set_double fun(self: self, conf_path: string, f: number): boolean
---@field get_string fun(self: self, conf_path: string): string|nil
---@field set_string fun(self: self, conf_path: string, s: string): boolean
---@field get_item fun(self: self, conf_path: string): ConfigItem|nil
---@field set_item fun(self: self, conf_path: string, item: ConfigItem): boolean
---@field get_value fun(self: self, conf_path: string): ConfigValue|nil
---@field set_value fun(self: self, conf_path: string, value: ConfigValue): boolean
---@field get_list fun(self: self, conf_path: string): ConfigList|nil
---@field set_list fun(self: self, conf_path: string, list: ConfigList): boolean
---@field get_map fun(self: self, conf_path: string): ConfigMap|nil
---@field set_map fun(self: self, conf_path: string, map: ConfigMap): boolean
---@field get_list_size fun(self: self, conf_path: string): integer|nil
---@class ConfigMap
---@field type ConfigType
---@field size integer
---@field element ConfigItem
---@field empty fun(self: self): boolean
---@field has_key fun(self: self, key: string): boolean
---@field keys fun(self: self): string[]
---@field get fun(self: self, key: string): ConfigItem|nil
---@field get_value fun(self: self, key: string): ConfigValue|nil
---@field set fun(self: self, key: string, item: ConfigItem)
---@field clear fun(self: self)
---@return ConfigMap
function ConfigMap() end
---@class ConfigList
---@field type ConfigType
---@field size integer
---@field element ConfigItem
---@field get_at fun(self: self, index: integer): ConfigItem|nil
---@field get_value_at fun(self: self, index: integer): ConfigValue|nil
---@field set_at fun(self: self, index: integer, item: ConfigItem): boolean
---@field append fun(self: self, item: ConfigItem): boolean
---@field insert fun(self: self, i: integer, item: ConfigItem): boolean
---@field clear fun(self: self): boolean
---@field empty fun(self: self): boolean
---@field resize fun(self: self, size: integer): boolean
---@return ConfigList
function ConfigList() end
---@class ConfigValue
---@field type ConfigType
---@field value string
---@field element ConfigItem
---@field get_bool fun(self: self): boolean|nil
---@field get_int fun(self: self): integer|nil
---@field get_double fun(self: self): number|nil
---@field get_string fun(self: self): string|nil
---@field set_bool fun(self: self, b: boolean)
---@field set_int fun(self: self, i: integer)
---@field set_double fun(self: self, f: number)
---@field set_string fun(self: self, s: string)
---@param value string | boolean
---@return ConfigValue
function ConfigValue(value) end
---@class ConfigItem
---@field type ConfigType
---@field empty boolean
---@field get_value fun(self: self): ConfigValue|nil
---@field get_map fun(self: self): ConfigMap|nil
---@field get_list fun(self: self): ConfigList|nil
---@field get_obj fun(self: self): ConfigMap|ConfigList|ConfigValue|nil
---@class KeyEvent
---@field keycode integer
---@field modifier integer
---@field shift fun(self: self): boolean
---@field ctrl fun(self: self): boolean
---@field alt fun(self: self): boolean
---@field caps fun(self: self): boolean
---@field super fun(self: self): boolean
---@field release fun(self: self): boolean
---@field repr fun(self: self): string
---@field eq fun(self: self, key: KeyEvent): boolean
---@field lt fun(self: self, key: KeyEvent): boolean
---@param repr string
---@return KeyEvent
function KeyEvent(repr) end
---@param keycode integer
---@param modifier integer
---@return KeyEvent
function KeyEvent(keycode, modifier) end
---@class KeySequence
---@field parse fun(self: self, repr: string): boolean
---@field repr fun(self: self): string
---@field toKeyEvent fun(self: self): KeyEvent[]
---@param repr string?
---@return KeySequence
function KeySequence(repr) end
---@class Candidate
---@field type string
---@field start integer
---@field _start integer
---@field _end integer
---@field quality number
---@field text string
---@field comment string
---@field preedit string
---@field get_dynamic_type fun(self: self): CandidateDynamicType
---@field get_genuine fun(self: self): Candidate
---@field get_genuines fun(self: self): Candidate[]
---@field to_shadow_candidate fun(self: self, type: string?, text: string?, comment: string?, inherit_comment: boolean?): ShadowCandidate
---@field to_uniquified_candidate fun(self: self, type: string?, text: string?, comment: string?): UniquifiedCandidate
---@field to_phrase fun(self: self): Phrase
---@field to_sentence fun(self: self): Sentence
---@field append fun(self: self, cand: Candidate)
---@field spans fun(self: self): Spans
---@param type string
---@param start integer
---@param _end integer
---@param text string
---@param comment string
---@return Candidate
function Candidate(type, start, _end, text, comment) end
---@class UniquifiedCandidate: Candidate
---@param candidate Candidate
---@param type string?
---@param text string?
---@param comment string?
function UniquifiedCandidate(candidate, type, text, comment) end
---@class ShadowCandidate: Candidate
---@param candidate Candidate
---@param type string?
---@param text string?
---@param comment string?
---@param inherit_comment boolean?
---@return ShadowCandidate
function ShadowCandidate(candidate, type, text, comment, inherit_comment) end
---@class Phrase
-----@field language Language 暂时不支持
---@field lang_name string
---@field type string
---@field start integer
---@field _start integer
---@field _end integer
---@field quality number
---@field text string
---@field comment string
---@field preedit string
---@field weight number
---@field code Code
---@field entry DictEntry
---@field toCandidate fun(self: self): Candidate
---@field spans fun(self: self): Spans
---@param memory Memory
---@param type string
---@param start integer
---@param _end integer
---@param entry DictEntry
---@return Phrase
function Phrase(memory, type, start, _end, entry) end
---@class Sentence
-----@field language Language 暂时不支持
---@field lang_name string
---@field type string
---@field start integer
---@field _start integer
---@field _end integer
---@field quality number
---@field text string
---@field comment string
---@field preedit string
---@field weight number
---@field code Code
---@field entry DictEntry
---@field word_lengths integer[]
---@field entrys DictEntry[]
---@field entrys_size integer
---@field entrys_empty boolean
---@field toCandidate fun(self: self): Candidate
---@class Menu
---@field add_translation fun(self: self, translation: Translation)
---@field prepare fun(self: self, candidate_count: integer): integer
---@field get_candidate_at fun(self: self, i: integer): Candidate|nil
---@field candidate_count fun(self: self): integer
---@field empty fun(self: self): boolean
---@return Menu
function Menu() end
---@class Opencc
---@field convert fun(self: self, text: string): string
---@field convert_text fun(self: self, text: string): string
---@field random_convert_text fun(self: self, text: string): string
---@field convert_word fun(self: self, text: string): string[]
---@param filename string
---@return Opencc
function Opencc(filename) end
---@class Dictionary
---@field name string
---@field loaded boolean
---@field lookup_words fun(self: self, code: string, predictive: boolean, limit: integer): boolean
---@field decode fun(self: self, code: Code): string[]
---@class DictEntryIterator
---@field exhausted boolean
---@field size integer
---@field iter fun(self: self): fun(): DictEntry|nil
---@class UserDictionary
---@field name string
---@field loaded boolean
---@field tick integer
---@field lookup_words fun(self: self, code: string, predictive: boolean, limit: integer): boolean
---@field update_entry fun(self: self, entry: DictEntry, commits: integer, prefix: string, lang_name: string): boolean
---@class UserDictEntryIterator
---@field exhausted boolean
---@field size integer
---@field iter fun(self: self): fun(): DictEntry|nil
---@class ReverseDb
---@field lookup fun(self: self, key: string): string
---@param file_name string
---@return ReverseDb
function ReverseDb(file_name) end
---@class ReverseLookup
---@field lookup fun(self: self, key: string): string
---@field lookup_stems fun(self: self, key: string): string
---@param dict_name string
---@return ReverseLookup
function ReverseLookup(dict_name) end
---@class DictEntry
---@field text string
---@field comment string
---@field preedit string
---@field weight number
---@field commit_count integer `2`
---@field custom_code string "hao", "ni hao"
---@field remaining_code_length integer "~ao"
---@field code Code
---@return DictEntry
function DictEntry() end
---@class CommitEntry: DictEntry
---@field get fun(self: self): DictEntry[]
---@field update_entry fun(self: self, entry: DictEntry, commit: integer, prefix: string): boolean
---@field update fun(self: self, commit: integer): boolean
---@class Code
---@field push fun(self: self, syllable_id: integer)
---@field print fun(self: self): string
---@return Code
function Code() end
---@class Translation
---@field exhausted boolean
---@field iter fun(self: self): fun(): Candidate|nil
function Translation() end
---@class Memory
---@field lang_name string
---@field dict Dictionary
---@field user_dict UserDictionary
---@field start_session fun(self: self): boolean
---@field finish_session fun(self: self): boolean
---@field discard_session fun(self: self): boolean
---@field dict_lookup fun(self: self, input: string, predictive: boolean, limit: integer): boolean
---@field user_lookup fun(self: self, input: string, predictive: boolean): boolean
---@field dictiter_lookup fun(self: self, input: string, predictive: boolean, limit: integer): DictEntryIterator
---@field useriter_lookup fun(self: self, input: string, predictive: boolean): UserDictEntryIterator
---@field memorize fun(self: self, callback: fun(ce: CommitEntry))
---@field decode fun(self: self, code: Code): string[]
---@field iter_dict fun(self: self): fun(): DictEntry|nil
---@field iter_user fun(self: self): fun(): DictEntry|nil
---@field update_userdict fun(self: self, entry: DictEntry, commits: integer, prefix: string): boolean
---@field update_entry fun(self: self, entry: DictEntry, commits: integer, prefix: string, lang_name?: string): boolean
---@field update_candidate fun(self: self, candidate: Candidate, commits: integer): boolean
---@field disconnect fun(self: self)
---@param engine Engine
---@param schema Schema
---@param namespace string?
---@return Memory
function Memory(engine, schema, namespace) end
---@class Projection
---@field load fun(self: self, rules: ConfigList): boolean
---@field apply fun(self: self, str: string, ret_org_str?: boolean): string
---@return Projection
function Projection() end
---@class Component
---@field Processor fun(engine: Engine, namespace: string, klass: string): Processor
---@field Translator fun(engine: Engine, namespace: string, klass: string): Translator
---@field Segmentor fun(engine: Engine, namespace: string, klass: string): Segmentor
---@field Filter fun(engine: Engine, namespace: string, klass: string): Filter
---@field ScriptTranslator fun(engine: Engine, namespace: string, klass: string): ScriptTranslator
---@field TableTranslator fun(engine: Engine, namespace: string, klass: string): TableTranslator
Component = {}
---@class Processor
---@field name_space string
---@field process_key_event fun(self: self, key_event: KeyEvent): ProcessResult
---@class Segmentor
---@field name_space string
---@field proceed fun(self: self, segmentation: Segmentation): boolean
---@class Translator
---@field name_space string
---@field query fun(self: self, input: string, segment: Segment): Translation
---@class ScriptTranslator
---@field name_space string
---@field lang_name string
---@field memorize_callback fun(ce: CommitEntry)
---@field max_homophones integer
---@field spelling_hints integer
---@field always_show_comments boolean
---@field enable_correction boolean
---@field delimiters string
---@field tag string
---@field enable_completion boolean
---@field contextual_suggestions boolean
---@field strict_spelling boolean
---@field initial_quality number
---@field preedit_formatter Projection
---@field comment_formatter Projection
---@field dict Dictionary
---@field user_dict UserDictionary
---@field translator Translator
---@field query fun(self: self, input: string, segment: Segment): Translation
---@field start_session fun(self: self): boolean
---@field finish_session fun(self: self): boolean
---@field discard_session fun(self: self): boolean
---@field memorize fun(self: self, callback: fun(ce: CommitEntry))
---@field update_entry fun(self: self, entry: DictEntry, commits: integer, prefix: string): boolean
---@field reload_user_dict_disabling_patterns fun(self: self, config_list: ConfigList): boolean
---@field set_memorize_callback fun(self: self, callback: fun(ce: CommitEntry))
---@field disconnect fun(self: self)
---@class TableTranslator
---@field name_space string
---@field lang_name string
---@field memorize_callback fun(ce: CommitEntry)
---@field enable_charset_filter boolean
---@field enable_encoder boolean
---@field enable_sentence boolean
---@field sentence_over_completion boolean
---@field encode_commit_history boolean
---@field max_phrase_length integer
---@field max_homographs integer
---@field delimiters string
---@field tag string
---@field enable_completion boolean
---@field contextual_suggestions boolean
---@field strict_spelling boolean
---@field initial_quality number
---@field preedit_formatter Projection
---@field comment_formatter Projection
---@field dict Dictionary
---@field user_dict UserDictionary
---@field translator Translator
---@field query fun(self: self, input: string, segment: Segment): Translation
---@field start_session fun(self: self): boolean
---@field finish_session fun(self: self): boolean
---@field discard_session fun(self: self): boolean
---@field memorize fun(self: self, callback: fun(ce: CommitEntry))
---@field update_entry fun(self: self, entry: DictEntry, commits: integer, prefix: string): boolean
---@field reload_user_dict_disabling_patterns fun(self: self, config_list: ConfigList): boolean
---@field set_memorize_callback fun(self: self, callback: fun(ce: CommitEntry))
---@field disconnect fun(self: self)
---@class Filter
---@field name_space string
---@field apply fun(self: self, translation: Translation): Translation
---@class Notifier
---@field connect fun(self: self, f: fun(ctx: Context), group: integer|nil): Connection
---@class OptionUpdateNotifier: Notifier
---@field connect fun(self: self, f: fun(ctx: Context, name: string), group:integer|nil): function[]
---@class PropertyUpdateNotifier: Notifier
---@field connect fun(self: self, f: fun(ctx: Context, name: string), group:integer|nil): function[]
---@class KeyEventNotifier: Notifier
---@field connect fun(self: self, f: fun(ctx: Context, key: string), group:integer|nil): function[]
---@class Connection
---@field disconnect fun(self: self)
---@class Switcher
---@field attached_engine Engine
---@field user_config Config
---@field active boolean
---@field process_key fun(self: self, key_event: KeyEvent): boolean
---@field select_next_schema fun(self: self)
---@field is_auto_save fun(self: self, option: string): boolean
---@field refresh_menu fun(self: self)
---@field activate fun(self: self)
---@field deactivate fun(self: self)
---@param engine Engine
---@return Switcher
function Switcher(engine) end
---@class CommitRecord
---@field text string
---@field type string
---@class CommitHistory
---@field size integer
---@field push fun(self: self, key_event: KeyEvent)
---@field back fun(self: self): CommitRecord|nil
---@field to_table fun(self: self): CommitRecord[]
---@field iter fun(self: self): fun(): (number, CommitRecord)|nil
---@field latest_text fun(self: self): string
---@field empty fun(self: self): boolean
---@field clear fun(self: self)
---@field pop_back fun(self: self)
---@class DbAccessor
---@field reset fun(self: self): boolean
---@field jump fun(self: self, prefix: string): boolean
---@field iter fun(self: self): fun(): (string, string) | nil
---@class UserDb
---@field _loaded boolean
---@field read_only boolean
---@field disabled boolean
---@field name string
---@field file_name string
---@field open fun(self: self): boolean
---@field open_read_only fun(self: self): boolean
---@field close fun(self: self): boolean
---@field query fun(self: self, prefix: string): DbAccessor
---@field fetch fun(self: self, key: string): string|nil
---@field update fun(self: self, key: string, value: string): boolean
---@field erase fun(self: self, key: string): boolean
---@field loaded fun(self: self): boolean
---@field disable fun(self: self): boolean
---@field enable fun(self: self): boolean
---@param db_name string
---@param db_class string
---@return UserDb
function UserDb(db_name, db_class) end
---@class LevelDb: UserDb
---@param db_name string
---@return LevelDb
function LevelDb(db_name) end
---@class TableDb: UserDb
---@param db_name string
---@return TableDb
function TableDb(db_name) end

61
lua/limit_repeated.lua Normal file
View File

@@ -0,0 +1,61 @@
-- 用于限制最大候选数量以及重复最大输入编码,防止卡顿性能异常
--@amzxyz
--https://github.com/amzxyz
local M = {}
local ACCEPT, PASS = 1, 2
local MAX_REPEAT = 8 -- 连续重复输入声母上限
local MAX_SEGMENTS = 40 -- 允许的最大“分段”数
local INITIALS = "[bpmfdtnlgkhjqxrzcsywiu]"
-- 计算末尾重复
local function tail_rep(s)
local last, n = s:sub(-1), 1
for i = #s - 1, 1, -1 do
if s:sub(i, i) == last then n = n + 1 else break end
end
return last, n
end
-- 在候选栏最后一个 segment 加提示
local function prompt(ctx, msg)
local comp = ctx.composition
if comp and not comp:empty() then comp:back().prompt = msg end
end
function M.func(key, env)
local ctx, kc = env.engine.context, key.keycode
-- 先拿到“上一轮”高亮候选的 preedit 及段数
local cand = ctx:get_selected_candidate()
local preedit = cand and (cand.preedit or cand:get_genuine().preedit) or ""
local segs = 1
for _ in preedit:gmatch("[%'%s]") do segs = segs + 1 end
-- 本次按键字符(只关心字母 / 分隔符)
local ch
if kc >= 0x61 and kc <= 0x7A then -- a~z
ch = string.char(kc)
elseif kc == 0x27 then -- '
ch = "'"
elseif kc == 0x20 then -- space
ch = " "
end
-- ① 连续声母限制:第 MAX_REPEAT 个同声母直接拦截
if ch and kc >= 0x61 and kc <= 0x7A then
local nxt = ctx.input .. ch
local last, rep_n = tail_rep(nxt)
if last:match(INITIALS) and rep_n > MAX_REPEAT then
prompt(ctx, " 〔已超最大重复声母〕")
return ACCEPT
end
end
-- ② 分段限制:第 MAX_SEGMENTS 段拦截
local segs_after = segs
if ch == "'" or ch == " " then segs_after = segs + 1 end
if segs_after >= MAX_SEGMENTS and kc >= 0x61 and kc <= 0x7A then
prompt(ctx, " 〔已超最大输入长度〕")
return ACCEPT
end
return PASS
end
return M

175
lua/number_translator.lua Normal file
View File

@@ -0,0 +1,175 @@
-- 来源 https://github.com/yanhuacuo/98wubi-tables > http://98wb.ysepan.com/
-- 数字、金额大写
-- 触发前缀默认为 recognizer/patterns/number 的第 2 个字符,即 R
local function splitNumPart(str)
local part = {}
part.int, part.dot, part.dec = string.match(str, "^(%d*)(%.?)(%d*)")
return part
end
local function GetPreciseDecimal(nNum, n)
if type(nNum) ~= "number" then nNum = tonumber(nNum) end
n = n or 0;
n = math.floor(n)
if n < 0 then n = 0 end
local nDecimal = 10 ^ n
local nTemp = math.floor(nNum * nDecimal);
local nRet = nTemp / nDecimal;
return nRet;
end
local function decimal_func(str, posMap, valMap)
local dec
posMap = posMap or { [1] = "", [2] = "", [3] = "", [4] = "" }
valMap = valMap or { [0] = "", "", "", "", "", "", "", "", "", "" }
if #str > 4 then dec = string.sub(tostring(str), 1, 4) else dec = tostring(str) end
dec = string.gsub(dec, "0+$", "")
if dec == "" then return "" end
local result = ""
for pos = 1, #dec do
local val = tonumber(string.sub(dec, pos, pos))
if val ~= 0 then result = result .. valMap[val] .. posMap[pos] else result = result .. valMap[val] end
end
result = result:gsub(valMap[0] .. valMap[0], valMap[0])
return result:gsub(valMap[0] .. valMap[0], valMap[0])
end
-- 把数字串按千分位四位数分割,进行转换为中文
local function formatNum(num, t)
local digitUnit, wordFigure
local result = ""
num = tostring(num)
if tonumber(t) < 1 then digitUnit = { "", "", "", "" } else digitUnit = { "", "", "", "" } end
if tonumber(t) < 1 then
wordFigure = { "", "", "", "", "", "", "", "", "", "" }
else
wordFigure = { "", "", "", "", "", "", "", "", "", "" }
end
if string.len(num) > 4 or tonumber(num) == 0 then return wordFigure[1] end
local lens = string.len(num)
for i = 1, lens do
local n = wordFigure[tonumber(string.sub(num, -i, -i)) + 1]
if n ~= wordFigure[1] then result = n .. digitUnit[i] .. result else result = n .. result end
end
result = result:gsub(wordFigure[1] .. wordFigure[1], wordFigure[1])
result = result:gsub(wordFigure[1] .. "$", "")
result = result:gsub(wordFigure[1] .. "$", "")
return result
end
-- 数值转换为中文
local function number2cnChar(num, flag, digitUnit, wordFigure) --flag=0中文小写反之为大写
local result = ""
if tonumber(flag) < 1 then
digitUnit = digitUnit or { [1] = "", [2] = "亿" }
wordFigure = wordFigure or { [1] = "", [2] = "", [3] = "", [4] = "" }
else
digitUnit = digitUnit or { [1] = "", [2] = "亿" }
wordFigure = wordFigure or { [1] = "", [2] = "", [3] = "", [4] = "" }
end
local lens = string.len(num)
if lens < 5 then
result = formatNum(num, flag)
elseif lens < 9 then
result = formatNum(string.sub(num, 1, -5), flag) .. digitUnit[1] .. formatNum(string.sub(num, -4, -1), flag)
elseif lens < 13 then
result = formatNum(string.sub(num, 1, -9), flag) ..
digitUnit[2] ..
formatNum(string.sub(num, -8, -5), flag) .. digitUnit[1] .. formatNum(string.sub(num, -4, -1), flag)
else
result = ""
end
result = result:gsub("^" .. wordFigure[1], "")
result = result:gsub(wordFigure[1] .. digitUnit[1], "")
result = result:gsub(wordFigure[1] .. digitUnit[2], "")
result = result:gsub(wordFigure[1] .. wordFigure[1], wordFigure[1])
result = result:gsub(wordFigure[1] .. "$", "")
if lens > 4 then result = result:gsub("^" .. wordFigure[2] .. wordFigure[3], wordFigure[3]) end
if result ~= "" then result = result .. wordFigure[4] else result = "数值超限!" end
return result
end
local function number2zh(num, t)
local result, wordFigure
result = ""
if tonumber(t) < 1 then
wordFigure = { "", "", "", "", "", "", "", "", "", "" }
else
wordFigure = { "", "", "", "", "", "", "", "", "", "" }
end
if tostring(num) == nil then return "" end
for pos = 1, string.len(num) do
result = result .. wordFigure[tonumber(string.sub(num, pos, pos) + 1)]
end
result = result:gsub(wordFigure[1] .. wordFigure[1], wordFigure[1])
return result:gsub(wordFigure[1] .. wordFigure[1], wordFigure[1])
end
local function number_translatorFunc(num)
local numberPart = splitNumPart(num)
local result = {}
if numberPart.dot ~= "" then
table.insert(result,
{ number2cnChar(numberPart.int, 0, { "", "亿" }, { "", "", "", "" }) .. number2zh(numberPart.dec, 0),
"" })
-- table.insert(result,
-- { number2cnChar(numberPart.int, 1, { "萬", "億" }, { "", "一", "十", "点" }) .. number2zh(numberPart.dec, 1),
-- "〔数字大写〕" })
local upperRaw = number2cnChar(numberPart.int, 1, { "", "" }, { "", "", "", "" })
local upper = upperRaw:gsub("^拾", "壹拾") .. number2zh(numberPart.dec, 1)
table.insert(result, { upper, "" })
else
table.insert(result, { number2cnChar(numberPart.int, 0, { "", "亿" }, { "", "", "", "" }), "" })
table.insert(result,
{ number2cnChar(numberPart.int, 1, { "", "" }, { "", "", "", "" }):gsub("^拾", "壹拾"), "" })
end
table.insert(result,
{ number2cnChar(numberPart.int, 0) ..
decimal_func(numberPart.dec, { [1] = "", [2] = "", [3] = "", [4] = "" },
{ [0] = "", "", "", "", "", "", "", "", "", "" }), "" })
local number2cnCharInt = number2cnChar(numberPart.int, 1)
local number2cnCharDec = decimal_func(numberPart.dec, { [1] = "", [2] = "", [3] = "", [4] = "" },
{ [0] = "", "", "", "", "", "", "", "", "", "" })
if string.len(numberPart.int) > 4 and number2cnCharInt:find('^拾[壹贰叁肆伍陆柒捌玖]?') and number2cnCharInt:find('[万亿]') then -- 简易地规避 utf8 匹配问题
local number2cnCharInt_var = number2cnCharInt:gsub('^拾', '壹拾')
table.insert(result, { number2cnCharInt_var .. number2cnCharDec, "" })
-- 会计书写要求 https://github.com/iDvel/rime-ice/issues/989
else
table.insert(result, { number2cnCharInt .. number2cnCharDec, "" })
end
local result = { result[1], result[4], result[2], result[3] }
return result
end
local function number_translator(input, seg, env)
-- 获取 recognizer/patterns/number 的第 2 个字符作为触发前缀
env.number_keyword = env.number_keyword or
env.engine.schema.config:get_string('recognizer/patterns/number'):sub(2, 2)
local str, num, numberPart
if env.number_keyword ~= '' and input:sub(1, 1) == env.number_keyword then
local context = env.engine.context
local segment = context.composition:back()
-- 设置手动排序的排序编码以支持 N 指令的手动排序
context:set_property("sequence_adjustment_code", env.number_keyword)
str = string.gsub(input, "^(%a+)", "")
numberPart = number_translatorFunc(str)
if str and #str > 0 and #numberPart > 0 then
-- 设置标签
segment.tags = segment.tags + Set({ "number" })
for i = 1, #numberPart do
yield(Candidate(input, seg.start, seg._end, numberPart[i][1], numberPart[i][2]))
end
end
end
end
-- print(#number_translatorFunc(3355.433))
return number_translator

191
lua/partial_commit.lua Normal file
View File

@@ -0,0 +1,191 @@
-- @amzxyz https://github.com/amzxyz/rime_wanxiang
-- Ctrl+1..9,0上屏首选前 N 字;按 preedit/script_text 的前 N 音节对齐 raw input
local wanxiang = require("wanxiang")
local M = {}
-- 数字键映射(主键盘 + 小键盘)
local DIGIT = { [0x31]=1,[0x32]=2,[0x33]=3,[0x34]=4,[0x35]=5,[0x36]=6,[0x37]=7,[0x38]=8,[0x39]=9,[0x30]=10 }
local KP = { [0xFFB1]=1,[0xFFB2]=2,[0xFFB3]=3,[0xFFB4]=4,[0xFFB5]=5,[0xFFB6]=6,[0xFFB7]=7,[0xFFB8]=8,[0xFFB9]=9,[0xFFB0]=10 }
-- 工具:字符串缩略 / 获取分隔符 / 安全转义 / 清洗 raw
local function short(s)
if not s then return "" end
if #s > 120 then
return s:sub(1, 117) .. "..."
end
return s
end
local function get_delimiters(ctx)
local cfg = ctx.engine and ctx.engine.schema and ctx.engine.schema.config
local delimiter = (cfg and cfg:get_string("speller/delimiter")) or " '"
return delimiter:sub(1, 1), delimiter:sub(2, 2) -- auto, manual
end
-- 放进字符类 [...] 使用的转义(只转义 % ^ ] -
local function esc_class(c)
if not c or c == "" then return "" end
return (c:gsub("([%%%^%]%-])", "%%%1"))
end
-- 普通模式串位置的单字符转义(最小化:仅非字母数字下划线时转义)
local function esc_pat(c)
if not c or c == "" then return "" end
if c:match("[%w_]") then return c end
return (c:gsub("(%W)", "%%%1"))
end
-- 清洗整串 raw去掉手动分隔符如 "'"
local function clean_raw(ctx, raw)
if not raw or raw == "" then return "" end
local _, manual = get_delimiters(ctx)
if manual and #manual == 1 then
raw = raw:gsub(esc_pat(manual), "")
end
return raw
end
-- 取候选前 n 个字符
local function utf8_head(s, n)
local i, c = 1, 0
while i <= #s and c < n do
local b = s:byte(i)
i = i + ((b < 0x80) and 1 or ((b < 0xE0) and 2 or ((b < 0xF0) and 3 or 4)))
c = c + 1
end
return s:sub(1, i - 1)
end
-- 生成 target按分隔符切 preedit/script_text取前 n 个并去分隔符拼接
local function script_prefix(ctx, n)
local raw_in = ctx.input or ""
local prop_key = ctx:get_property("sequence_preedit_key") or ""
local prop_val = ctx:get_property("sequence_preedit_val") or ""
local script_txt = ctx:get_script_text() or ""
local s
if prop_key == raw_in and prop_val ~= "" then
s = prop_val
else
s = script_txt
end
if s == "" then return "" end
local auto, manual = get_delimiters(ctx)
local pat = "[^" .. esc_class(auto) .. esc_class(manual) .. "%s]+"
local parts = {}
for w in s:gmatch(pat) do parts[#parts + 1] = w end
if #parts == 0 then return "" end
local upto = math.min(n, #parts)
local target = table.concat({ table.unpack(parts, 1, upto) }, "")
return target
end
-- 对齐“去分隔符后的 raw_clean”与 target返回消耗长度基于 raw_clean
local function eat_len_by_target(ctx, target)
if target == "" then return 0 end
local raw = ctx.input or ""
if raw == "" then return 0 end
local clean = clean_raw(ctx, raw)
local i, j, Lc, Lt = 1, 1, #clean, #target
while i <= Lc and j <= Lt do
if clean:sub(i, i) ~= target:sub(j, j) then
return 0
end
i, j = i + 1, j + 1
end
if j <= Lt then return 0 end
return i - 1
end
local function set_pending(env, rest)
env._cpc_pending_rest = rest or ""
end
local function has_pending(env)
return type(env._cpc_pending_rest) == "string" and env._cpc_pending_rest ~= nil
end
local function take_pending(env)
local r = env._cpc_pending_rest
env._cpc_pending_rest = nil
return r
end
function M.init(env)
local ctx = env.engine.context
env._cpc_update_conn = ctx.update_notifier:connect(function(c)
if not has_pending(env) then return end
local rest = take_pending(env) or ""
c.input = rest
if c.clear_non_confirmed_composition then
c:clear_non_confirmed_composition()
end
if c.caret_pos ~= nil then
c.caret_pos = #rest
end
end)
env._cpc_key_handler = function(key)
if not key:ctrl() or key:release() then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
local n = DIGIT[key.keycode] or KP[key.keycode]
if not n then return wanxiang.RIME_PROCESS_RESULTS.kNoop end
local c = env.engine.context
if not c:is_composing() then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
local cand = c:get_selected_candidate() or c:get_candidate(0)
if not cand or not cand.text or #cand.text == 0 then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
local head = utf8_head(cand.text, n)
if head == "" then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
local target = script_prefix(c, n)
if target == "" then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
local consumed = eat_len_by_target(c, target)
if consumed == 0 then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
local raw_clean = clean_raw(c, c.input or "")
local rest = raw_clean:sub(consumed + 1)
env.engine:commit_text(head)
set_pending(env, rest)
c:refresh_non_confirmed_composition()
return wanxiang.RIME_PROCESS_RESULTS.kAccepted
end
end
function M.fini(env)
if env._cpc_update_conn then
env._cpc_update_conn:disconnect()
env._cpc_update_conn = nil
end
env._cpc_key_handler = nil
end
function M.func(key, env)
if not env._cpc_key_handler then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
return env._cpc_key_handler(key)
end
return M

137
lua/quick_symbol_text.lua Normal file
View File

@@ -0,0 +1,137 @@
-- 欢迎使用万象拼音方案quick_symbol_text
-- @amzxyz
-- https://github.com/amzxyz/rime_wanxiang
-- 触发:由 schema.yaml -> quick_symbol_text/trigger 加载(默认 ^([a-z])/$
-- a/、b/ ... 单字母触发预设编码自动上屏;值可设为 "repeat" 实现重复上屏上一条提交内容
-- custom>schema>lua 合并键值(仅合并单字母 a-z 键)
local wanxiang = require("wanxiang")
-- 读取 symkey
local function load_mapping_from_config(config)
local symbol_map = {}
local ok_map, map = pcall(function() return config:get_map("quick_symbol_text/symkey") end)
if not ok_map or not map then return symbol_map end
local ok_keys, keys = pcall(function() return map:keys() end)
if not ok_keys or not keys then return symbol_map end
for _, key in ipairs(keys) do
local v = config:get_string("quick_symbol_text/symkey/" .. key)
if v ~= nil then
symbol_map[string.lower(tostring(key))] = v
end
end
return symbol_map
end
-- 读取 trigger
local function load_trigger_from_config(config)
local default_pat = "^([a-z])/$"
if not config then return default_pat end
local ok, s = pcall(function() return config:get_string("quick_symbol_text/trigger") end)
if ok and type(s) == "string" and #s > 0 then return s end
return default_pat
end
-- 默认单字母映射
local default_mapping = {
q = "",
w = "",
e = "",
r = "",
t = "~",
y = "·",
u = "",
i = "",
o = "",
p = "",
a = "",
s = "……",
d = "",
f = "",
g = "",
h = "",
j = "",
k = "",
l = "",
z = "",
x = "",
c = "",
v = "——",
b = "%",
n = "",
m = "",
}
local function init(env)
local config = env.engine.schema.config
env.single_symbol_pattern = load_trigger_from_config(config)
-- 默认表
env.mapping = {}
for k, v in pairs(default_mapping) do
if #k == 1 and k:match("^[a-z]$") then env.mapping[k] = v end
end
-- 覆盖(仅单字母)
local custom = load_mapping_from_config(config)
for k, v in pairs(custom) do
local key = tostring(k):lower()
if #key == 1 and key:match("^[a-z]$") then
env.mapping[key] = v -- ""=禁用;"repeat"=特殊语义
end
end
env.last_commit_text = "欢迎使用万象拼音!"
-- 记录上屏文本(供 repeat
env.quick_symbol_text_commit_notifier =
env.engine.context.commit_notifier:connect(function(ctx)
local t = ctx:get_commit_text()
if t ~= "" then env.last_commit_text = t end
end)
-- 命中触发则上屏并清空
env.quick_symbol_text_update_notifier =
env.engine.context.update_notifier:connect(function(context)
local input = context.input or ""
local key = string.match(input, env.single_symbol_pattern)
if not key then return end
key = string.lower(key)
local symbol = env.mapping[key]
if symbol == nil or symbol == "" then return end -- 未配置/禁用
if type(symbol) == "string" and symbol:lower() == "repeat" then
if env.last_commit_text ~= "" then
env.engine:commit_text(env.last_commit_text)
context:clear()
end
else
env.engine:commit_text(symbol)
context:clear()
end
end)
end
local function fini(env)
if env.quick_symbol_text_commit_notifier then
env.quick_symbol_text_commit_notifier:disconnect()
env.quick_symbol_text_commit_notifier = nil
end
if env.quick_symbol_text_update_notifier then
env.quick_symbol_text_update_notifier:disconnect()
env.quick_symbol_text_update_notifier = nil
end
end
-- 命中时吃键,避免后续流程处理
local function processor(key_event, env)
local input = env.engine.context.input or ""
local key = string.match(input, env.single_symbol_pattern)
if key then
key = string.lower(key)
local symbol = env.mapping[key]
if symbol ~= nil and symbol ~= "" then
return wanxiang.RIME_PROCESS_RESULTS.kAccepted
end
end
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
return { init = init, fini = fini, func = processor }

41
lua/select_character.lua Normal file
View File

@@ -0,0 +1,41 @@
-- 以词定字
local wanxiang = require("wanxiang")
local select = {}
function select.init(env)
local config = env.engine.schema.config
select.first_key = config:get_string('key_binder/select_first_character')
select.last_key = config:get_string('key_binder/select_last_character')
end
function select.func(key, env)
local engine = env.engine
local context = env.engine.context
if
not key:release()
and (context:is_composing() or context:has_menu())
and (select.first_key or select.last_key)
then
local text = context.input
if context:get_selected_candidate() then
text = context:get_selected_candidate().text
end
if utf8.len(text) > 1 then
if (key:repr() == select.first_key) then
engine:commit_text(text:sub(1, utf8.offset(text, 2) - 1))
context:clear()
return wanxiang.RIME_PROCESS_RESULTS.kAccepted
elseif (key:repr() == select.last_key) then
engine:commit_text(text:sub(utf8.offset(text, -1)))
context:clear()
return wanxiang.RIME_PROCESS_RESULTS.kAccepted
end
end
end
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
return select

185
lua/set_schema.lua Normal file
View File

@@ -0,0 +1,185 @@
--https://github.com/amzxyz/rime_wanxiang
--@amzxyz
--一个快速初始化方案类型的工具,使用方法,方案文件放进用户目录后先部署,再执行相关指令后重新部署完成切换
local wanxiang = require("wanxiang")
-- 文件复制函数
local function copy_file(src, dest)
local fi = io.open(src, "r")
if not fi then
return false
end
local content = fi:read("*a")
fi:close()
local fo = io.open(dest, "w")
if not fo then
return false
end
fo:write(content)
fo:close()
return true
end
-- 替换方案函数(根据文件名应用特定替换模式)
local function replace_schema(file_path, target_schema)
local f = io.open(file_path, "r")
if not f then
return false
end
local content = f:read("*a")
f:close()
-- 根据文件名决定替换模式
if file_path:find("wanxiang_reverse") then
-- 把 "__include: wanxiang_reverse.schema:/"(含可选后缀)改成 "__include: wanxiang_algebra:/mixed/"
content = content:gsub("(%-?%s*__include:%s*)wanxiang_reverse%.schema:/[^%s\r\n]*", "%1wanxiang_algebra:/reverse/" .. target_schema)
-- "__patch: wanxiang_reverse.schema:/hspzn" -> "__patch: wanxiang_algebra:/reverse/hspzn"
content = content:gsub("(%-?%s*__patch:%s*)wanxiang_reverse%.schema:/([^%s\r\n]+)", "%1wanxiang_algebra:/reverse/%2")
content = content:gsub("([%s]*__include:%s*wanxiang_algebra:/reverse/)%S+", "%1" .. target_schema)
elseif file_path:find("wanxiang_mixedcode") then
-- "__include: wanxiang_mixedcode.schema:/全拼"
-- -> "__include: wanxiang_algebra:/mixed/通用派生规则"
-- "__patch: wanxiang_algebra:/mixed/全拼"
content = content:gsub(
"(%-?%s*)__include:%s*wanxiang_mixedcode%.schema:/全拼",
function(lead)
return lead .. "__include: wanxiang_algebra:/mixed/通用派生规则\n"
.. lead .. "__patch: wanxiang_algebra:/mixed/全拼"
end
)
content = content:gsub("([%s]*__patch:%s*wanxiang_algebra:/mixed/)%S+", "%1" .. target_schema)
elseif file_path:find("wanxiang%.custom") or file_path:find("wanxiang_pro%.custom") then
-- 先把旧前缀整体替换为新前缀
-- "- wanxiang.schema:/" -> "- wanxiang_algebra:/base/"
-- "- wanxiang_pro.schema:/" -> "- wanxiang_algebra:/pro/"
content = content:gsub("(%-+%s*)wanxiang%.schema:/", "%1wanxiang_algebra:/base/")
content = content:gsub("(%-+%s*)wanxiang_pro%.schema:/", "%1wanxiang_algebra:/pro/")
-- 再将 base/pro 后面的 schema 名替换为 target_schema
content = content:gsub("([%s%-]*wanxiang_algebra:/pro/)%S+", "%1" .. target_schema, 1)
content = content:gsub("([%s%-]*wanxiang_algebra:/base/)%S+", "%1" .. target_schema, 1)
end
f = io.open(file_path, "w")
if not f then
return false
end
f:write(content)
f:close()
return true
end
-- translator 主函数
local function translator(input, seg, env)
if input == "/zjf" or input == "/jjf" then
local target_aux = (input == "/zjf") and "直接辅助" or "间接辅助"
local user_dir = rime_api.get_user_data_dir()
local paths = {
user_dir .. "/wanxiang_pro.custom.yaml",
user_dir .. "/wanxiang.custom.yaml",
}
local total_hits, touched = 0, 0
for _, p in ipairs(paths) do
local f = io.open(p, "r")
if f then
local content = f:read("*a"); f:close()
-- 两次 gsub 都要接收“新文本 + 命中次数”
local n1, n2 = 0, 0
content, n1 = content:gsub("(%-+%s*wanxiang_algebra:/pro/)直接辅助(%s*#?.*)", "%1" .. target_aux .. "%2")
content, n2 = content:gsub("(%-+%s*wanxiang_algebra:/pro/)间接辅助(%s*#?.*)", "%1" .. target_aux .. "%2")
local n = n1 + n2
if n > 0 then
local w = io.open(p, "w")
if w then w:write(content); w:close() end
total_hits = total_hits + n
touched = touched + 1
end
end
end
local msg = (total_hits > 0)
and ("已切换到〔" .. target_aux .. "〕,请重新部署")
or "未找到可切换的条目"
yield(Candidate("switch", seg.start, seg._end, msg, ""))
return
end
local schema_map = {
["/flypy"] = "小鹤双拼",
["/mspy"] = "微软双拼",
["/zrm"] = "自然码",
["/sogou"] = "搜狗双拼",
["/znabc"] = "智能ABC",
["/ziguang"] = "紫光双拼",
["/pyjj"] = "拼音加加",
["/gbpy"] = "国标双拼",
["/lxsq"] = "乱序17",
["/zrlong"] = "自然龙",
["/hxlong"] = "汉心龙",
["/pinyin"] = "全拼",
}
local target_schema = schema_map[input]
if target_schema then
local user_dir = rime_api.get_user_data_dir()
-- 检查根目录是否存在自定义文件
local pro_file = user_dir .. "/wanxiang_pro.custom.yaml"
local normal_file = user_dir .. "/wanxiang.custom.yaml"
local pro_exists = io.open(pro_file, "r")
local normal_exists = io.open(normal_file, "r")
local custom_file_exists = false
if pro_exists or normal_exists then
custom_file_exists = true
if pro_exists then pro_exists:close() end
if normal_exists then normal_exists:close() end
end
local files = {
"wanxiang_mixedcode.custom.yaml",
"wanxiang_reverse.custom.yaml"
}
-- 判断是否为专业版
local is_pro = wanxiang.is_pro_scheme(env)
local fourth_file = is_pro and "wanxiang_pro.custom.yaml" or "wanxiang.custom.yaml"
table.insert(files, fourth_file)
for _, name in ipairs(files) do
local src = user_dir .. "/custom/" .. name
local dest = user_dir .. "/" .. name
if name == fourth_file and custom_file_exists then
-- 根目录自定义文件已存在,不复制,但依然修改
replace_schema(dest, target_schema)
else
-- 其他文件: 若 custom 目录存在文件,则复制到根目录并修改
local src_file = io.open(src, "r")
if src_file then
src_file:close()
if copy_file(src, dest) then
replace_schema(dest, target_schema)
end
end
end
end
-- 返回提示候选
if custom_file_exists then
yield(Candidate("switch", seg.start, seg._end, "检测到已有自定义文件,已为您切换到〔" .. target_schema .. "〕,请手动重新部署", ""))
else
yield(Candidate("switch", seg.start, seg._end, "已帮您复制并切换到〔" .. target_schema .. "〕,请手动重新部署", ""))
end
end
end
return translator

3017
lua/shijian.lua Normal file

File diff suppressed because it is too large Load Diff

3491
lua/super_calculator.lua Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,535 @@
--@amzxyz https://github.com/amzxyz/rime_wanxiang
local wanxiang = require('wanxiang')
local tone_map = {
['ā']='a', ['á']='a', ['ǎ']='a', ['à']='a',
['ē']='e', ['é']='e', ['ě']='e', ['è']='e',
['ī']='i', ['í']='i', ['ǐ']='i', ['ì']='i',
['ō']='o', ['ó']='o', ['ǒ']='o', ['ò']='o', ['ň']='n',
['ū']='u', ['ú']='u', ['ǔ']='u', ['ù']='u', ['ǹ']='n',
['ǖ']='ü', ['ǘ']='ü', ['ǚ']='ü', ['ǜ']='ü', ['ń']='n',
}
local function remove_pinyin_tone(s)
local result = {}
for uchar in s:gmatch("[%z\1-\127\194-\244][\128-\191]*") do
table.insert(result, tone_map[uchar] or uchar)
end
return table.concat(result)
end
-- ----------------------
-- # 辅助码拆分提示模块
-- PRO 专用
-- ----------------------
local CF = {}
function CF.init(env)
if wanxiang.is_pro_scheme(env) then -- pro 版直接初始化
CF.get_dict(env)
end
end
function CF.fini(env)
env.chaifen_dict = nil
collectgarbage()
end
function CF.get_dict(env)
if env.chaifen_dict == nil then
env.chaifen_dict = ReverseLookup("wanxiang_chaifen")
end
return env.chaifen_dict
end
function CF.get_comment(cand, env)
local dict = CF.get_dict(env)
if not dict then return "" end
local raw = dict:lookup(cand.text)
if not raw or raw == "" then return "" end
local tpl = (env and env.settings and env.settings.chaifen) or ""
if tpl ~= "" then
-- 取 chaifen 左右两边
local left, right = tpl:match("^(.-)chaifen(.-)$")
if left then
return left .. raw .. right
end
end
return raw
end
-- ----------------------
-- # 错音错字提示模块
-- ----------------------
local CR = {}
local corrections_cache = nil -- 用于缓存已加载的词典
function CR.init(env)
CR.style = env.settings.corrector_type or '{comment}'
--if corrections_cache then return end
local auto_delimiter = env.settings.auto_delimiter
local is_pro = wanxiang.is_pro_scheme(env)
-- 根据方案选择加载路径
local path = (is_pro and "dicts/cuoyin.pro.dict.yaml") or "dicts/cuoyin.dict.yaml"
local file, close_file, err = wanxiang.load_file_with_fallback(path)
if not file then
log.error(string.format("[super_comment]: 加载失败 %s错误: %s", path, err))
return
end
corrections_cache = {}
for line in file:lines() do
if not line:match("^#") then
local text, code, weight, comment = line:match("^(.-)\t(.-)\t(.-)\t(.-)$")
if text and code then
text = text:match("^%s*(.-)%s*$")
code = code:match("^%s*(.-)%s*$")
comment = comment and comment:match("^%s*(.-)%s*$") or ""
comment = comment:gsub("%s+", auto_delimiter)
code = code:gsub("%s+", auto_delimiter)
corrections_cache[code] = { text = text, comment = comment }
end
end
end
close_file()
end
function CR.get_comment(cand)
local correction = corrections_cache and corrections_cache[cand.comment] or nil
if not (correction and cand.text == correction.text) then
return nil
end
-- 只认占位符 `comment`,按“刀法”切分
local tpl = CR.style or "comment"
local left, right = tpl:match("^(.-)comment(.-)$")
if left then
return left .. correction.comment .. right
else
return correction.comment
end
end
-- ----------------------
-- 部件组字返回的注释
-- ----------------------
---@return string
local function get_az_comment(_, env, initial_comment)
if not initial_comment or initial_comment == "" then return "〔无〕" end
local final_comment = nil
local auto_delimiter = env.settings.auto_delimiter or " "
-- 拆分初始评论为多个段落
local segments = {}
for segment in initial_comment:gmatch("[^%s]+") do
table.insert(segments, segment)
end
local semicolon_count = select(2, segments[1]:gsub(";", "")) -- 使用第一个段来判断分号的数量
local pinyins = {}
local fuzhu = nil
for _, segment in ipairs(segments) do
local pinyin = segment:match("^[^;~]+")
local fz = nil
if semicolon_count == 1 then
-- 一个分号:取后段
fz = segment:match(";(.+)$")
else
-- 无分号不取辅助码
fz = nil
end
if pinyin then table.insert(pinyins, pinyin) end
if not fuzhu and fz and fz ~= "" then fuzhu = fz end
end
-- 拼接结果
if #pinyins > 0 then
local pinyin_str = table.concat(pinyins, ",")
if fuzhu then
final_comment = string.format("〔音%s 辅%s", pinyin_str, fuzhu)
else
final_comment = string.format("〔音%s", pinyin_str)
end
end
return final_comment or "〔无〕"
end
-- ----------------------
-- # 辅助码提示或带调全拼注释模块 (Fuzhu)
-- ----------------------
local function get_fz_comment(cand, env, initial_comment)
local length = utf8.len(cand.text)
if length > env.settings.candidate_length then
return ""
end
local auto_delimiter = env.settings.auto_delimiter or " "
local segments = {}
for segment in string.gmatch(initial_comment, "[^" .. auto_delimiter .. "]+") do
table.insert(segments, segment)
end
-- 根据 option 动态决定是否强制使用 tone
local use_tone = env.engine.context:get_option("tone_hint")
local fuzhu_type = use_tone and "tone" or "fuzhu"
local first_segment = segments[1] or ""
local semicolon_count = select(2, first_segment:gsub(";", ""))
local fuzhu_comments = {}
-- 没有分号的情况
if semicolon_count == 0 then
return initial_comment:gsub(auto_delimiter, " ")
else
-- 有分号:按类型提取
for _, segment in ipairs(segments) do
if fuzhu_type == "tone" then
-- 取第一个分号“前”的内容
local before = segment:match("^(.-);")
if before and before ~= "" then
table.insert(fuzhu_comments, before)
end
else -- "fuzhu"
-- 取第一个分号“后”的内容(到行尾)
local after = segment:match(";(.+)$")
if after and after ~= "" then
table.insert(fuzhu_comments, after)
end
end
end
end
-- 最终拼接输出fuzhu用 `,`tone用 /连接
if #fuzhu_comments > 0 then
if fuzhu_type == "tone" then
return table.concat(fuzhu_comments, " ")
else
return table.concat(fuzhu_comments, "/")
end
else
return ""
end
end
local SV = {}
-- 工具:取光标前的编码(安全处理 caret 越界)
local function front_input(ctx)
if not ctx then return "" end
local raw_full = ctx.input or ""
local caret = ctx.caret_pos or #raw_full
if caret < 0 then
caret = 0
elseif caret > #raw_full then
caret = #raw_full
end
return raw_full:sub(1, caret)
end
-- 这个模块主要用于将滤镜阶段未修改前的注释或者 preedit
-- 存到上下文变量里按键处理阶段使用update_notifier 保证一致性
function SV.init(env)
env._sv_seq_sig = ""
env._sv_last_pre = "" -- 最近一次要写入的 preedit
env._saved_input_for_seq = "" -- 上次对应的 raw_in光标前编码
local ctx = env.engine.context
env._sv_ctx_conn = ctx.update_notifier:connect(function(c)
local raw_in = front_input(c)
local pre = env._sv_last_pre or ""
if pre == "" or raw_in == "" then
return
end
-- 不重写:光标前编码 + preedit
local sig = raw_in .. "\t" .. pre
if env._sv_seq_sig == sig then
return
end
c:set_property("sequence_preedit_key", raw_in)
c:set_property("sequence_preedit_val", pre)
env._sv_seq_sig = sig
end)
end
-- 断开 notifier清理状态
function SV.fini(env)
if env._sv_ctx_conn then
env._sv_ctx_conn:disconnect()
env._sv_ctx_conn = nil
end
env._sv_seq_sig = nil
env._sv_last_pre = nil
env._saved_input_for_seq = nil
end
-- 限制更新范围:同一个 raw_in 只记第一次的 preedit
function SV.update_preedit(env, preedit)
local ctx = env.engine.context
if not ctx then return end
local raw_in = front_input(ctx)
preedit = preedit or ""
if raw_in == "" or preedit == "" then
return
end
if env._saved_input_for_seq ~= raw_in then
env._saved_input_for_seq = raw_in
env._sv_last_pre = preedit
end
end
-- 对 cand.preedit 应用 tone_preedit/0..9 的映射(数字 -> 上标等)
local function apply_tone_preedit(env, cand)
if not cand or not cand.preedit or cand.preedit == "" then
return
end
-- 用 context.input 判断是否有相邻数字
local input
local engine = env.engine
if engine and engine.context then
-- Rime 里一般是 string保险起见兜个 nil
input = engine.context.input or ""
end
-- 如果整条输入串中存在相邻两个数字(例如 "li39"、"abc10" 等),
-- 则整体不做任何转换,直接返回,为了配合小键盘输入逻辑中包吃书字面大小一致性
if input and input ~= "" and input:match("%d%d") then
return
end
-- 懒加载 tone_map
if not env.tone_map then
env.tone_map = {}
local cfg = engine and engine.schema and engine.schema.config
if cfg then
for d = 0, 9 do
local k = tostring(d)
local v = cfg:get_string("tone_preedit/" .. k)
if v and v ~= "" then
env.tone_map[k] = v
end
end
end
end
local preedit = cand.preedit
local converted = preedit:gsub("([^%d%s]+)(%d+)", function(body, digits)
local mapped = digits:gsub("%d", function(d)
return env.tone_map and env.tone_map[d] or d
end)
return body .. mapped
end)
if converted ~= preedit then
cand.preedit = converted
end
end
-- ----------------------
-- 主函数根据优先级处理候选词的注释和preedit
-- ----------------------
local ZH = {}
function ZH.init(env)
local config = env.engine.schema.config
local delimiter = config:get_string('speller/delimiter') or " '"
local auto_delimiter = delimiter:sub(1, 1)
local manual_delimiter = delimiter:sub(2, 2)
env.settings = {
delimiter = delimiter,
auto_delimiter = auto_delimiter,
manual_delimiter = manual_delimiter,
corrector_enabled = config:get_bool("super_comment/corrector") or true,
corrector_type = config:get_string("super_comment/corrector_type") or "{comment}",
chaifen = config:get_string("super_comment/chaifen") or "chaifen",
candidate_length = tonumber(config:get_string("super_comment/candidate_length")) or 1,
}
CR.init(env)
SV.init(env)
end
function ZH.fini(env)
-- 清理
CF.fini(env)
SV.fini(env)
end
function ZH.func(input, env)
local config = env.engine.schema.config
local context = env.engine.context
local input_str = context.input
local is_radical_mode = wanxiang.is_in_radical_mode(env)
local schema_id = env.engine.schema.schema_id or ""
local is_wanxiang_pro = (schema_id == "wanxiang_pro")
local should_skip_candidate_comment = wanxiang.is_function_mode_active(context) or input_str == ""
local is_tone_comment = env.engine.context:get_option("tone_hint")
local is_comment_hint = env.engine.context:get_option("fuzhu_hint")
local is_chaifen_enabled = env.engine.context:get_option("chaifen_switch")
--preedit相关声明
local delimiter = env.settings.delimiter
local auto_delimiter = env.settings.auto_delimiter
local manual_delimiter = env.settings.manual_delimiter
local visual_delim = config:get_string("speller/visual_delimiter") or " "
local tone_isolate = config:get_bool("speller/tone_isolate")
local is_tone_display = context:get_option("tone_display")
local is_full_pinyin = context:get_option("full_pinyin")
local index = 0
-- auto_phrase 相关声明
local enable_auto_phrase = config:get_bool("add_user_dict/enable_auto_phrase") or false
local enable_user_dict = config:get_bool("add_user_dict/enable_user_dict") or false
for cand in input:iter() do
local genuine_cand = cand:get_genuine()
local preedit = genuine_cand.preedit or ""
local initial_comment = genuine_cand.comment
local final_comment = initial_comment
index = index + 1
SV.update_preedit(env, preedit) --储存到环境变量
-- preedit相关处理只跳过 preedit不影响注释
if is_radical_mode then
goto after_preedit
end
if not is_tone_display and not is_full_pinyin then
goto after_preedit
end
if (not initial_comment or initial_comment == "") then
goto after_preedit
end
do
-- 拆分 preedit
local input_parts = {}
local current_segment = ""
for i = 1, #preedit do
local char = preedit:sub(i, i)
if char == auto_delimiter or char == manual_delimiter then
if #current_segment > 0 then
table.insert(input_parts, current_segment)
current_segment = ""
end
table.insert(input_parts, char)
else
current_segment = current_segment .. char
end
end
if #current_segment > 0 then
table.insert(input_parts, current_segment)
end
-- 拆分拼音段comment
local pinyin_segments = {}
for segment in string.gmatch(initial_comment, "[^" .. auto_delimiter .. manual_delimiter .. "]+") do
local pinyin = segment:match("^[^;]+")
if pinyin then
pinyin = pinyin:gsub("[%[%]]", "") --去掉英文词库编码中的[]
table.insert(pinyin_segments, pinyin)
end
end
-- 替换逻辑
local pinyin_index = 1
for i, part in ipairs(input_parts) do
if part == auto_delimiter or part == manual_delimiter then
input_parts[i] = visual_delim
else
local body, tone = part:match("([%a]+)([^%a]+)") --后面加号很必要
local py = pinyin_segments[pinyin_index]
if py then
if is_wanxiang_pro then
input_parts[i] = py
pinyin_index = pinyin_index + 1
elseif i == #input_parts and #part == 1 then
local prefix = py:sub(1, 2)
local first_char = part:sub(1,1):lower()
if first_char == "s" or first_char == "c" or first_char == "z" then
input_parts[i] = part
else
if prefix == "zh" or prefix == "ch" or prefix == "sh" then
input_parts[i] = prefix
else
input_parts[i] = part
end
end
else
if tone_isolate then
input_parts[i] = py .. (tone or "")
else
input_parts[i] = py
end
pinyin_index = pinyin_index + 1
end
end
end
end
if is_full_pinyin then
for idx, part in ipairs(input_parts) do
input_parts[idx] = remove_pinyin_tone(part)
end
end
genuine_cand.preedit = table.concat(input_parts)
end
::after_preedit::
apply_tone_preedit(env, genuine_cand)
if should_skip_candidate_comment then
yield(genuine_cand)
goto continue
end
-- 进入注释处理阶段
-- ① 辅助码注释或者声调注释
if is_comment_hint then
local fz_comment = get_fz_comment(cand, env, initial_comment)
if fz_comment then
final_comment = fz_comment
end
elseif is_tone_comment then
local fz_comment = get_fz_comment(cand, env, initial_comment)
if fz_comment then
final_comment = fz_comment
end
else
final_comment = ""
end
-- ② 拆分注释
if is_chaifen_enabled then
local cf_comment = CF.get_comment(cand, env)
if cf_comment and cf_comment ~= "" then --不为空很重要
final_comment = cf_comment
end
end
-- ③ 错音错字提示
if env.settings.corrector_enabled then
local cr_comment = CR.get_comment(cand)
if cr_comment and cr_comment ~= "" then
final_comment = cr_comment
end
end
-- ④ 反查模式提示
if is_radical_mode then
local az_comment = get_az_comment(cand, env, initial_comment)
if az_comment and az_comment ~= "" then
final_comment = az_comment
end
end
-- 应用注释
if final_comment ~= initial_comment then
genuine_cand.comment = final_comment
end
yield(genuine_cand)
::continue::
end
end
return ZH

1030
lua/super_filter.lua Normal file

File diff suppressed because it is too large Load Diff

589
lua/super_lookup.lua Normal file
View File

@@ -0,0 +1,589 @@
--@amzxyz https://github.com/amzxyz/rime_wanxiang
--wanxiang_lookup: #设置归属于super_lookup.lua
--tags: [ abc ] # 检索当前tag的候选
--key: "`" # 输入中反查引导符,要添加到 speller/alphabet
--lookup: [ wanxiang_reverse ] #反查滤镜数据库,万象都合并为一个了
-- 获取 wanxiang 模块
local function get_wanxiang()
local ok, mod = pcall(function() return require('wanxiang') end)
if ok and type(mod) == 'table' then return mod end
if type(_G.wanxiang) == 'table' then return _G.wanxiang end
return nil
end
-- 各输入法类型对应的转换规则
-- flypy/mspy/sogou/abc/ziguang/pyjj/gbpy/lxsq/zrlong/hxlong
local LOCAL_PROJECTION_RULES = {
-- 全拼pinyin
pinyin = {
"xform/'//",
"derive/^([nl])ue$/$1ve/",
"derive/'([nl])ue$/'$1ve/",
"derive/^([jqxy])u/$1v/",
"derive/'([jqxy])u/'$1v/",
},
-- 自然码zrm
zrm = {
"derive/^([jqxy])u(?=^|$|')/$1v/",
"derive/'([jqxy])u(?=^|$|')/'$1v/",
"derive/^([aoe])([ioun])(?=^|$|')/$1$1$2/",
"derive/'([aoe])([ioun])(?=^|$|')/'$1$1$2/",
"xform/^([aoe])(ng)?(?=^|$|')/$1$1$2/",
"xform/'([aoe])(ng)?(?=^|$|')/'$1$1$2/",
"xform/iu(?=^|$|')/<q>/",
"xform/[iu]a(?=^|$|')/<w>/",
"xform/[uv]an(?=^|$|')/<r>/",
"xform/[uv]e(?=^|$|')/<t>/",
"xform/ing(?=^|$|')|uai(?=^|$|')/<y>/",
"xform/^sh/<u>/",
"xform/^ch/<i>/",
"xform/^zh/<v>/",
"xform/'sh/'<u>/",
"xform/'ch/'<i>/",
"xform/'zh/'<v>/",
"xform/uo(?=^|$|')/<o>/",
"xform/[uv]n(?=^|$|')/<p>/",
"xform/([a-z>])i?ong(?=^|$|')/$1<s>/",
"xform/[iu]ang(?=^|$|')/<d>/",
"xform/([a-z>])en(?=^|$|')/$1<f>/",
"xform/([a-z>])eng(?=^|$|')/$1<g>/",
"xform/([a-z>])ang(?=^|$|')/$1<h>/",
"xform/ian(?=^|$|')/<m>/",
"xform/([a-z>])an(?=^|$|')/$1<j>/",
"xform/iao(?=^|$|')/<c>/",
"xform/([a-z>])ao(?=^|$|')/$1<k>/",
"xform/([a-z>])ai(?=^|$|')/$1<l>/",
"xform/([a-z>])ei(?=^|$|')/$1<z>/",
"xform/ie(?=^|$|')/<x>/",
"xform/ui(?=^|$|')/<v>/",
"xform/([a-z>])ou(?=^|$|')/$1<b>/",
"xform/in(?=^|$|')/<n>/",
"xform/'|<|>//",
},
-- 小鹤flypy
flypy = {
"derive/^([jqxy])u(?=^|$|')/$1v/",
"derive/'([jqxy])u(?=^|$|')/'$1v/",
"derive/^([aoe])([ioun])(?=^|$|')/$1$1$2/",
"derive/'([aoe])([ioun])(?=^|$|')/'$1$1$2/",
"xform/^([aoe])(ng)?(?=^|$|')/$1$1$2/",
"xform/'([aoe])(ng)?(?=^|$|')/'$1$1$2/",
"xform/iu(?=^|$|')/<q>/",
"xform/(.)ei(?=^|$|')/$1<w>/",
"xform/uan(?=^|$|')/<r>/",
"xform/[uv]e(?=^|$|')/<t>/",
"xform/un(?=^|$|')/<y>/",
"xform/^sh/<u>/",
"xform/^ch/<i>/",
"xform/^zh/<v>/",
"xform/'sh/'<u>/",
"xform/'ch/'<i>/",
"xform/'zh/'<v>/",
"xform/uo(?=^|$|')/<o>/",
"xform/ie(?=^|$|')/<p>/",
"xform/([a-z>])i?ong(?=^|$|')/$1<s>/",
"xform/ing(?=^|$|')|uai(?=^|$|')/<k>/",
"xform/([a-z>])ai(?=^|$|')/$1<d>/",
"xform/([a-z>])en(?=^|$|')/$1<f>/",
"xform/([a-z>])eng(?=^|$|')/$1<g>/",
"xform/[iu]ang(?=^|$|')/<l>/",
"xform/([a-z>])ang(?=^|$|')/$1<h>/",
"xform/ian(?=^|$|')/<m>/",
"xform/([a-z>])an(?=^|$|')/$1<j>/",
"xform/([a-z>])ou(?=^|$|')/$1<z>/",
"xform/[iu]a(?=^|$|')/<x>/",
"xform/iao(?=^|$|')/<n>/",
"xform/([a-z>])ao(?=^|$|')/$1<c>/",
"xform/ui(?=^|$|')/<v>/",
"xform/in(?=^|$|')/<b>/",
"xform/'|<|>//",
},
-- 微软mspy
mspy = {
"derive/^([jqxy])u(?=^|$|')/$1v/",
"derive/'([jqxy])u(?=^|$|')/'$1v/",
"derive/^([aoe].*)(?=^|$|')/o$1/",
"derive/'([aoe].*)(?=^|$|')/'o$1/",
"xform/^([ae])(.*)(?=^|$|')/$1$1$2/",
"xform/'([ae])(.*)(?=^|$|')/'$1$1$2/",
"xform/iu(?=^|$|')/<q>/",
"xform/[iu]a(?=^|$|')/<w>/",
"xform/er(?=^|$|')|[uv]an(?=^|$|')/<r>/",
"xform/[uv]e(?=^|$|')/<t>/",
"xform/v(?=^|$|')|uai(?=^|$|')/<y>/",
"xform/^sh/<u>/",
"xform/^ch/<i>/",
"xform/^zh/<v>/",
"xform/'sh/'<u>/",
"xform/'ch/'<i>/",
"xform/'zh/'<v>/",
"xform/uo(?=^|$|')/<o>/",
"xform/[uv]n(?=^|$|')/<p>/",
"xform/([a-z>])i?ong(?=^|$|')/$1<s>/",
"xform/[iu]ang(?=^|$|')/<d>/",
"xform/([a-z>])en(?=^|$|')/$1<f>/",
"xform/([a-z>])eng(?=^|$|')/$1<g>/",
"xform/([a-z>])ang(?=^|$|')/$1<h>/",
"xform/ian(?=^|$|')/<m>/",
"xform/([a-z>])an(?=^|$|')/$1<j>/",
"xform/iao(?=^|$|')/<c>/",
"xform/([a-z>])ao(?=^|$|')/$1<k>/",
"xform/([a-z>])ai(?=^|$|')/$1<l>/",
"xform/([a-z>])ei(?=^|$|')/$1<z>/",
"xform/ie(?=^|$|')/<x>/",
"xform/ui(?=^|$|')/<v>/",
"derive/<t>(?=^|$|')/<v>/",
"xform/([a-z>])ou(?=^|$|')/$1<b>/",
"xform/in(?=^|$|')/<n>/",
"xform/ing(?=^|$|')/;/",
"xform/'|<|>//",
},
-- 搜狗双拼sogou
sogou = {
"derive/^([jqxy])u(?=^|$|')/$1v/",
"derive/'([jqxy])u(?=^|$|')/'$1v/",
"derive/^([aoe].*)(?=^|$|')/o$1/",
"derive/'([aoe].*)(?=^|$|')/'o$1/",
"xform/^([ae])(.*)(?=^|$|')/$1$1$2/",
"xform/'([ae])(.*)(?=^|$|')/'$1$1$2/",
"xform/iu(?=^|$|')/<q>/",
"xform/[iu]a(?=^|$|')/<w>/",
"xform/er(?=^|$|')|[uv]an(?=^|$|')/<r>/",
"xform/[uv]e(?=^|$|')/<t>/",
"xform/v(?=^|$|')|uai(?=^|$|')/<y>/",
"xform/^sh/<u>/",
"xform/^ch/<i>/",
"xform/^zh/<v>/",
"xform/'sh/'<u>/",
"xform/'ch/'<i>/",
"xform/'zh/'<v>/",
"xform/uo(?=^|$|')/<o>/",
"xform/[uv]n(?=^|$|')/<p>/",
"xform/([a-z>])i?ong(?=^|$|')/$1<s>/",
"xform/[iu]ang(?=^|$|')/<d>/",
"xform/([a-z>])en(?=^|$|')/$1<f>/",
"xform/([a-z>])eng(?=^|$|')/$1<g>/",
"xform/([a-z>])ang(?=^|$|')/$1<h>/",
"xform/ian(?=^|$|')/<m>/",
"xform/([a-z>])an(?=^|$|')/$1<j>/",
"xform/iao(?=^|$|')/<c>/",
"xform/([a-z>])ao(?=^|$|')/$1<k>/",
"xform/([a-z>])ai(?=^|$|')/$1<l>/",
"xform/([a-z>])ei(?=^|$|')/$1<z>/",
"xform/ie(?=^|$|')/<x>/",
"xform/ui(?=^|$|')/<v>/",
"xform/([a-z>])ou(?=^|$|')/$1<b>/",
"xform/in(?=^|$|')/<n>/",
"xform/ing(?=^|$|')/;/",
"xform/'|<|>//",
},
-- 智能abc
abc = {
"xform/^zh/<a>/",
"xform/^ch/<e>/",
"xform/^sh/<v>/",
"xform/'zh/'<a>/",
"xform/'ch/'<e>/",
"xform/'sh/'<v>/",
"xform/^([aoe].*)(?=^|$|')/<o>$1/",
"xform/'([aoe].*)(?=^|$|')/'<o>$1/",
"xform/ei(?=^|$|')/<q>/",
"xform/ian(?=^|$|')/<w>/",
"xform/er(?=^|$|')|iu(?=^|$|')/<r>/",
"xform/[iu]ang(?=^|$|')/<t>/",
"xform/ing(?=^|$|')/<y>/",
"xform/uo(?=^|$|')/<o>/",
"xform/uan(?=^|$|')/<p>/",
"xform/([a-z>])i?ong(?=^|$|')/$1<s>/",
"xform/[iu]a(?=^|$|')/<d>/",
"xform/en(?=^|$|')/<f>/",
"xform/eng(?=^|$|')/<g>/",
"xform/ang(?=^|$|')/<h>/",
"xform/an(?=^|$|')/<j>/",
"xform/iao(?=^|$|')/<z>/",
"xform/ao(?=^|$|')/<k>/",
"xform/in(?=^|$|')|uai(?=^|$|')/<c>/",
"xform/ai(?=^|$|')/<l>/",
"xform/ie(?=^|$|')/<x>/",
"xform/ou(?=^|$|')/<b>/",
"xform/un(?=^|$|')/<n>/",
"xform/[uv]e(?=^|$|')|ui(?=^|$|')/<m>/",
"xform/'|<|>//",
},
-- 紫光ziguang
ziguang = {
"derive/^([jqxy])u(?=^|$|')/$1v/",
"derive/'([jqxy])u(?=^|$|')/'$1v/",
"xform/'([aoe].*)(?=^|$|')/'<o>$1/",
"xform/^([aoe].*)(?=^|$|')/<o>$1/",
"xform/en(?=^|$|')/<w>/",
"xform/eng(?=^|$|')/<t>/",
"xform/in(?=^|$|')|uai(?=^|$|')/<y>/",
"xform/^zh/<u>/",
"xform/^sh/<i>/",
"xform/'zh/'<u>/",
"xform/'sh/'<i>/",
"xform/uo(?=^|$|')/<o>/",
"xform/ai(?=^|$|')/<p>/",
"xform/^ch/<a>/",
"xform/'ch/'<a>/",
"xform/[iu]ang(?=^|$|')/<g>/",
"xform/ang(?=^|$|')/<s>/",
"xform/ie(?=^|$|')/<d>/",
"xform/ian(?=^|$|')/<f>/",
"xform/([a-z>])i?ong(?=^|$|')/$1<h>/",
"xform/er(?=^|$|')|iu(?=^|$|')/<j>/",
"xform/ei(?=^|$|')/<k>/",
"xform/uan(?=^|$|')/<l>/",
"xform/ing(?=^|$|')/;/",
"xform/ou(?=^|$|')/<z>/",
"xform/[iu]a(?=^|$|')/<x>/",
"xform/iao(?=^|$|')/<b>/",
"xform/ue(?=^|$|')|ui(?=^|$|')|ve(?=^|$|')/<n>/",
"xform/un(?=^|$|')/<m>/",
"xform/ao(?=^|$|')/<q>/",
"xform/an(?=^|$|')/<r>/",
"xform/'|<|>//",
},
-- 拼音加加pyjj
pyjj = {
"derive/^([jqxy])u(?=^|$|')/$1v/",
"derive/'([jqxy])u(?=^|$|')/'$1v/",
"derive/^([aoe])([ioun])(?=^|$|')/$1$1$2/",
"derive/'([aoe])([ioun])(?=^|$|')/'$1$1$2/",
"xform/^([aoe])(ng)?(?=^|$|')/$1$1$2/",
"xform/'([aoe])(ng)?(?=^|$|')/'$1$1$2/",
"xform/iu(?=^|$|')/<n>/",
"xform/[iu]a(?=^|$|')/<b>/",
"xform/[uv]an(?=^|$|')/<c>/",
"xform/[uv]e(?=^|$|')|uai(?=^|$|')/<x>/",
"xform/ing(?=^|$|')|er(?=^|$|')/<q>/",
"xform/^sh/<i>/",
"xform/^ch/<u>/",
"xform/^zh/<v>/",
"xform/'sh/'<i>/",
"xform/'ch/'<u>/",
"xform/'zh/'<v>/",
"xform/uo(?=^|$|')/<o>/",
"xform/[uv]n(?=^|$|')/<z>/",
"xform/([a-z>])i?ong(?=^|$|')/$1<y>/",
"xform/[iu]ang(?=^|$|')/<h>/",
"xform/([a-z>])en(?=^|$|')/$1<r>/",
"xform/([a-z>])eng(?=^|$|')/$1<t>/",
"xform/([a-z>])ang(?=^|$|')/$1<g>/",
"xform/ian(?=^|$|')/<j>/",
"xform/([a-z>])an(?=^|$|')/$1<f>/",
"xform/iao(?=^|$|')/<k>/",
"xform/([a-z>])ao(?=^|$|')/$1<d>/",
"xform/([a-z>])ai(?=^|$|')/$1<s>/",
"xform/([a-z>])ei(?=^|$|')/$1<w>/",
"xform/ie(?=^|$|')/<m>/",
"xform/ui(?=^|$|')/<v>/",
"xform/([a-z>])ou(?=^|$|')/$1<p>/",
"xform/in(?=^|$|')/<l>/",
"xform/'|<|>//",
},
-- 国标双拼gbpy
gbpy = {
"derive/^([aoe])([ioun])(?=^|$|')/$1$1$2/",
"derive/'([aoe])([ioun])(?=^|$|')/'$1$1$2/",
"xform/^([aoe])(ng)?(?=^|$|')/$1$1$2/",
"xform/'([aoe])(ng)?(?=^|$|')/'$1$1$2/",
"xform/iu(?=^|$|')/<y>/",
"xform/(.)ei(?=^|$|')/$1<b>/",
"xform/uan(?=^|$|')/<w>/",
"xform/[uv]e(?=^|$|')/<x>/",
"xform/un(?=^|$|')/<z>/",
"xform/^sh/<u>/",
"xform/^ch/<i>/",
"xform/^zh/<v>/",
"xform/'sh/'<u>/",
"xform/'ch/'<i>/",
"xform/'zh/'<v>/",
"xform/uo(?=^|$|')/<o>/",
"xform/ie(?=^|$|')/<t>/",
"xform/([a-z>])i?ong(?=^|$|')/$1<s>/",
"xform/ing(?=^|$|')|uai(?=^|$|')/<j>/",
"xform/([a-z>])ai(?=^|$|')/$1<k>/",
"xform/([a-z>])en(?=^|$|')/$1<r>/",
"xform/([a-z>])eng(?=^|$|')/$1<h>/",
"xform/[iu]ang(?=^|$|')/<n>/",
"xform/([a-z>])ang(?=^|$|')/$1<g>/",
"xform/ian(?=^|$|')/<d>/",
"xform/([a-z>])an(?=^|$|')/$1<f>/",
"xform/([a-z>])ou(?=^|$|')/$1<p>/",
"xform/[iu]a(?=^|$|')/<q>/",
"xform/iao(?=^|$|')/<m>/",
"xform/([a-z>])ao(?=^|$|')/$1<c>/",
"xform/ui(?=^|$|')/<v>/",
"xform/in(?=^|$|')/<l>/",
"xform/'|<|>//",
},
}
-- 根据输入法类型选择一套规则(只看 id
local function pick_rules(env)
local wanx = get_wanxiang()
local id = 'pinyin'
if wanx and type(wanx.get_input_method_type) == 'function' then
local ok, ret_id = pcall(wanx.get_input_method_type, env)
if ok and type(ret_id) == 'string' and #ret_id > 0 then
id = ret_id
end
end
return LOCAL_PROJECTION_RULES[id] or LOCAL_PROJECTION_RULES['pinyin'] or {}
end
------------------------------------------------------------
-- 工具函数
------------------------------------------------------------
local function alt_lua_punc(s)
if s then
return s:gsub('([%.%+%-%*%?%[%]%^%$%(%)%%])', '%%%1')
else
return ''
end
end
-- 仅保留纯小写字母
local function is_pure_lower_alpha(s)
return type(s) == "string" and s:match("^[a-z]+$") ~= nil
end
local function is_all_upper(s) return s:match('^%u+$') ~= nil end
local function is_all_lower(s) return s:match('^%l+$') ~= nil end
local function add_to_set_list(set_map, list, elem)
if not elem or #elem == 0 then return end
if not set_map[elem] then
set_map[elem] = true
table.insert(list, elem)
end
end
------------------------------------------------------------
-- 规则应用 / 反查逻辑
------------------------------------------------------------
local function expand_code_variant(code_projection, part)
local out, seen = {}, {}
local function add(s) add_to_set_list(seen, out, s) end
add(part)
if code_projection then
local p = code_projection:apply(part, true)
if p and #p > 0 then add(p) end
end
local base = {}
for i = 1, #out do base[i] = out[i] end
for _, s in ipairs(base) do
if is_all_upper(s) then add(string.lower(s)) end -- 笔画:仅转小写参与
if #s == 4 and is_all_lower(s) then -- 4 小写 → 取 1/3
local s13 = s:sub(1,1) .. s:sub(3,3)
add(s13)
end
end
return out
end
local function build_reverse_group(code_projection, db_table, text)
local group, seen = {}, {}
for _, db in ipairs(db_table) do
local code = db:lookup(text)
if code and #code > 0 then
for part in code:gmatch('%S+') do
local variants = expand_code_variant(code_projection, part)
for _, v in ipairs(variants) do add_to_set_list(seen, group, v) end
end
end
end
-- 最终清理:只保留纯小写字母
local cleaned, seen2 = {}, {}
for _, v in ipairs(group) do
v = tostring(v)
if is_pure_lower_alpha(v) then add_to_set_list(seen2, cleaned, v) end
end
return cleaned
end
-- 不支持通配global_match=true 为“包含”,否则“前缀”
local function group_match(group, fuma, global_match)
if not fuma or #fuma == 0 then return false end
local patt = alt_lua_punc(string.lower(fuma))
for _, elem in ipairs(group) do
local e = string.lower(elem)
if global_match then
if e:find(patt) then return true end
else
if e:find('^' .. patt) then return true end
end
end
return false
end
-- 单字优先
local function handle_long_cand(if_single_char_first, cand, long_word_cands)
if if_single_char_first and utf8.len(cand.text) > 1 then
table.insert(long_word_cands, cand)
else
yield(cand)
end
end
------------------------------------------------------------
-- 过滤器主体
------------------------------------------------------------
local f = {}
function f.init(env)
local config = env.engine.schema.config
-- 反查 db
env.if_reverse_lookup = false
env.db_table = nil
local db = config:get_list("wanxiang_lookup/lookup")
if db and db.size > 0 then
env.db_table = {}
for i = 0, db.size - 1 do
table.insert(env.db_table, ReverseLookup(db:get_value_at(i).value))
end
env.if_reverse_lookup = true
end
if not env.if_reverse_lookup then return end
-- 内置规则 + 自动选择(不读 schema 的 format
do
local rules = pick_rules(env)
if type(rules) == 'table' and #rules > 0 then
env.code_projection = Projection()
env.code_projection:load(rules)
else
env.code_projection = nil
end
end
-- 引导键:优先从 wanxiang_lookup/key 读;否则默认 `
env.search_key_str = config:get_string('wanxiang_lookup/key') or '`'
env.search_key_alt = alt_lua_punc(env.search_key_str)
-- tags
local tag = config:get_list('wanxiang_lookup/tags')
if tag and tag.size > 0 then
env.tag = {}
for i = 0, tag.size - 1 do
table.insert(env.tag, tag:get_value_at(i).value)
end
else
env.tag = { 'abc' }
end
-- 选词接管:词组保留引导码,否则上屏
env.notifier = env.engine.context.select_notifier:connect(function(ctx)
local input = ctx.input
local code = input:match('^(.-)' .. env.search_key_alt)
if (not code or #code == 0) then return end
local preedit = ctx:get_preedit()
local no_search_string = ctx.input:match('^(.-)' .. env.search_key_alt)
local edit = preedit.text:match('^(.-)' .. env.search_key_alt)
if edit and edit:match('[%w;]') then
ctx.input = no_search_string .. env.search_key_str
else
ctx.input = no_search_string
env.commit_code = no_search_string
ctx:commit()
end
end)
env._group_cache = setmetatable({}, { __mode = 'kv' })
end
function f.func(input, env)
if not env.if_reverse_lookup then
for cand in input:iter() do yield(cand) end
return
end
local code, fuma = env.engine.context.input:match('^(.-)' .. env.search_key_alt .. '(.+)$')
if (not code or #code == 0) or (not fuma or #fuma == 0) then
for cand in input:iter() do yield(cand) end
return
end
-- 双段辅码a`X`Y第二段匹配第二字或第一字“包含”
local fuma_2
if fuma:find(env.search_key_alt) then
fuma, fuma_2 = fuma:match('^(.-)' .. env.search_key_alt .. '(.*)$')
end
local if_single_char_first = env.engine.context:get_option('char_priority')
local long_word_cands = {}
for cand in input:iter() do
if cand.type == 'sentence' then goto skip end
local cand_text = cand.text
local text = cand_text
local text_2 = nil
if utf8.len(cand_text) and utf8.len(cand_text) > 1 then
text = cand_text:sub(1, utf8.offset(cand_text, 2) - 1)
local cand_text_2 = cand_text:gsub('^' .. text, '')
text_2 = cand_text_2:sub(1, utf8.offset(cand_text_2, 2) - 1)
end
local group1 = env._group_cache[text]
if not group1 then
group1 = build_reverse_group(env.code_projection, env.db_table, text)
env._group_cache[text] = group1
end
local ok = false
if fuma_2 and #fuma_2 > 0 then
local group2 = nil
if text_2 then
group2 = env._group_cache[text_2]
if not group2 then
group2 = build_reverse_group(env.code_projection, env.db_table, text_2)
env._group_cache[text_2] = group2
end
end
ok =
group_match(group1, fuma, false) and
(
(group2 and group_match(group2, fuma_2, false)) or
group_match(group1, fuma_2, true) -- 第一字“包含”
)
else
ok = group_match(group1, fuma, false) -- 单段:前缀匹配第一字
end
if ok then
handle_long_cand(if_single_char_first, cand, long_word_cands)
end
::skip::
end
for _, c in ipairs(long_word_cands) do yield(c) end
end
function f.tags_match(seg, env)
for _, v in ipairs(env.tag) do if seg.tags[v] then return true end end
return false
end
function f.fini(env)
if env.if_reverse_lookup and env.notifier then env.notifier:disconnect() end
env.db_table = nil
env._group_cache = nil
collectgarbage('collect')
end
return f

250
lua/super_segmentation.lua Normal file
View File

@@ -0,0 +1,250 @@
-- super_segmentation.lua
--@amzxyz https://github.com/amzxyz/rime_wanxiang
-- 规则:
-- 1) 第 1 个 '仅记录“现场”baseline_head=当前整段输入,含你之前的手动分隔),记录起点索引,不重建
-- 2) 第 2 个 ' 起:开始循环
-- - 命中起点 s只循环 s 后面的 m-1 个形态(跳过 s 本身)
-- - 未命中:从 all[1] 开始循环 m 个形态
-- 3) 走完一圈:恢复到 baseline_head并尾部只保留 1 个 '
-- 4) 支持 N=3..8(可扩展 PATTERNS
-- 5) 使用 update_notifier 预缓存可见分段,避免移动端“晚一拍”
local K_REJECT, K_ACCEPT, K_NOOP = 0, 1, 2
local M = {}
-- ---------- utils ----------
local function escp(ch) return ch:gsub("(%W)","%%%1") end
local function sum(a) local s=0; for _,v in ipairs(a) do s=s+v end; return s end
local function key_of(a) return table.concat(a, ",") end
local function find_idx(list, key) for i,t in ipairs(list) do if key_of(t)==key then return i end end end
local function count_trailing(s, ch) local n=0; for i=#s,1,-1 do if s:sub(i,i)==ch then n=n+1 else break end end; return n end
local function strip_trailing(s, ch) return (s:gsub(escp(ch).."+$","")) end
-- 去掉手动与自动分隔符,得到“纯编码”
local function strip_delims(s, md, ad)
if md and md~="" then s = s:gsub(escp(md),"") end
if ad and ad~="" then s = s:gsub(escp(ad),"") end
return s
end
-- 依据分组把 core 插入手动分隔符重建
local function build_by_groups(core, ch_manual, groups)
if not groups or #groups==0 or sum(groups)~=#core then return core end
local out, i = {}, 1
for gi,g in ipairs(groups) do
out[#out+1] = core:sub(i, i+g-1); i = i + g
if gi < #groups then out[#out+1] = ch_manual end
end
return table.concat(out)
end
-- 从字符串解析分段长度(空格或 ' 都视为可见分隔)
local function lens_from_string(s, md, ad)
if not s or s=="" then return nil end
local segs, buf = {}, {}
local function flush() if #buf>0 then segs[#segs+1]=table.concat(buf); buf={} end end
for i=1,#s do
local c=s:sub(i,i)
if c==md or c==ad or c==" " then
flush()
else
local b=string.byte(c)
if b and ((b>=65 and b<=90) or (b>=97 and b<=122)) then
buf[#buf+1]=string.char(b):lower()
end
end
end
flush()
if #segs==0 then return nil end
local L={}; for _,seg in ipairs(segs) do L[#L+1]=#seg end
return L
end
-- —— 缓存读取:优先用通知器缓存的 lens其次现场计算 ——
local function get_cached_lens(env, ctx, md, ad)
local L = env._last_preedit_lens
if L and type(L)=="table" and #L>0 then return L end
local seg = ctx.composition:back()
local cand = seg and seg:get_selected_candidate() or nil
return lens_from_string(cand and cand.preedit or nil, md, ad)
end
-- ---------- patterns ----------
local PATTERNS = {
[3] = { all = { {2,1}, {1,2} } },
[4] = { all = { {2,2}, {1,3}, {3,1} } },
[5] = { all = { {2,3}, {3,2} } },
[6] = { all = { {2,2,2}, {3,3} } },
[7] = { all = { {2,2,3}, {2,3,2}, {3,2,2} } },
[8] = { all = { {2,2,2,2}, {2,3,3}, {3,2,3}, {3,3,2} } },
[10] = { all = { {2,2,2,2,2} } },
[12] = { all = { {2,2,2,2,2,2} } },
}
-- ---------- session state ----------
local function reset_session(env)
env._ss_core_letters = nil -- 纯编码(去分隔)
env._ss_start_idx = nil -- 起点索引1..m未命中则 0
env._ss_N = nil
env._ss_baseline_head = nil -- 基线:包含你之前的手动分隔/空格
end
local function ulen(s)
if not s or s == "" then return 0 end
if utf8 and utf8.len then
local ok, n = pcall(utf8.len, s)
if ok and n then return n end
end
-- 兜底:简单按 UTF-8 码点数
local n = 0
if utf8 and utf8.codes then
for _ in utf8.codes(s) do n = n + 1 end
return n
end
-- 再兜底:直接 #s有误差但总比没有好
return #s
end
function M.init(env)
local cfg = env.engine.schema.config
local delimiter = cfg:get_string("speller/delimiter") or " '"
if #delimiter < 2 then delimiter = " '" end
env.auto_delim = delimiter:sub(1,1) -- 通常空格
env.manual_delim = delimiter:sub(2,2) -- 通常单引号
-- 缓存最新一帧的可见分段与输入
env._upd_conn = env.engine.context.update_notifier:connect(function(ctx)
local seg = ctx.composition:back()
local cand = seg and seg:get_selected_candidate() or nil
local pre = cand and cand.preedit or nil
env._last_preedit_lens = lens_from_string(pre, env.manual_delim, env.auto_delim)
env._last_input_head = ctx.input
env._last_input_for_caret = ctx.input
env._last_caret_pos = ctx.caret_pos
end)
reset_session(env)
end
function M.fini(env)
if env._upd_conn then env._upd_conn:disconnect(); env._upd_conn=nil end
end
-- ---------- main ----------
function M.func(key_event, env)
if key_event:release() then return K_NOOP end
local ctx = env.engine.context
if ctx.composition:empty() then return K_NOOP end
local md = env.manual_delim or "'"
local ad = env.auto_delim or " "
-- 只处理手动分隔符键
if key_event.keycode ~= string.byte(md) then
reset_session(env); return K_NOOP
end
--用「上一帧」的光标位置判断是不是在中间编辑
do
local last_input = env._last_input_for_caret or ctx.input or ""
local last_caret = env._last_caret_pos
local total_len = ulen(last_input)
-- 只有「上一帧光标在末尾」我们才认定在玩超分段
if not last_caret or last_caret ~= total_len then
-- 上一帧光标不在末尾:说明用户在中间编辑,这次 ' 交给默认逻辑
reset_session(env)
return K_NOOP
end
end
-- 把这次 ' 并入输入,统计尾部 ' 数
local before = ctx.input or ""
local after = before .. md
local tlen = count_trailing(after, md)
-- 去掉末尾 ' 串,得到 head本次按键前的完整输入与 core纯编码
local head = strip_trailing(after, md)
local core = strip_delims(head, md, ad)
local N = #core
local conf = PATTERNS[N]
-- 若核心/长度变化,重置会话
if env._ss_core_letters ~= core or env._ss_N ~= N then
env._ss_core_letters = core
env._ss_N = N
env._ss_start_idx = nil
env._ss_baseline_head = nil
end
-- 只要本轮还没记过,就立刻记录“基线 + 起点”(无论 tlen==1 还是 tlen>=2
if env._ss_baseline_head == nil then
env._ss_baseline_head = head -- 保留你原有的空格或手动 '
end
if conf and env._ss_start_idx == nil then
local start_idx = 0
-- 先用缓存的可见分段;不行就直接用 head 切分可避免“23 又走到 23'”的伪步骤)
local L = get_cached_lens(env, ctx, md, ad)
if not (L and sum(L)==N) then
L = lens_from_string(head, md, ad)
end
if L and sum(L)==N then
local idx = find_idx(conf.all, key_of(L))
if idx then start_idx = idx end
end
env._ss_start_idx = start_idx
end
-- 第 1 个 ' :仅记录,不重建
if tlen == 1 then
ctx.input = after
return K_ACCEPT
end
-- 第 2 个 ' 起:循环(若无该长度配置,直接接纳输入)
if not conf then
ctx.input = after
return K_ACCEPT
end
local m = #conf.all
local k = tlen - 1 -- 从第二个 ' 开始计数
-- 恢复:回到第一拍记录的 baseline保留空格/已有 '),尾部只留 1 个 '
local function restore()
local baseline = env._ss_baseline_head or head
ctx.input = baseline .. md
reset_session(env)
env._ss_core_letters = core
env._ss_N = N
end
if env._ss_start_idx and env._ss_start_idx ~= 0 then
-- 命中起点:只循环后续 m-1 个形态,跳过当前形态
local variants_count = m - 1
local cycle_len = variants_count + 1
local r = k % cycle_len
if r == 0 then
restore(); return K_ACCEPT
else
local idx = ((env._ss_start_idx - 1 + r) % m) + 1 -- 跳过起点本身
local groups = conf.all[idx]
local rebuilt = build_by_groups(core, md, groups)
ctx.input = rebuilt .. md:rep(tlen)
return K_ACCEPT
end
else
-- 未命中起点:从 all[1] 开始循环 m 个形态
local variants_count = m
local cycle_len = variants_count + 1
local r = k % cycle_len
if r == 0 then
restore(); return K_ACCEPT
else
local idx = ((r - 1) % m) + 1
local groups = conf.all[idx]
local rebuilt = build_by_groups(core, md, groups)
ctx.input = rebuilt .. md:rep(tlen)
return K_ACCEPT
end
end
end
return { init = M.init, fini = M.fini, func = M.func }

734
lua/super_sequence.lua Normal file
View File

@@ -0,0 +1,734 @@
-- 万象拼音 · 手动自由排序
-- 核心规则: 向前移动 = "Control+j", 向后移动 = "Control+k", 重置 = "Control+l", 置顶 = "Control+p
-- 1) p>0有效排序DB upsert + 导出)
-- 2) p=0墓碑DB 删除 + 导出墓碑)
-- 3) 初始化:先 flush 本机增量到导出 → 外部合并(所有设备文件+本机DBLWW) → 重写本机导出(含墓碑) → 导入覆盖DBp=0删除键不导入
-- 4) 关于同步的使用方法先点击同步确保同步目录已经创建建立sequence_device_list.txt设备清单内部填写不同设备导出文件名称
-- sequence_ff9b2823-8733-44bb-a497-daf382b74ca5.txt
-- sequence_deepin.txt
-- 可能是自定义名称,可能是随机串号
-- sequence_开头后面跟着installation_id这个参数来自用户目录installation.yaml
-- 清单有什么文件就会读取什么文件
-- 仅使用 installation.yaml 的 sync_dir读不到就回退到 user_dir/sync
-- 核心规则: 向前移动 = "Control+j", 向后移动 = "Control+k", 重置 = "Control+l", 置顶 = "Control+p"
-- 1) p>0有效排序DB upsert + 导出)
-- 2) p=0墓碑DB 删除 + 导出墓碑)
-- 3) 初始化:先 flush 本机增量到导出 → 外部合并(所有设备文件+本机DBLWW) → 重写本机导出(含墓碑) → 导入覆盖DBp=0删除键不导入
-- 4) 同步路径策略:能从 installation.yaml 读取到 sync_dir 就用它;读不到才用默认 user_dir/sync
local wanxiang = require("wanxiang")
local userdb = require("lib/userdb")
------------------------------------------------------------
-- 一、常量与键位
------------------------------------------------------------
local DEFAULT_SEQ_KEY = { up = "Control+j", down = "Control+k", reset = "Control+l", pin = "Control+p" }
local SYNC_FILE_PREFIX, SYNC_FILE_SUFFIX = "sequence", ".txt"
-- 运行期是否立刻写出到导出文件(只在重新部署时写出→设为 false
local RUNTIME_EXPORT = false
-- ☆☆ 前向声明,避免被当作全局导致 nil ☆☆
local _normalize_path, _is_abs_path, _path_join, _manifest_path
------------------------------------------------------------
-- 二、通用工具(仅处理 "\" 与 "\\", 统一成 "/"
------------------------------------------------------------
_normalize_path = function(p)
if not p or p == "" then return "" end
if p:sub(1, 2) == "\\\\" then
-- UNC\\server\share\foo -> //server/share/foo
return "//" .. p:sub(3):gsub("\\", "/"):gsub("/+", "/")
else
-- 普通D:\dir\\file -> D:/dir/file
return p:gsub("\\", "/"):gsub("/+", "/")
end
end
_is_abs_path = function(p)
p = _normalize_path(p)
return p:sub(1, 2) == "//" or p:match("^[A-Za-z]:/")
end
_path_join = function(a, b)
a = _normalize_path(a)
b = _normalize_path(b)
if not a or a == "" then return b end
if not b or b == "" then return a end
if _is_abs_path(b) then return b end
if a:sub(-1) ~= "/" then a = a .. "/" end
return a .. b
end
_manifest_path = function(dir)
return _path_join(dir, "sequence_device_list.txt")
end
local function _read_lines(path)
local t, f = {}, io.open(path, "r")
if not f then return t end
for line in f:lines() do t[#t + 1] = line end
f:close()
return t
end
local function _write_lines(path, lines)
local f = io.open(path, "w"); if not f then return false end
for _, line in ipairs(lines) do f:write(line, "\n") end
f:close()
return true
end
local function _trim(s) return (s:gsub("^%s+", ""):gsub("%s+$", "")) end
local function _file_exists(path)
if not path or path == "" then return false end
local f = io.open(path, "r"); if f then f:close(); return true end
return false
end
------------------------------------------------------------
-- 三、安装信息 & 同步目录(仅看 YAML读不到就默认
------------------------------------------------------------
local function _read_installation_yaml()
local user_dir = rime_api.get_user_data_dir()
if not user_dir or user_dir == "" then return nil, nil end
local path = _path_join(user_dir, "installation.yaml")
local f = io.open(path, "r"); if not f then return nil, nil end
local installation_id, sync_dir
for line in f:lines() do
line = line:gsub("%s+#.*$", "")
local key, val = line:match("^%s*([%w_]+)%s*:%s*(.+)$")
if key and val then
-- 去引号
val = val:gsub('^%s*"(.*)"%s*$', "%1"):gsub("^%s*'(.*)'%s*$", "%1")
val = val:gsub("^%s+", ""):gsub("%s+$", "")
if key == "installation_id" then
installation_id = val
elseif key == "sync_dir" then
sync_dir = _normalize_path(val)
end
end
end
f:close()
return installation_id, sync_dir
end
-- 只看 installation.yaml读到就用读不到就 user_dir/sync
local function _sync_dir()
local user_dir = rime_api.get_user_data_dir() or ""
local _, ysync = _read_installation_yaml()
local function fix(x)
if not x or x == "" then return "" end
if x == "sync" then
return (user_dir ~= "" and _path_join(user_dir, "sync")) or "sync"
end
return _normalize_path(x)
end
if ysync and ysync ~= "" then
return fix(ysync)
end
return _path_join(user_dir, "sync")
end
local function _sync_ready()
local install_id, ysync = _read_installation_yaml()
local user_dir = rime_api.get_user_data_dir() or ""
local dir
if ysync and ysync ~= "" then
dir = _normalize_path(ysync)
if dir == "sync" then dir = _path_join(user_dir, "sync") end
else
dir = _path_join(user_dir, "sync")
end
local ok = (install_id and install_id ~= "") and (dir and dir ~= "")
return ok, dir, install_id
end
local function _detect_device_name()
local installation_id = select(1, _read_installation_yaml())
local function _san(s) return tostring(s):gsub("[%s/\\:%*%?\"<>|]", "_") end
if installation_id and installation_id ~= "" then return _san(installation_id) end
local dir = _sync_dir()
for _, raw in ipairs(_read_lines(_manifest_path(dir))) do
local name = _trim(raw or "")
local m = name:match("^sequence_(.+)%.txt$")
if m and not _is_abs_path(name) then return _san(m) end
end
return "device"
end
------------------------------------------------------------
-- 四、时间
------------------------------------------------------------
local function get_timestamp()
local ms = type(rime_api.get_time_ms) == "function" and tonumber(rime_api.get_time_ms()) or nil
return ms and (os.time() + ms / 1000.0) or os.time()
end
------------------------------------------------------------
-- 五、DB 与状态
------------------------------------------------------------
local seq_db = userdb.LevelDb("lua/sequence")
local seq_property = {
ADJUST_KEY = "sequence_adjustment_code",
}
---@param context Context
function seq_property.get(context)
return context:get_property(seq_property.ADJUST_KEY)
end
---@param context Context
function seq_property.reset(context)
local code = seq_property.get(context)
if code ~= nil and code ~= "" then
context:set_property(seq_property.ADJUST_KEY, "")
end
end
local curr_state = {}
curr_state.ADJUST_MODE = { None = -1, Reset = 0, Pin = 1, Adjust = 2 }
curr_state.default = {
selected_phrase = nil, offset = 0, mode = curr_state.ADJUST_MODE.None,
highlight_index = nil, adjust_code = nil, adjust_key = nil,
dirty = false, last_dirty_ts = 0,
}
function curr_state.reset()
if curr_state.mode == curr_state.ADJUST_MODE.None then return end
for k, v in pairs(curr_state.default) do curr_state[k] = v end
end
function curr_state.is_pin_mode() return curr_state.mode == curr_state.ADJUST_MODE.Pin end
function curr_state.is_reset_mode() return curr_state.mode == curr_state.ADJUST_MODE.Reset end
function curr_state.is_adjust_mode() return curr_state.mode == curr_state.ADJUST_MODE.Adjust end
function curr_state.has_adjustment() return curr_state.mode ~= curr_state.ADJUST_MODE.None end
------------------------------------------------------------
-- 六、关键日志(精简)
------------------------------------------------------------
--[[
local function _print_sync_probe(phase)
local user_dir = tostring(rime_api.get_user_data_dir() or "")
local iid, ysync = _read_installation_yaml()
local chosen = _sync_dir()
local inst_yaml = _path_join(user_dir, "installation.yaml")
log.warning(string.format(
"[sequence][%s] installation_id=%s yaml_sync_dir=%s chosen_sync_dir=%s inst_yaml=%s exists=%s",
phase, tostring(iid), tostring(ysync), tostring(chosen),
inst_yaml, tostring(_file_exists(inst_yaml))
))
end
local function _debug_paths_once()
local dir = _sync_dir()
local device_name = _detect_device_name()
local export_name = string.format("%s_%s%s", SYNC_FILE_PREFIX, device_name, SYNC_FILE_SUFFIX)
local export_path = _path_join(dir, export_name)
log.info(string.format("[sequence] chosen_sync_dir=%s manifest_exists=%s",
tostring(dir), tostring(_file_exists(_manifest_path(dir)))))
log.info(string.format("[sequence] export_path=%s exists=%s",
tostring(export_path), tostring(_file_exists(export_path))))
end ]]--
------------------------------------------------------------
-- 七、记录解析(新格式)
------------------------------------------------------------
local function parse_adjustment_value_item(value_item)
local item, p, o, t = value_item:match("i=(.+) p=(%S+) o=(%S*) t=(%S+)")
if not item then return nil, nil end
return item, { fixed_position = tonumber(p) or 0, offset = tonumber(o) or 0, updated_at = tonumber(t) }
end
local function parse_adjustment_values(values_str)
local mp = {}
for seg in values_str:gmatch("[^\t]+") do
local item, adj = parse_adjustment_value_item(seg)
if item then mp[item] = adj end
end
return next(mp) and mp or nil
end
local function get_input_adjustments(input)
if not input or input == "" then return nil end
local value_str = seq_db:fetch(input)
return value_str and parse_adjustment_values(value_str) or nil
end
------------------------------------------------------------
-- 八、导出缓冲(去重 + 节流)
------------------------------------------------------------
local seq_data = {
status = "pending",
device_name = "device",
last_export_ts = 0,
export_interval = 1.2, -- 秒
pending_map = {}, -- key: input.."\t"..item => line
}
local function _pending_count() local n = 0; for _ in pairs(seq_data.pending_map) do n = n + 1 end; return n end
function seq_data._current_paths()
local dir = _sync_dir()
local device_name = seq_data.device_name or "device"
local export_name = string.format("%s_%s%s", SYNC_FILE_PREFIX, device_name, SYNC_FILE_SUFFIX)
local export_path = _path_join(dir, export_name)
local manifest = _manifest_path(dir)
return dir, device_name, export_name, export_path, manifest
end
function seq_data._ensure_export_file()
local ok = _sync_ready()
if not ok then
--log.info("[sequence] installation_id 或 sync_dir 缺失,跳过导出")
return false
end
local _, _, export_name, export_path, manifest = seq_data._current_paths()
if not _file_exists(manifest) then
local mf = io.open(manifest, "w"); if not mf then return false end; mf:close()
end
if not _file_exists(export_path) then
local f = io.open(export_path, "w"); if not f then return false end
local user_id = wanxiang.get_user_id()
if user_id then f:write("\001/user_id\t", user_id, "\n") end
f:write("\001/device_name\t", seq_data.device_name or "device", "\n")
f:close()
end
local names = _read_lines(manifest)
local seen = {}; for _, n in ipairs(names) do seen[_trim(n)] = true end
if not seen[export_name] then names[#names + 1] = export_name; _write_lines(manifest, names) end
return true
end
local function _enqueue_export(input, item, adj)
local k = input .. "\t" .. item
seq_data.pending_map[k] = string.format("%s\ti=%s p=%s o=%s t=%s\n",
input, item, adj.fixed_position or 0, adj.offset or 0, adj.updated_at or "")
end
function seq_data.flush_pending(max_lines)
if _pending_count() == 0 then return end
if not seq_data._ensure_export_file() then return end
local _, _, _, export_path = seq_data._current_paths()
local f = io.open(export_path, "a"); if not f then return end
local wrote = 0
for _, line in pairs(seq_data.pending_map) do
if max_lines and wrote >= max_lines then break end
f:write(line); wrote = wrote + 1
end
f:close()
seq_data.pending_map = {}
end
function seq_data.maybe_export(force)
if force then
seq_data.flush_pending(nil)
seq_data.last_export_ts = get_timestamp()
return
end
if _pending_count() == 0 then return end
local now = get_timestamp()
if now - (seq_data.last_export_ts or 0) < (seq_data.export_interval or 1.2) then return end
seq_data.flush_pending(200)
seq_data.last_export_ts = now
end
------------------------------------------------------------
-- 九、保存本机操作p=0 也导出墓碑运行期不写盘DB 暂存墓碑以便重部署覆盖)
------------------------------------------------------------
local function save_adjustment(input, item, adjustment, no_export)
if not input or input == "" or not item or item == "" then return end
local p = tonumber(adjustment.fixed_position) or 0
local o = tonumber(adjustment.offset) or 0
local t = adjustment.updated_at
local mp = get_input_adjustments(input) or {}
if p <= 0 then
-- 关键DB 内也保留 p=0 墓碑(含时间戳),用于重部署时 LWW 覆盖外部文件
mp[item] = { fixed_position = 0, offset = o, updated_at = t }
else
mp[item] = { fixed_position = p, offset = o, updated_at = t }
end
local arr = {}
for it, a in pairs(mp) do
arr[#arr + 1] = string.format("i=%s p=%s o=%s t=%s",
it, a.fixed_position, a.offset or 0, a.updated_at or "")
end
seq_db:update(input, table.concat(arr, "\t"))
-- 仅在允许运行期写出时才入队(默认 RUNTIME_EXPORT=false不入队
if (not no_export) and RUNTIME_EXPORT then
_enqueue_export(input, item, { fixed_position = p, offset = o, updated_at = t }) -- 包含 p=0 墓碑
end
end
------------------------------------------------------------
-- 十、合并器:收集“所有文件 + 本机DB”按 t 取最新(包含 p=0
------------------------------------------------------------
local function _keep_latest(latest, input, item, adj)
latest[input] = latest[input] or {}
local prev = latest[input][item]
if (not prev) or ((adj.updated_at or 0) > (prev.updated_at or 0)) then
latest[input][item] = {
fixed_position = tonumber(adj.fixed_position) or 0,
offset = tonumber(adj.offset) or 0,
updated_at = tonumber(adj.updated_at) or 0
}
end
end
local function collect_latest_from_all_sources()
local latest = {}
-- A) 本机 DB包含 p=0 墓碑:让 DB 能覆盖外部)
seq_db:query_with("", function(key, value)
local mp = parse_adjustment_values(value)
if mp then
for item, a in pairs(mp) do
_keep_latest(latest, key, item, a)
end
end
end)
-- B) 清单里的所有导出文件(包含 p=0
local dir = _sync_dir()
local names = _read_lines(_manifest_path(dir))
for _, raw in ipairs(names) do
local name = _trim(raw or "")
if name ~= "" and name:sub(1, 1) ~= "#" then
if name:sub(1, #SYNC_FILE_PREFIX) == SYNC_FILE_PREFIX
and name:sub(-#SYNC_FILE_SUFFIX) == SYNC_FILE_SUFFIX then
local path = _is_abs_path(name) and name or _path_join(dir, name)
local f = io.open(path, "r")
if f then
for line in f:lines() do
if line ~= "" and line:sub(1, 2) ~= "\001" .. "/" then
local key, value = line:match("^(%S+)\t(.+)$")
if key and value then
local item, adj1 = parse_adjustment_value_item(value)
if item then
_keep_latest(latest, key, item, adj1)
else
local mp = parse_adjustment_values(value)
if mp then for it, a in pairs(mp) do _keep_latest(latest, key, it, a) end end
end
end
end
end
f:close()
end
end
end
end
return latest
end
------------------------------------------------------------
-- 十一、把“合并结果”重写到我机导出(含 p=0
------------------------------------------------------------
local function rewrite_export_from_latest(latest)
local ok = _sync_ready()
if not ok then return end
local dir = _sync_dir()
local installation_id = select(1, _read_installation_yaml())
local device_name = (installation_id and installation_id ~= "") and tostring(installation_id):gsub("[%s/\\:%*%?\"<>|]", "_") or "device"
local export_name = string.format("%s_%s%s", SYNC_FILE_PREFIX, device_name, SYNC_FILE_SUFFIX)
local export_path = _path_join(dir, export_name)
local manifest = _manifest_path(dir)
if not _file_exists(manifest) then local mf = io.open(manifest, "w"); if mf then mf:close() end end
do
local names = _read_lines(manifest); local seen = {}; for _, n in ipairs(names) do seen[_trim(n)] = true end
if not seen[export_name] then names[#names + 1] = export_name; _write_lines(manifest, names) end
end
local f = io.open(export_path, "w"); if not f then return end
local user_id = wanxiang.get_user_id()
if user_id then f:write("\001/user_id\t", user_id, "\n") end
f:write("\001/device_name\t", device_name, "\n")
local inputs = {}
for input, _ in pairs(latest) do inputs[#inputs + 1] = input end
table.sort(inputs)
for _, input in ipairs(inputs) do
local items, keys = latest[input], {}
for item, _ in pairs(items) do keys[#keys + 1] = item end
table.sort(keys)
for _, item in ipairs(keys) do
local a = items[item]
f:write(string.format("%s\ti=%s p=%s o=%s t=%s\n",
input, item, a.fixed_position or 0, a.offset or 0, a.updated_at or ""))
end
end
f:close()
--log.info(string.format("[sequence] export rewritten (merged LWW, incl tombstones): %s", export_path))
end
------------------------------------------------------------
-- 十二、把“合并结果”导入覆盖 DBp<=0 删)
------------------------------------------------------------
local function apply_latest_to_db(latest)
local updated_keys = 0
for input, kv in pairs(latest) do
local keep = {}
for item, a in pairs(kv) do
if (tonumber(a.fixed_position) or 0) > 0 then
keep[item] = { fixed_position = a.fixed_position, offset = a.offset or 0, updated_at = a.updated_at }
end
end
if next(keep) == nil then
seq_db:erase(input)
else
local arr = {}
for item, a in pairs(keep) do
arr[#arr + 1] = string.format("i=%s p=%s o=%s t=%s", item, a.fixed_position, a.offset or 0, a.updated_at or "")
end
seq_db:update(input, table.concat(arr, "\t"))
end
updated_keys = updated_keys + 1
end
--log.info(string.format("[sequence] DB applied from merged LWW: %d keys", updated_keys))
end
------------------------------------------------------------
-- 十三、初始化先导出→合并→重写导出→导入DB
------------------------------------------------------------
local function init_once()
-- 1) 先导出:把本机 pending 增量写出去如果是旧版本留下的队列这里可一次性落盘RUNTIME_EXPORT 与此无关)
seq_data._ensure_export_file()
seq_data.maybe_export(true)
-- 2) 外部合并(所有设备文件 + 本机 DBLWW含 p=0
local latest = collect_latest_from_all_sources()
-- 3) 用合并结果重写我机导出(包含 p=0——始终写盘
rewrite_export_from_latest(latest)
-- 4) 导入合并结果覆盖 DBp<=0 删)
apply_latest_to_db(latest)
end
------------------------------------------------------------
-- 十四、PipelineP / F
------------------------------------------------------------
local P = {}
function P.init(env)
seq_db:open()
seq_data.device_name = _detect_device_name()
--_print_sync_probe("init") -- 关键:一次性输出最终使用的 sync_dir
--_debug_paths_once() -- 关键:简要输出导出与清单路径是否存在
init_once()
end
local function process_adjustment(context)
local c = context:get_selected_candidate()
curr_state.selected_phrase = c and c.text or nil
context:refresh_non_confirmed_composition()
if context.highlight and curr_state.highlight_index and curr_state.highlight_index > 0 then
context:highlight(curr_state.highlight_index)
end
end
-- 辅助:判断是否单个 ASCII 小写字母
local function _is_single_lowercase_letter(s)
return type(s) == "string" and #s == 1 and s:match("^[a-z]$") ~= nil
end
function P.func(key_event, env)
local context = env.engine.context
-- 不要在早期就重置 offset保持原代码行为
curr_state.reset()
local selected_cand = context:get_selected_candidate()
if not context:has_menu() or not selected_cand or not selected_cand.text then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
-- 先判断当前的 adjust_code与 extract_adjustment_code 的逻辑一致)
local function get_adjust_code()
if wanxiang.is_function_mode_active(context) then
local code = seq_property.get(context)
if code and code ~= "" then return code end
return nil
end
return context.input:sub(1, context.caret_pos)
end
local adjust_code = get_adjust_code()
-- 如果不是 function-mode 且 adjust_code 是单个小写字母,则按键不应改变 curr_state.offset因为单字母存在时间复杂度
if (not wanxiang.is_function_mode_active(context)) and _is_single_lowercase_letter(adjust_code) then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
local key_repr = key_event:repr()
local function get_seq_key(type)
return env.engine.schema.config:get_string("key_binder/sequence/" .. type) or DEFAULT_SEQ_KEY[type]
end
if key_repr == get_seq_key("up") then
curr_state.offset = -1; curr_state.mode = curr_state.ADJUST_MODE.Adjust
elseif key_repr == get_seq_key("down") then
curr_state.offset = 1; curr_state.mode = curr_state.ADJUST_MODE.Adjust
elseif key_repr == get_seq_key("reset") then
curr_state.offset = nil; curr_state.mode = curr_state.ADJUST_MODE.Reset
elseif key_repr == get_seq_key("pin") then
curr_state.offset = nil; curr_state.mode = curr_state.ADJUST_MODE.Pin
else
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
process_adjustment(context)
return wanxiang.RIME_PROCESS_RESULTS.kAccepted
end
local F = {}
function F.fini()
-- 退出时不落盘(仅在重新部署 init_once 重写导出)
if RUNTIME_EXPORT then
seq_data.maybe_export(true)
end
end
local function apply_prev_adjustment(cands, prev)
local list = {}
for _, info in pairs(prev or {}) do
if info.raw_position then info.from_position = info.raw_position; table.insert(list, info) end
end
table.sort(list, function(a, b) return (a.updated_at or 0) < (b.updated_at or 0) end)
local n = #cands
for i, record in ipairs(list) do
local fromp = record.from_position
if fromp and (record.fixed_position or 0) > 0 then
local top = (record.offset == 0) and record.fixed_position or (record.raw_position + record.offset)
if top < 1 then top = 1 elseif top > n then top = n end
if fromp ~= top then
local cand = table.remove(cands, fromp)
table.insert(cands, top, cand)
local lo, hi = math.min(fromp, top), math.max(fromp, top)
for j = i, #list do
local r = list[j]
if lo <= r.from_position and r.from_position <= hi then
r.from_position = r.from_position + ((top < fromp) and 1 or -1)
end
end
end
end
end
end
local function apply_curr_adjustment(candidates, curr_adjustment)
if curr_adjustment == nil then return end
---@type integer | nil
local from_position = nil
for position, cand in ipairs(candidates) do
if cand.text == curr_state.selected_phrase then
from_position = position
break
end
end
if from_position == nil then return end
local to_position = from_position
if curr_state.is_adjust_mode() then
to_position = from_position + curr_state.offset
curr_adjustment.offset = to_position - curr_adjustment.raw_position
curr_adjustment.fixed_position = to_position
local min_position, max_position = 1, #candidates
if from_position ~= to_position then
if to_position < min_position then
to_position = min_position
elseif to_position > max_position then
to_position = max_position
end
local candidate = table.remove(candidates, from_position)
table.insert(candidates, to_position, candidate)
-- 运行期仅写 DB不入导出队列
save_adjustment(curr_state.adjust_code, curr_state.adjust_key, curr_adjustment, true)
end
end
curr_state.highlight_index = to_position - 1
end
local function extract_adjustment_code(context)
if wanxiang.is_function_mode_active(context) then
local code = seq_property.get(context)
if code and code ~= "" then return code end
return nil
end
return context.input:sub(1, context.caret_pos)
end
function F.func(input, env)
local function original_list() for cand in input:iter() do yield(cand) end end
local context = env.engine.context
local adjustment_allowed = not (wanxiang.is_function_mode_active(context) and seq_property.get(context) == nil)
if not adjustment_allowed then
--log.warning("[sequence] 当前指令不支持手动排序")
return original_list()
end
local adjust_code = extract_adjustment_code(context)
if not adjust_code then return original_list() end
local prev_adjustments = get_input_adjustments(adjust_code)
local curr_adjustment = curr_state.has_adjustment() and { fixed_position = 0, offset = 0, updated_at = get_timestamp() } or nil
if (not curr_adjustment) and (not prev_adjustments) then return original_list() end
local cands, seen = {}, {}
local is_fun_mode = wanxiang.is_function_mode_active(context)
local pos = 0
for candidate in input:iter() do
local phrase = candidate.text
if not seen[phrase] then
seen[phrase] = true; pos = pos + 1; table.insert(cands, candidate)
local curr_key = is_fun_mode and tostring(pos - 1) or phrase
if curr_adjustment and curr_state.selected_phrase == phrase then
curr_state.adjust_code = adjust_code
curr_state.adjust_key = curr_key
curr_adjustment.raw_position = pos
end
if prev_adjustments and prev_adjustments[curr_key] then
prev_adjustments[curr_key].raw_position = pos
end
end
end
prev_adjustments = prev_adjustments or {}
-- 非位移:置顶/重置立即仅保存到 DB不入队
if curr_adjustment and not curr_state.is_adjust_mode() then
curr_adjustment.offset = 0
local key = tostring(curr_state.adjust_key)
if curr_state.is_reset_mode() then
curr_adjustment.fixed_position = 0
prev_adjustments[key] = nil
save_adjustment(curr_state.adjust_code, curr_state.adjust_key, curr_adjustment, true)
elseif curr_state.is_pin_mode() then
curr_adjustment.fixed_position = 1
prev_adjustments[key] = curr_adjustment
save_adjustment(curr_state.adjust_code, curr_state.adjust_key, curr_adjustment, true)
end
end
apply_prev_adjustment(cands, prev_adjustments)
apply_curr_adjustment(cands, curr_adjustment)
for _, cand in ipairs(cands) do yield(cand) end
-- 运行期不写盘;如需调试可改为 if RUNTIME_EXPORT then ... end
if RUNTIME_EXPORT and (not curr_state.is_reset_mode()) then
seq_data.maybe_export(false)
end
end
return { P = P, F = F }

279
lua/super_tips.lua Normal file
View File

@@ -0,0 +1,279 @@
-- 万象家族lua,超级提示,表情\化学式\方程式\简码等等直接上屏,不占用候选位置
-- 采用leveldb数据库,支持大数据遍历,支持多种类型混合,多种拼音编码混合,维护简单
-- 支持候选匹配和编码匹配两种,候选支持方向键高亮遍历
-- https://github.com/amzxyz/rime_wanxiang
-- - lua_processor@*super_tips
-- key_binder/tips_key: "slash" # 上屏按键配置
-- tips/disabled_types: [] # 禁用的 tips 类型
local wanxiang = require("wanxiang")
local bit = require("lib/bit")
local userdb = require("lib/userdb")
local tips_db = userdb.LevelDb("lua/tips")
-- 获取文件内容哈希值,使用 FNV-1a 哈希算法
local function calculate_file_hash(filepath)
local file = io.open(filepath, "rb")
if not file then return nil end
-- FNV-1a 哈希参数32位
local FNV_OFFSET_BASIS = 0x811C9DC5
local FNV_PRIME = 0x01000193
local hash = FNV_OFFSET_BASIS
while true do
local chunk = file:read(4096)
if not chunk then break end
for i = 1, #chunk do
local byte = string.byte(chunk, i)
hash = bit.bxor(hash, byte)
hash = (hash * FNV_PRIME) % 0x100000000
hash = bit.band(hash, 0xFFFFFFFF)
end
end
file:close()
return string.format("%08x", hash)
end
local tips = {}
---@type "pending" | "initialing" | "done"
tips.status = "pending"
---@type table<string, boolean>
tips.disabled_types = {}
tips.preset_file_path = wanxiang.get_filename_with_fallback("lua/tips/tips_show.txt")
tips.user_override_path = rime_api.get_user_data_dir() .. "/lua/tips/tips_user.txt"
local META_KEY = {
version = "wanxiang_version",
user_file_hash = "user_tips_file_hash",
disabled_types = "disabled_types",
}
---@param tip string
function tips.is_disabled(tip)
local type = tip:match("^(..-):")
or tip:match("^(..-)")
if not type then return false end
return tips.disabled_types[type] == true
end
function tips.init_db_from_file(path)
local file = io.open(path, "r")
if not file then return end
for line in file:lines() do
local value, key = line:match("([^\t]+)\t([^\t]+)")
if key and value
and not tips.is_disabled(value)
then
tips_db:update(key, value)
end
end
file:close()
end
function tips.ensure_dir_exist(dir)
-- 获取系统路径分隔符
local sep = package.config:sub(1, 1)
dir = dir:gsub([["]], [[\"]]) -- 处理双引号
if sep == "/" then
local cmd = 'mkdir -p "' .. dir .. '" 2>/dev/null'
os.execute(cmd)
end
end
---@param config Config
function tips.init(config)
if tips.status ~= "pending" then return end
local dist = rime_api.get_distribution_code_name() or ""
local user_lua_dir = rime_api.get_user_data_dir() .. "/lua"
if dist ~= "hamster" and dist ~= "hamster3" and dist ~= "Weasel" then
tips.ensure_dir_exist(user_lua_dir)
tips.ensure_dir_exist(user_lua_dir .. "/tips")
end
-- 读取配置
local disabled_types_list = config:get_list("tips/disabled_types")
if disabled_types_list then
for i = 1, disabled_types_list.size do
local item = disabled_types_list:get_value_at(i - 1)
if item and #item.value > 0 then
tips.disabled_types[item.value] = true
end
end
end
-- 检查是否需要重建数据库
tips_db:open()
local needs_rebuild = false
-- 检查 1: 万象版本号
if tips_db:meta_fetch(META_KEY.version) ~= wanxiang.version then
needs_rebuild = true
end
-- 检查 2: 用户文件哈希 (仅在版本号相同时检查)
local user_file_hash = calculate_file_hash(tips.user_override_path) or ""
if not needs_rebuild
and (tips_db:meta_fetch(META_KEY.user_file_hash) or "") ~= user_file_hash
then
needs_rebuild = true
end
-- 检查 3: 禁用类型 (仅在前两者都相同时检查)
local disabled_keys = {}
for k, _ in pairs(tips.disabled_types) do
table.insert(disabled_keys, k)
end
table.sort(disabled_keys) -- 排序以确保顺序一致
local disabled_types_str = table.concat(disabled_keys, ",")
if not needs_rebuild
and (tips_db:meta_fetch(META_KEY.disabled_types) or "") ~= disabled_types_str
then
needs_rebuild = true
end
-- 如果需要,则执行重建
if needs_rebuild then
tips_db:empty()
tips.init_db_from_file(tips.preset_file_path)
tips.init_db_from_file(tips.user_override_path)
-- 重建成功后,再更新所有元数据,确保操作的原子性
tips_db:meta_update(META_KEY.version, wanxiang.version)
tips_db:meta_update(META_KEY.user_file_hash, user_file_hash)
tips_db:meta_update(META_KEY.disabled_types, disabled_types_str)
end
-- 关闭并以只读模式重新打开
tips_db:close()
tips_db:open_read_only()
end
---从数据库中查询 tips
---@param keys string | string[] 接受一个字符串或一个字符串数组作为键,使用数组时会挨个查询,直到获得有效值
---@return string | nil
function tips.get_tip(keys)
-- 输入归一化:如果输入是 string将其包装成单元素的 table
if type(keys) == 'string' then
keys = { keys }
end
for _, key in ipairs(keys) do
if key and key ~= "" then
local tip = tips_db:fetch(key)
if tip and #tip > 0 then
return tip
end
end
end
return nil
end
---@class Env
---@field current_tip string | nil 当前 tips 值
---@field last_prompt string 最后一次设置的 prompt 值
---@field tips_update_connection Connection
---tips prompt 处理
---@param context Context
---@param env Env
local function update_tips_prompt(context, env)
env.current_tip = nil
local is_tips_enabled = context:get_option("super_tips")
if not is_tips_enabled then return end
local segment = context.composition:back()
if not segment then return end
local cand = context:get_selected_candidate() or {}
if segment.selected_index == 0 then
env.current_tip = tips.get_tip({ context.input, cand.text })
else
env.current_tip = tips.get_tip(cand.text)
end
if env.current_tip ~= nil and env.current_tip ~= "" then
-- 有 tips 则直接设置 prompt
segment.prompt = "" .. env.current_tip .. ""
env.last_prompt = segment.prompt
elseif segment.prompt ~= "" and env.last_prompt == segment.prompt then
-- 没有 tips且当前 prompt 不为空,且是由 super_tips 设置的,则重置
segment.prompt = ""
env.last_prompt = segment.prompt
end
end
local P = {}
-- Processor按键触发上屏 (S)
---@param env Env
function P.init(env)
local config = env.engine.schema.config
tips.init(config)
P.tips_key = config:get_string("key_binder/tips_key")
-- 注册 tips 查找监听器
local context = env.engine.context
env.tips_update_connection = context.update_notifier:connect(
function(context)
update_tips_prompt(context, env)
end
)
end
function P.fini(env)
-- 清理连接
if env.tips_update_connection then
env.tips_update_connection:disconnect()
env.tips_update_connection = nil
end
end
---@param key KeyEvent
---@param env Env
---@return ProcessResult
function P.func(key, env)
local context = env.engine.context
local is_tips_enabled = context:get_option("super_tips")
if not is_tips_enabled then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
-- 以下处理 tips 上屏逻辑
if not P.tips_key -- 未设置上屏键
or P.tips_key ~= key:repr() -- 或者当前按下的不是上屏键
or wanxiang.is_function_mode_active(context) -- 或者是功能模式不用上屏
or not env.current_tip or env.current_tip == "" -- 或匹配的 tips 为空/空字符串
then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
---@type string 从 tips 内容中获取上屏文本
local commit_txt = env.current_tip:match("%s*(.*)%s*") -- 优先匹配常规的全角冒号
or env.current_tip:match(":%s*(.*)%s*") -- 没有匹配则回落到半角冒号
if commit_txt and #commit_txt > 0 then
env.engine:commit_text(commit_txt)
context:clear()
return wanxiang.RIME_PROCESS_RESULTS.kAccepted
end
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
return P

6853
lua/tips/tips_show.txt Normal file

File diff suppressed because it is too large Load Diff

128
lua/tone_fallback.lua Normal file
View File

@@ -0,0 +1,128 @@
-- 欢迎使用万象拼音方案
-- @amzxyz
-- https://github.com/amzxyz/rime_wanxiang
-- 用来在声调辅助的时候当你输入 2 个数字的时候自动将声调替换为第二个数字,
-- 也就是说你发现输入错误声调你可以手动轮巡输入而不用回退删除直接输入下一个即可
-- 兼容小键盘输入中不回退
local wanxiang = require("wanxiang")
-- 将目标字符的连续段压缩为“最后一个字符”
local function compress_runs_keep_last(text)
local changed = false
local out = text:gsub('([:"<>7890])([:"<>7890]+)', function(_, tail)
changed = true
return tail:sub(-1)
end)
return out, changed
end
local function should_ignore(ctx)
return wanxiang.is_function_mode_active(ctx) or ctx.input == ""
end
-- 小键盘 keycode 判定0xFFB0 ~ 0xFFB9
local function is_kp_digit_keycode(kc)
return kc >= 0xFFB0 and kc <= 0xFFB9
end
---@class Env
---@field tone_state "idle"|"skip"|"compress"
---@field tone_fallback_update_connection Connection|nil
local P = {}
function P.init(env)
env.tone_state = "idle"
local ctx = env.engine and env.engine.context
if not ctx or not ctx.update_notifier then return end
env.tone_fallback_update_connection = ctx.update_notifier:connect(function(c)
if should_ignore(c) then
env.tone_state = "idle"
return
end
-- 只在当前按键对应的一次更新里消费 state
local state = env.tone_state or "idle"
env.tone_state = "idle" -- 立刻消费,避免“下一次才生效”
-- 小键盘数字:标记为 skip本次直接不压缩
if state == "skip" then
return
end
-- 非压缩状态:直接不动
if state ~= "compress" then
return
end
-- 压缩逻辑
local input = c.input
local caret = (c.caret_pos ~= nil) and c.caret_pos or #input
if caret < 0 then caret = 0 end
if caret > #input then caret = #input end
-- 仅处理光标左侧;右侧保持不变
local left = (caret > 0) and input:sub(1, caret) or ""
local right = (caret < #input) and input:sub(caret + 1) or ""
local left_new, changed = compress_runs_keep_last(left)
if not changed then return end
-- 只改左侧,避免干扰右侧;并精确设置 caret_pos
if caret > 0 then c:pop_input(caret) end
if #left_new > 0 then c:push_input(left_new) end
if c.caret_pos ~= nil then c.caret_pos = #left_new end
-- 右侧 right 不需处理Rime 会保持不变
end)
end
function P.fini(env)
if env.tone_fallback_update_connection then
env.tone_fallback_update_connection:disconnect()
env.tone_fallback_update_connection = nil
end
env.tone_state = "idle"
end
---@return ProcessResult
function P.func(key, env)
local ctx = env.engine.context
if should_ignore(ctx) then
env.tone_state = "idle"
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
-- 小键盘数字:标记这次按键为 skip本轮 update_notifier 不压缩
local kc = key.keycode
if is_kp_digit_keycode(kc) then
env.tone_state = "skip"
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
-- 主键盘数字 09标记为 compress
local r = key:repr() or ""
if r:match("^[0-9]$") then
env.tone_state = "compress"
-- 这里用“预测压缩是否会发生”来决定要不要告诉 Rime “我处理了这个按键”
local input = ctx.input or ""
local caret = (ctx.caret_pos ~= nil) and ctx.caret_pos or #input
if caret < 0 then caret = 0 end
if caret > #input then caret = #input end
local left = (caret > 0) and input:sub(1, caret) or ""
local _, changed = compress_runs_keep_last(left)
return changed and wanxiang.RIME_PROCESS_RESULTS.kAccepted
or wanxiang.RIME_PROCESS_RESULTS.kNoop
end
-- 其它按键:不触发压缩,也不跳过
env.tone_state = "idle"
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
return P

32
lua/unicode.lua Normal file
View File

@@ -0,0 +1,32 @@
-- Unicode
-- 示例:输入 U62fc 得到「拼」
-- 触发前缀默认为 recognizer/patterns/unicode 的第 2 个字符,即 U
-- 2024.02.26: 限定编码最大值
local function unicode(input, seg, env)
-- 获取 recognizer/patterns/unicode 的第 2 个字符作为触发前缀
env.unicode_keyword = env.unicode_keyword or
env.engine.schema.config:get_string('recognizer/patterns/unicode'):sub(2, 2)
if seg:has_tag("unicode") and env.unicode_keyword ~= '' and input:sub(1, 1) == env.unicode_keyword then
local ucodestr = input:match(env.unicode_keyword .. "(%x+)")
if ucodestr and #ucodestr > 1 then
local segment = env.engine.context.composition:back()
-- 设置标签
segment.tags = segment.tags + Set({ "unicode" })
local code = tonumber(ucodestr, 16)
if code > 0x10FFFF then
yield(Candidate("unicode", seg.start, seg._end, "数值超限!", ""))
return
end
local text = utf8.char(code)
yield(Candidate("unicode", seg.start, seg._end, text, string.format("U%x", code)))
if code < 0x10000 then
for i = 0, 15 do
local text = utf8.char(code * 16 + i)
yield(Candidate("unicode", seg.start, seg._end, text, string.format("U%x~%x", code, i)))
end
end
end
end
end
return unicode

16
lua/version_display.lua Normal file
View File

@@ -0,0 +1,16 @@
local wanxiang = require("wanxiang")
--输入'/wx',显示万象项目地址和当前版本号
local function translator(input, seg, env)
if input == "/wx" then
-- 候选1: GitHub 网址
yield(Candidate("url", seg.start, seg._end, "https://github.com/amzxyz/rime_wanxiang", ""))
-- 候选2: CNB 网址
yield(Candidate("url", seg.start, seg._end, "https://cnb.cool/amzxyz/rime-wanxiang", ""))
-- 判断是否为专业版
local version_prefix = wanxiang.is_pro_scheme(env) and "增强版" or "标准版"
-- 候选3: 当前版本号(加上“增强版”或“标准版”前缀)
yield(Candidate("version", seg.start, seg._end, version_prefix .. wanxiang.version, ""))
end
end
return translator

710
lua/wanxiang.lua Normal file
View File

@@ -0,0 +1,710 @@
---@diagnostic disable: undefined-global
-- 万象的一些共用工具函数
local wanxiang = {}
-- x-release-please-start-version
wanxiang.version = "v13.6.3"
-- x-release-please-end
-- 全局内容
---@alias PROCESS_RESULT ProcessResult
wanxiang.RIME_PROCESS_RESULTS = {
kRejected = 0, -- 表示处理器明确拒绝了这个按键,停止处理链但不返回 true
kAccepted = 1, -- 表示处理器成功处理了这个按键,停止处理链并返回 true
kNoop = 2, -- 表示处理器没有处理这个按键,继续传递给下一个处理器
}
-- 整个生命周期内不变,缓存判断结果
local is_mobile_device = nil
-- 判断是否为手机设备
---@author amzxyz
---@return boolean
function wanxiang.is_mobile_device()
local function _is_mobile_device()
local dist = rime_api.get_distribution_code_name() or ""
local user_data_dir = rime_api.get_user_data_dir() or ""
local sys_dir = rime_api.get_shared_data_dir() or ""
-- 转换为小写以便比较
local lower_dist = dist:lower()
local lower_path = user_data_dir:lower()
local sys_lower_path = sys_dir:lower()
-- 主判断:常见移动端输入法
if lower_dist == "trime" or
lower_dist == "hamster" or
lower_dist == "hamster3" or
lower_dist == "squirrel" then
return true
end
-- 补充判断路径中包含移动设备特征很可以mac的运行逻辑和手机一球样
if lower_path:find("/android/") or
lower_path:find("/mobile/") or
lower_path:find("/sdcard/") or
lower_path:find("/data/storage/") or
lower_path:find("/storage/emulated/") or
lower_path:find("applications") or
lower_path:find("library") then
return true
end
-- 补充判断路径中包含移动设备特征很可以mac的运行逻辑和手机一球样
if sys_lower_path:find("applications") or
sys_lower_path:find("library") then
return true
end
-- 特定平台判断Android/Linux
if jit and jit.os then
local os_name = jit.os:lower()
if os_name:find("android") then
return true
end
end
-- 所有检查未通过则默认为桌面设备
return false
end
if is_mobile_device == nil then
is_mobile_device = _is_mobile_device()
end
return is_mobile_device
end
--- 检测是否为万象专业版
---@param env Env
---@return boolean
function wanxiang.is_pro_scheme(env)
-- local schema_name = env.engine.schema.schema_name
-- return schema_name:gsub("PRO$", "") ~= schema_name
return env.engine.schema.schema_id == "wanxiang_pro"
end
-- 以 `tag` 方式检测是否处于反查模式
function wanxiang.is_in_radical_mode(env)
local seg = env.engine.context.composition:back()
return seg and (
seg:has_tag("wanxiang_reverse")
) or false
end
---判断是否在命令模式
---@param context Context | nil
---@return boolean
function wanxiang.is_function_mode_active(context)
if not context or not context.composition or context.composition:empty() then
return false
end
local seg = context.composition:back()
if not seg then return false end
return seg:has_tag("number") or -- number_translator.lua 数字金额转换 R+数字
seg:has_tag("unicode") or -- unicode.lua 输出 Unicode 字符 U+小写字母或数字
--seg:has_tag("punct") or -- 标点符号 全角半角提示
seg:has_tag("calculator") or -- super_calculator.lua V键计算器
seg:has_tag("shijian") or -- shijian.lua /rq /sr 等与时间日期相关功能
seg:has_tag("Ndate") -- shijian.lua N日期功能
end
---判断文件是否存在
function wanxiang.file_exists(filename)
local f = io.open(filename, "r")
if f ~= nil then
io.close(f)
return true
else
return false
end
end
-- 判断字符是否为汉字
function wanxiang.IsChineseCharacter(text)
local codepoint = utf8.codepoint(text)
return
(codepoint >= 0x4E00 and codepoint <= 0x9FFF) -- Basic
or (codepoint >= 0x3400 and codepoint <= 0x4DBF) -- Ext A
or (codepoint >= 0x20000 and codepoint <= 0x2A6DF) -- Ext B
or (codepoint >= 0x2A700 and codepoint <= 0x2B73F) -- Ext C
or (codepoint >= 0x2B740 and codepoint <= 0x2B81F) -- Ext D
or (codepoint >= 0x2B820 and codepoint <= 0x2CEAF) -- Ext E
or (codepoint >= 0x2CEB0 and codepoint <= 0x2EBEF) -- Ext F
or (codepoint >= 0x30000 and codepoint <= 0x3134F) -- Ext G
or (codepoint >= 0x31350 and codepoint <= 0x323AF) -- Ext H
or (codepoint >= 0x2EBF0 and codepoint <= 0x2EE5F) -- Ext I
or (codepoint >= 0xF900 and codepoint <= 0xFAFF) -- Compatibility
or (codepoint >= 0x2F800 and codepoint <= 0x2FA1F) -- Compatibility Supplement
or (codepoint >= 0x2E80 and codepoint <= 0x2EFF) -- Radicals Supplement
or (codepoint >= 0x2F00 and codepoint <= 0x2FDF) -- Kangxi Radicals
end
---按照优先顺序获取文件:用户目录 > 系统目录
---@param filename string 相对路径
---@retur string | nil
function wanxiang.get_filename_with_fallback(filename)
local _path = filename:gsub("^/+", "") -- 去掉开头的斜杠
local user_path = rime_api.get_user_data_dir() .. '/' .. _path
if wanxiang.file_exists(user_path) then
return user_path
end
local shared_path = rime_api.get_shared_data_dir() .. '/' .. _path
if wanxiang.file_exists(shared_path) then
return shared_path
end
return nil
end
-- 按照优先顺序加载文件:用户目录 > 系统目录
---@param filename string 相对路径
---@retur file* | nil, function
function wanxiang.load_file_with_fallback(filename, mode)
mode = mode or "r" -- 默认读取模式
local _filename = wanxiang.get_filename_with_fallback(filename)
local file, err
local function close()
if not file then return end
file:close()
file = nil
end
if _filename then
file, err = io.open(_filename, mode)
end
return file, close, err
end
local USER_ID_DEFAULT = "unknown"
---作为「小狼毫」和「仓」 `rime_api.get_user_id()` 的一个 workaround
---详见:
---1. https://github.com/rime/weasel/pull/1649
---2. https://github.com/rime/librime/issues/1038
---@return string
function wanxiang.get_user_id()
local user_id = rime_api.get_user_id()
if user_id ~= USER_ID_DEFAULT then return user_id end
local user_data_dir = rime_api.get_user_data_dir()
local installation_path = user_data_dir .. "/installation.yaml"
local installation_file, _ = io.open(installation_path, "r")
if not installation_file then return user_id end
for line in installation_file:lines() do
local key, value = line:match('^([^#:]+):%s+"?([^"]%S+[^"])"?')
if key == "installation_id" then
user_id = value
break
end
end
installation_file:close()
return user_id
end
wanxiang.INPUT_METHOD_MARKERS = {
[""] = "pinyin", --全拼
[""] = "zrm", --自然码双拼
[""] = "flypy", --小鹤双拼
[""] = "mspy", --微软双拼
[""] = "sogou", --搜狗双拼
[""] = "abc", --智能abc双拼
[""] = "ziguang", --紫光双拼
[""] = "pyjj", --拼音加加
[""] = "gbpy", --国标双拼
[""] = "wxsp", --万象双拼
[""] = "zrlong", --自然龙
[""] = "hxlong", --汉心龙
[""] = "lxsq", --乱序17
[""] = "", -- 间接辅助标记:命中则额外返回 md="ⅲ"
}
local __input_type_cache = {} -- 缓存首个命中的 id兼容旧用法
local __input_md_cache = {} -- 新增:是否命中“ⅲ”(若命中则为 "ⅲ",否则为 nil
--- 根据 speller/algebra 中的特殊符号返回输入类型:
--- - 若未命中“ⅲ”,只返回 id保持旧行为
--- - 若命中“ⅲ”返回两个值id, "ⅲ"
---@param env Env
---@return string -- id
---@return string|nil -- md仅在命中“ⅲ”时返回 "ⅲ"
function wanxiang.get_input_method_type(env)
local schema_id = env.engine.schema.schema_id or "unknown"
-- 命中缓存则按是否有 md 决定返回 1 个或 2 个值
local cached_id = __input_type_cache[schema_id]
if cached_id then
local cached_md = __input_md_cache[schema_id]
if cached_md then
return cached_id, cached_md -- 返回两个值id, "ⅲ"
else
return cached_id -- 只返回 id
end
end
local cfg = env.engine.schema.config
local result_id = "unknown"
local md = nil -- 只有命中“ⅲ”时设为 "ⅲ"
local n = cfg:get_list_size("speller/algebra")
for i = 0, n - 1 do
local s = cfg:get_string(("speller/algebra/@%d"):format(i))
if s then
-- 不提前返回:需要把整段都扫描完,才能知道是否命中“ⅲ”
for symbol, id in pairs(wanxiang.INPUT_METHOD_MARKERS) do
if s:find(symbol, 1, true) then
if symbol == "" or id == "" then
md = "" -- 记录辅助标记
else
if result_id == "unknown" then
result_id = id -- 只记录第一个“正常映射”的 id
end
end
end
end
end
end
-- 写缓存
__input_type_cache[schema_id] = result_id
__input_md_cache[schema_id] = md -- 命中则为 "ⅲ",否则为 nil
-- 返回:命中“ⅲ”→两个值;否则一个值
if md then
return result_id, md
else
return result_id
end
end
wanxiang.tone_matrix = {
["a"] = {1,2,3,4},
["ai"] = {1,2,3,4},
["an"] = {1,2,3,4},
["ang"] = {1,2,3,4},
["ao"] = {1,2,3,4},
["ba"] = {1,2,3,4},
["bai"] = {1,2,3,4},
["ban"] = {1,3,4},
["bang"] = {1,3,4},
["bao"] = {1,2,3,4},
["bei"] = {1,3,4},
["ben"] = {1,3,4},
["beng"] = {1,2,3,4},
["bi"] = {1,2,3,4},
["bian"] = {1,3,4},
["biang"] = {2},
["biao"] = {1,2,3,4},
["bie"] = {1,2,3,4},
["bin"] = {1,4},
["bing"] = {1,3,4},
["bo"] = {1,2,3,4},
["bu"] = {1,2,3,4},
["bun"] = {1},
["ca"] = {1,3,4},
["cai"] = {1,2,3,4},
["can"] = {1,2,3,4},
["cang"] = {1,2,4},
["cao"] = {1,2,3,4},
["ce"] = {4},
["cei"] = {4},
["cen"] = {1,2},
["ceng"] = {1,2,4},
["ceok"] = {},
["ceon"] = {},
["cha"] = {1,2,3,4},
["chai"] = {1,2,3,4},
["chan"] = {1,2,3,4},
["chang"] = {1,2,3,4},
["chao"] = {1,2,3,4},
["che"] = {1,2,3,4},
["chen"] = {1,2,3,4},
["cheng"] = {1,2,3,4},
["chi"] = {1,2,3,4},
["chong"] = {1,2,3,4},
["chou"] = {1,2,3,4},
["chu"] = {1,2,3,4},
["chua"] = {1,3,4},
["chuai"] = {1,2,3,4},
["chuan"] = {1,2,3,4},
["chuang"] = {1,2,3,4},
["chui"] = {1,2,4},
["chun"] = {1,2,3},
["chuo"] = {1,4},
["ci"] = {1,2,3,4},
["cong"] = {1,2,3,4},
["cou"] = {1,2,3,4},
["cu"] = {1,2,3,4},
["cuan"] = {1,2,4},
["cui"] = {1,3,4},
["cun"] = {1,2,3,4},
["cuo"] = {1,2,3,4},
["da"] = {1,2,3,4},
["dai"] = {1,3,4},
["dan"] = {1,3,4},
["dang"] = {1,3,4},
["dao"] = {1,2,3,4},
["de"] = {1,2},
["dei"] = {1,3},
["den"] = {4},
["deng"] = {1,3,4},
["di"] = {1,2,3,4},
["dia"] = {3},
["dian"] = {1,2,3,4},
["diao"] = {1,3,4},
["die"] = {1,2,3,4},
["dim"] = {2},
["din"] = {4},
["ding"] = {1,3,4},
["diu"] = {1},
["dong"] = {1,3,4},
["dou"] = {1,2,3,4},
["du"] = {1,2,3,4},
["duan"] = {1,3,4},
["dui"] = {1,3,4},
["dun"] = {1,3,4},
["duo"] = {1,2,3,4},
["e"] = {1,2,3,4},
["ei"] = {1,2,3,4},
["en"] = {1,3,4},
["eng"] = {1},
["er"] = {2,3,4},
["fa"] = {1,2,3,4},
["fan"] = {1,2,3,4},
["fang"] = {1,2,3,4},
["fei"] = {1,2,3,4},
["fen"] = {1,2,3,4},
["feng"] = {1,2,3,4},
["fiao"] = {4},
["fo"] = {2},
["fou"] = {1,2,3},
["fu"] = {1,2,3,4},
["ga"] = {1,2,3,4},
["gai"] = {1,3,4},
["gan"] = {1,3,4},
["gang"] = {1,3,4},
["gao"] = {1,3,4},
["ge"] = {1,2,3,4},
["gei"] = {3},
["gen"] = {1,2,3,4},
["geng"] = {1,3,4},
["gong"] = {1,3,4},
["gou"] = {1,3,4},
["gu"] = {1,2,3,4},
["gua"] = {1,2,3,4},
["guai"] = {1,3,4},
["guan"] = {1,3,4},
["guang"] = {1,3,4},
["gui"] = {1,3,4},
["gun"] = {3,4},
["guo"] = {1,2,3,4},
["ha"] = {1,2,3,4},
["hai"] = {1,2,3,4},
["han"] = {1,2,3,4},
["hang"] = {1,2,4},
["hao"] = {1,2,3,4},
["he"] = {1,2,3,4},
["hei"] = {1},
["hen"] = {2,3,4},
["heng"] = {1,2,4},
["hong"] = {1,2,3,4},
["hou"] = {1,2,3,4},
["hu"] = {1,2,3,4},
["hua"] = {1,2,4},
["huai"] = {2,4},
["huan"] = {1,2,3,4},
["huang"] = {1,2,3,4},
["hui"] = {1,2,3,4},
["hun"] = {1,2,3,4},
["huo"] = {1,2,3,4},
["ji"] = {1,2,3,4},
["jia"] = {1,2,3,4},
["jian"] = {1,3,4},
["jiang"] = {1,3,4},
["jiao"] = {1,2,3,4},
["jie"] = {1,2,3,4},
["jin"] = {1,3,4},
["jing"] = {1,3,4},
["jiong"] = {1,3,4},
["jiu"] = {1,2,3,4},
["ju"] = {1,2,3,4},
["juan"] = {1,3,4},
["jue"] = {1,2,3,4},
["jun"] = {1,3,4},
["ka"] = {1,3},
["kai"] = {1,3,4},
["kan"] = {1,3,4},
["kang"] = {1,2,3,4},
["kao"] = {1,3,4},
["ke"] = {1,2,3,4},
["kei"] = {1},
["ken"] = {1,3,4},
["keng"] = {1,3},
["kong"] = {1,3,4},
["kou"] = {1,3,4},
["ku"] = {1,2,3,4},
["kua"] = {1,3,4},
["kuai"] = {2,3,4},
["kuan"] = {1,3,4},
["kuang"] = {1,2,3,4},
["kui"] = {1,2,3,4},
["kun"] = {1,3,4},
["kuo"] = {4},
["la"] = {1,2,3,4},
["lai"] = {2,3,4},
["lan"] = {2,3,4},
["lang"] = {1,2,3,4},
["lao"] = {1,2,3,4},
["le"] = {1,4},
["lei"] = {1,2,3,4},
["leng"] = {1,2,3,4},
["li"] = {1,2,3,4},
["lia"] = {3},
["lian"] = {2,3,4},
["liang"] = {1,2,3,4},
["liao"] = {1,2,3,4},
["lie"] = {1,2,3,4},
["lin"] = {1,2,3,4},
["ling"] = {2,3,4},
["liu"] = {1,2,3,4},
["lo"] = {},
["long"] = {1,2,3,4},
["lou"] = {1,2,3,4},
["lu"] = {1,2,3,4},
["luan"] = {2,3,4},
["lun"] = {1,2,3,4},
["luo"] = {1,2,3,4},
["lv"] = {2,3,4},
["lve"] = {4},
["ma"] = {1,2,3,4},
["mai"] = {2,3,4},
["man"] = {1,2,3,4},
["mang"] = {1,2,3,4},
["mao"] = {1,2,3,4},
["me"] = {1,4},
["mei"] = {2,3,4},
["men"] = {1,2,4},
["meng"] = {1,2,3,4},
["mi"] = {1,2,3,4},
["mian"] = {2,3,4},
["miao"] = {1,2,3,4},
["mie"] = {1,2,4},
["min"] = {2,3},
["ming"] = {2,3,4},
["miu"] = {3,4},
["mo"] = {1,2,3,4},
["mou"] = {1,2,3,4},
["mu"] = {2,3,4},
[""] = {},
["n"] = {2,3,4},
["na"] = {1,2,3,4},
["nai"] = {2,3,4},
["nan"] = {1,2,3,4},
["nang"] = {1,2,3,4},
["nao"] = {1,2,3,4},
["ne"] = {2,4},
["nei"] = {2,3,4},
["nen"] = {4},
["neng"] = {2,3,4},
["ng"] = {2,3,4},
["ni"] = {1,2,3,4},
["nian"] = {1,2,3,4},
["niang"] = {2,3,4},
["niao"] = {3,4},
["nie"] = {1,2,3,4},
["nin"] = {2,3},
["ning"] = {2,3,4},
["niu"] = {1,2,3,4},
["nong"] = {2,3,4},
["nou"] = {2,3,4},
["nu"] = {2,3,4},
["nuan"] = {2,3,4},
["nun"] = {2},
["nuo"] = {2,3,4},
["nv"] = {2,3,4},
["nve"] = {4},
["o"] = {1,2,3,4},
["ou"] = {1,2,3,4},
["pa"] = {1,2,3,4},
["pai"] = {1,2,3,4},
["pan"] = {1,2,3,4},
["pang"] = {1,2,3,4},
["pao"] = {1,2,3,4},
["pei"] = {1,2,3,4},
["pen"] = {1,2,3,4},
["peng"] = {1,2,3,4},
["pi"] = {1,2,3,4},
["pian"] = {1,2,3,4},
["piao"] = {1,2,3,4},
["pie"] = {1,3,4},
["pin"] = {1,2,3,4},
["ping"] = {1,2,4},
["po"] = {1,2,3,4},
["pou"] = {1,2,3},
["pu"] = {1,2,3,4},
["qi"] = {1,2,3,4},
["qia"] = {1,2,3,4},
["qian"] = {1,2,3,4},
["qiang"] = {1,2,3,4},
["qiao"] = {1,2,3,4},
["qie"] = {1,2,3,4},
["qin"] = {1,2,3,4},
["qing"] = {1,2,3,4},
["qiong"] = {1,2,4},
["qiu"] = {1,2,3,4},
["qu"] = {1,2,3,4},
["quan"] = {1,2,3,4},
["que"] = {1,2,4},
["qun"] = {1,2,3},
["ran"] = {2,3,4},
["rang"] = {1,2,3,4},
["rao"] = {2,3,4},
["re"] = {2,3,4},
["ren"] = {2,3,4},
["reng"] = {1,2},
["ri"] = {4},
["rong"] = {2,3,4},
["rou"] = {2,3,4},
["ru"] = {1,2,3,4},
["rua"] = {2},
["ruan"] = {2,3,4},
["rui"] = {2,3,4},
["run"] = {2,3,4},
["ruo"] = {2,4},
["sa"] = {1,2,3,4},
["sai"] = {1,3,4},
["san"] = {1,3,4},
["sang"] = {1,3,4},
["sao"] = {1,3,4},
["se"] = {1,4},
["sen"] = {1,3},
["seng"] = {1,4},
["sha"] = {1,2,3,4},
["shai"] = {1,3,4},
["shan"] = {1,2,3,4},
["shang"] = {1,3,4},
["shao"] = {1,2,3,4},
["she"] = {1,2,3,4},
["shei"] = {2},
["shen"] = {1,2,3,4},
["sheng"] = {1,2,3,4},
["shi"] = {1,2,3,4},
["shou"] = {1,2,3,4},
["shu"] = {1,2,3,4},
["shua"] = {1,3,4},
["shuai"] = {1,3,4},
["shuan"] = {1,4},
["shuang"] = {1,3,4},
["shui"] = {2,3,4},
["shun"] = {3,4},
["shuo"] = {1,4},
["si"] = {1,2,3,4},
["song"] = {1,2,3,4},
["sou"] = {1,3,4},
["su"] = {1,2,3,4},
["suan"] = {1,3,4},
["sui"] = {1,2,3,4},
["sun"] = {1,3,4},
["suo"] = {1,2,3,4},
["ta"] = {1,2,3,4},
["tai"] = {1,2,3,4},
["tan"] = {1,2,3,4},
["tang"] = {1,2,3,4},
["tao"] = {1,2,3,4},
["te"] = {4},
["tei"] = {1},
["teng"] = {1,2,4},
["ti"] = {1,2,3,4},
["tian"] = {1,2,3,4},
["tiao"] = {1,2,3,4},
["tie"] = {1,2,3,4},
["tii"] = {2},
["ting"] = {1,2,3,4},
["tong"] = {1,2,3,4},
["tou"] = {1,2,3,4},
["tu"] = {1,2,3,4},
["tuan"] = {1,2,3,4},
["tui"] = {1,2,3,4},
["tun"] = {1,2,3,4},
["tuo"] = {1,2,3,4},
["wa"] = {1,2,3,4},
["wai"] = {1,3,4},
["wan"] = {1,2,3,4},
["wang"] = {1,2,3,4},
["wei"] = {1,2,3,4},
["wen"] = {1,2,3,4},
["weng"] = {1,3,4},
["wo"] = {1,3,4},
["wu"] = {1,2,3,4},
["xi"] = {1,2,3,4},
["xia"] = {1,2,3,4},
["xian"] = {1,2,3,4},
["xiang"] = {1,2,3,4},
["xiao"] = {1,2,3,4},
["xie"] = {1,2,3,4},
["xin"] = {1,2,3,4},
["xing"] = {1,2,3,4},
["xiong"] = {1,2,4},
["xiu"] = {1,2,3,4},
["xu"] = {1,2,3,4},
["xuan"] = {1,2,3,4},
["xue"] = {1,2,3,4},
["xun"] = {1,2,4},
["ya"] = {1,2,3,4},
["yan"] = {1,2,3,4},
["yang"] = {1,2,3,4},
["yao"] = {1,2,3,4},
["ye"] = {1,2,3,4},
["yi"] = {1,2,3,4},
["yin"] = {1,2,3,4},
["ying"] = {1,2,3,4},
["yo"] = {1},
["yong"] = {1,2,3,4},
["you"] = {1,2,3,4},
["yu"] = {1,2,3,4},
["yuan"] = {1,2,3,4},
["yue"] = {1,3,4},
["yun"] = {1,2,3,4},
["za"] = {1,2,3},
["zai"] = {1,3,4},
["zan"] = {1,2,3,4},
["zang"] = {1,3,4},
["zao"] = {1,2,3,4},
["ze"] = {2,4},
["zei"] = {2},
["zen"] = {1,3,4},
["zeng"] = {1,3,4},
["zha"] = {1,2,3,4},
["zhai"] = {1,2,3,4},
["zhan"] = {1,3,4},
["zhang"] = {1,3,4},
["zhao"] = {1,2,3,4},
["zhe"] = {1,2,3,4},
["zhei"] = {4},
["zhen"] = {1,2,3,4},
["zheng"] = {1,3,4},
["zhi"] = {1,2,3,4},
["zhong"] = {1,3,4},
["zhou"] = {1,2,3,4},
["zhu"] = {1,2,3,4},
["zhua"] = {1,3},
["zhuai"] = {1,3,4},
["zhuan"] = {1,3,4},
["zhuang"] = {1,3,4},
["zhui"] = {1,3,4},
["zhun"] = {1,3,4},
["zhuo"] = {1,2,4},
["zi"] = {1,2,3,4},
["zong"] = {1,3,4},
["zou"] = {1,3,4},
["zu"] = {1,2,3,4},
["zuan"] = {1,3,4},
["zui"] = {1,2,3,4},
["zun"] = {1,3,4},
["zuo"] = {1,2,3,4},
["ḿ"] = {2},
}
return wanxiang