mirror of
https://github.com/d0zingcat/rime_wanxiang.git
synced 2026-05-22 15:10:43 +00:00
chore:新的根节点
This commit is contained in:
231
lua/auto_phrase.lua
Normal file
231
lua/auto_phrase.lua
Normal 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
55
lua/backspace_limit.lua
Normal 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
BIN
lua/charset.bin
Normal file
Binary file not shown.
262
lua/input_statistics.lua
Normal file
262
lua/input_statistics.lua
Normal 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
92
lua/key_binder.lua
Normal 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
256
lua/kp_number_processor.lua
Normal 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
89
lua/letter_selector.lua
Normal 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
68
lua/lib/bit.lua
Normal 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
116
lua/lib/userdb.lua
Normal 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
722
lua/librime.lua
Normal 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
61
lua/limit_repeated.lua
Normal 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
175
lua/number_translator.lua
Normal 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
191
lua/partial_commit.lua
Normal 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
137
lua/quick_symbol_text.lua
Normal 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
41
lua/select_character.lua
Normal 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
185
lua/set_schema.lua
Normal 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
3017
lua/shijian.lua
Normal file
File diff suppressed because it is too large
Load Diff
3491
lua/super_calculator.lua
Normal file
3491
lua/super_calculator.lua
Normal file
File diff suppressed because it is too large
Load Diff
535
lua/super_comment_preedit.lua
Normal file
535
lua/super_comment_preedit.lua
Normal 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
1030
lua/super_filter.lua
Normal file
File diff suppressed because it is too large
Load Diff
589
lua/super_lookup.lua
Normal file
589
lua/super_lookup.lua
Normal 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
250
lua/super_segmentation.lua
Normal 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
734
lua/super_sequence.lua
Normal file
@@ -0,0 +1,734 @@
|
||||
-- 万象拼音 · 手动自由排序
|
||||
-- 核心规则: 向前移动 = "Control+j", 向后移动 = "Control+k", 重置 = "Control+l", 置顶 = "Control+p
|
||||
-- 1) p>0:有效排序(DB upsert + 导出)
|
||||
-- 2) p=0:墓碑(DB 删除 + 导出墓碑)
|
||||
-- 3) 初始化:先 flush 本机增量到导出 → 外部合并(所有设备文件+本机DB,LWW) → 重写本机导出(含墓碑) → 导入覆盖DB,p=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 本机增量到导出 → 外部合并(所有设备文件+本机DB,LWW) → 重写本机导出(含墓碑) → 导入覆盖DB,p=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
|
||||
|
||||
------------------------------------------------------------
|
||||
-- 十二、把“合并结果”导入覆盖 DB(p<=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) 外部合并(所有设备文件 + 本机 DB),LWW(含 p=0)
|
||||
local latest = collect_latest_from_all_sources()
|
||||
|
||||
-- 3) 用合并结果重写我机导出(包含 p=0)——始终写盘
|
||||
rewrite_export_from_latest(latest)
|
||||
|
||||
-- 4) 导入合并结果覆盖 DB(p<=0 删)
|
||||
apply_latest_to_db(latest)
|
||||
end
|
||||
|
||||
------------------------------------------------------------
|
||||
-- 十四、Pipeline:P / 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
279
lua/super_tips.lua
Normal 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
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
128
lua/tone_fallback.lua
Normal 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
|
||||
|
||||
-- 主键盘数字 0–9:标记为 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
32
lua/unicode.lua
Normal 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
16
lua/version_display.lua
Normal 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
710
lua/wanxiang.lua
Normal 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},
|
||||
["m̀"] = {},
|
||||
["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
|
||||
Reference in New Issue
Block a user