feat: 合并8个按键处理器lua为一个,KP小键盘、字母选词、符号快打、超强分词、重复限制、退格限制、声调回退、以词定字

This commit is contained in:
amzxyz
2026-01-20 16:38:29 +08:00
parent bbfc9b2272
commit 6baaef085f
12 changed files with 671 additions and 973 deletions

View File

@@ -54,17 +54,10 @@ switches:
# 输入引擎
engine:
processors:
- lua_processor@*select_character #以词定字,默认左中括号上屏一个词的前一个字,右中括号上屏一个词的后一个
- lua_processor@*super_processor #KP小键盘、字母选词、符号快打、超强分词、重复限制、退格限制、声调回退、以词定
- lua_processor@*partial_commit #通过ctrl+1~0局部提交10个字以内的句子的前几个字一般为正确的前几个使用时要遵循合理的分词结构能促进后续编码打出正确的词汇
- lua_processor@*letter_selector #在N模式R模式下输入数字被视为编码那么如何上屏呢现在除了方向键还提供qwertyuio对照1-9来选词
- lua_processor@*tone_fallback #声调辅助回退,当你输入声调数字错误时,继续输入正确的而不用回退删除
- lua_processor@*super_sequence*P #手动排序,高亮候选 ctrl+j左移动 ctrl+k 右移动 ctrl+l 移除位移 ctrl+p 置顶
- lua_processor@*quick_symbol_text #快符引导以及重复上屏配合quick_symbol_text顶层配置清单定义扩展按键
- lua_processor@*super_tips #超级提示模块:表情、简码、翻译、化学式、等等靠你想象
- lua_processor@*limit_repeated #用于限制最大候选长度以及最大重复输入声母编码长度,避免性能异常
- lua_processor@*backspace_limit #防止连续 Backspace 在编码为空时删除已上屏内容
- lua_processor@*kp_number_processor #管理主键盘小键盘的数字处理逻辑,有输入中数字不上屏和数字一直不上屏设置可选
- lua_processor@*super_segmentation #通过双击分词符号触发重新分词,并在持续输入分词符号时,能在预设方式之间循环,用于应对类似自然码:必输 必须是 为相同编码导致的必输前置的问题
- ascii_composer #处理英文模式及中英文切换
- recognizer #与 matcher 搭配,处理符合特定规则的输入码,如网址、反查等 tags
- key_binder #在特定条件下将按键绑定到其他按键,如重定义逗号、句号为候选翻页、开关快捷键等
@@ -106,7 +99,7 @@ engine:
- lua_filter@*super_filter #comment前,相关功能见Lua文件
- lua_filter@*super_english #comment前,负责英文方案及中英混输中英文单词格式化,语句流,自动加空格等策略
- lua_filter@*super_comment_preedit #OpenCC前超级注释模块、超级preedit支持错词提示、辅助码显示部件组字读音注释有声调、无声调全拼编码的转换支持个性化配置和关闭相应的功能详情搜索super_comment_preedit进行详细配置
- lua_filter@*super_replacer #用来替代OpenCC的处理器
- lua_filter@*super_replacer #OpenCC替代器,更灵活的处理方式,更自由的自定义方式
- lua_filter@*super_sequence*F #手动排序,对高亮候选 ctrl+j左移动 ctrl+k 右移动 ctrl+0 移除位移
- uniquifier #去重

View File

@@ -1,63 +0,0 @@
-- 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
-- 这里嵌入一段记录按键的逻辑,给英文空格使用
if not key:release() and ctx.composition:empty() then
-- 检测:回车 (0xff0d, 0xff8d) 或 空格 (0x20)
if kc == 0xff0d or kc == 0xff8d or kc == 0x20 then
-- 发送信号:刚才发生了空闲换行或空格,打断英文连贯性
ctx:set_property("english_spacing", "true")
end
end --嵌入结束
-- 非 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

View File

@@ -1,172 +0,0 @@
-- 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 = {}
-- [调试工具] 最小化日志打印 (如需调试请取消注释)
-- local function log_info(msg)
-- log.info("kp_number: " .. tostring(msg))
-- 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 then return false end
for _, pat in ipairs(pats) do
-- 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
local m = config:get_string("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()
-- 从 wanxiang 模块加载并转译正则
-- 这一步会自动处理 YAML 正则到 Lua 模式的所有转换
env.function_patterns = wanxiang.load_regex_patterns(config, "recognizer/patterns")
-- log_info("Loaded " .. #(env.function_patterns or {}) .. " patterns.")
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 context = env.context or env.engine.context
local mode = env.kp_mode or "auto"
local page_sz = env.page_size
-- 1) 小键盘数字处理
local kp_num = KP[key.keycode]
if kp_num ~= nil then
if key:ctrl() or key:alt() or key:super() or key:shift() then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
local ch = tostring(kp_num)
-- 如果匹配到正则(如网址、反查),则拦截,强制作为编码输入
if is_function_code_after_digit(env, context, ch) then
if context.push_input then context:push_input(ch)
else context.input = (context.input or "") .. ch end
return wanxiang.RIME_PROCESS_RESULTS.kAccepted
end
-- 正常数字逻辑
if mode == "auto" then
if env.is_composing then
if context.push_input then context:push_input(ch)
else context.input = (context.input or "") .. ch end
else
return wanxiang.RIME_PROCESS_RESULTS.kNoop
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) 主键盘数字处理
local r = key:repr() or ""
if r:match("^[0-9]$") then
if key:ctrl() or key:alt() or key:super() then
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
if is_function_code_after_digit(env, context, r) then
if context.push_input then context:push_input(r)
else context.input = (context.input or "") .. r end
return wanxiang.RIME_PROCESS_RESULTS.kAccepted
end
if env.has_menu then
local d = tonumber(r)
if d == 0 then d = 10 end
if d and d >= 1 and d <= page_sz then
local composition = context.composition
if composition and not composition:empty() then
local seg = composition:back()
local menu = seg and seg.menu
if menu and not menu:empty() then
local sel_index = seg.selected_index or 0
local page_start = math.floor(sel_index / page_sz) * page_sz
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

View File

@@ -1,89 +0,0 @@
-- @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

View File

@@ -1,61 +0,0 @@
-- 用于限制最大候选数量以及重复最大输入编码,防止卡顿性能异常
--@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

View File

@@ -1,137 +0,0 @@
-- 欢迎使用万象拼音方案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 }

View File

@@ -1,41 +0,0 @@
-- 以词定字
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

667
lua/super_processor.lua Normal file
View File

@@ -0,0 +1,667 @@
-- lua/super_processor.lua
-- @amzxyz
-- https://github.com/amzxyz/rime_wanxiang
-- 全能按键处理器:整合 KP小键盘、字母选词、符号快打、超强分词、重复限制、退格限制、声调回退、以词定字
--
-- 用法: 在 schema.yaml 中 engine/processors 列表添加 - lua_processor@*super_processor
local wanxiang = require("wanxiang")
local M = {}
local K_REJECT, K_ACCEPT, K_NOOP = 0, 1, 2
-- 1. 全局常量定义 (Constants)
-- [KpNumber] 小键盘键码映射
local KP_MAP = {
[0xFFB1] = 1, [0xFFB2] = 2, [0xFFB3] = 3,
[0xFFB4] = 4, [0xFFB5] = 5, [0xFFB6] = 6,
[0xFFB7] = 7, [0xFFB8] = 8, [0xFFB9] = 9,
[0xFFB0] = 0,
}
-- [LetterSelector] 字母选词键码映射 (qwert...)
local LETTER_SEL_MAP = {
[0x71] = 1, [0x77] = 2, [0x65] = 3, [0x72] = 4, [0x74] = 5,
[0x79] = 6, [0x75] = 7, [0x69] = 8, [0x6F] = 9, [0x70] = 10,
}
-- [QuickSymbol] 默认符号映射表
local SYMBOL_DEFAULT = {
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=""
}
-- [LimitRepeated] 重复限制配置
local MAX_REPEAT = 8
local MAX_SEGMENTS = 40
local INITIALS = "[bpmfdtnlgkhjqxrzcsywiu]"
-- [SuperSegmentation] 分词模式配置
local SEG_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} } },
}
-- 2. 核心辅助函数 (Utilities)
-- 字符串转义
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
-- 根据分组重构字符串
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
-- 获取缓存的分段长度
local function get_cached_lens(env, ctx, md, ad)
local L = env.seg_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
-- 增强版 UTF-8 长度计算 (Super Segmentation 使用)
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
local n = 0
if utf8 and utf8.codes then
for _ in utf8.codes(s) do n = n + 1 end
return n
end
return #s
end
-- 检查数字后是否紧跟功能编码 (KpNumber 使用)
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.kp_func_patterns
if not pats then return false end
for _, pat in ipairs(pats) do
if s:match(pat) then return true end
end
return false
end
-- 计算尾部重复字符数 (LimitRepeated 使用)
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
-- 设置候选框提示 (LimitRepeated 使用)
local function prompt(ctx, msg)
local comp = ctx.composition
if comp and not comp:empty() then comp:back().prompt = msg end
end
-- 压缩连续声调 (ToneFallback 使用)
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
-- 3. 初始化与资源管理 (Init & Fini)
function M.init(env)
local engine = env.engine
local config = engine.schema.config
local context = engine.context
-- [1] 配置加载 (按功能模块分类)
-- [BackspaceLimit]
env.bs_prev_len = -1
env.bs_sequence = false
-- [KpNumber] 小键盘
env.kp_page_size = config:get_int("menu/page_size") or 6
local m = config:get_string("kp_number_mode") or "auto"
env.kp_mode = (m == "auto" or m == "compose") and m or "auto"
env.kp_func_patterns = wanxiang.load_regex_patterns(config, "recognizer/patterns")
-- [LetterSelector] 字母选词状态位
env.ls_active = false
-- [ToneFallback] 声调容错
env.tone_state = "idle"
env.lookup_key = config:get_string('wanxiang_lookup/key') or '`'
-- [QuickSymbol] 符号快打
env.qs_trigger = "^([a-z])/$"
if config then
local ok, s = pcall(function() return config:get_string("quick_symbol_text/trigger") end)
if ok and type(s)=="string" and #s>0 then env.qs_trigger = s end
end
env.qs_mapping = {}
for k, v in pairs(SYMBOL_DEFAULT) do env.qs_mapping[k] = v end
local ok_map, map = pcall(function() return config:get_map("quick_symbol_text/symkey") end)
if ok_map and map then
local ok_keys, keys = pcall(function() return map:keys() end)
if ok_keys and keys then
for _, key in ipairs(keys) do
local v = config:get_string("quick_symbol_text/symkey/" .. key)
if v then env.qs_mapping[string.lower(tostring(key))] = v end
end
end
end
env.qs_last_commit = "欢迎使用万象拼音!"
-- [SelectCharacter] 以词定字
env.sc_first_key = config:get_string('key_binder/select_first_character')
env.sc_last_key = config:get_string('key_binder/select_last_character')
-- [SuperSegmentation] 超强分词
local delim = config:get_string("speller/delimiter") or " '"
env.seg_auto_delim = delim:sub(1,1)
env.seg_manual_delim = delim:sub(2,2)
env.seg_core = nil
env.seg_start_idx = nil
env.seg_N = nil
env.seg_base = nil
-- [2] 统一 Update Notifier (状态缓存与自动处理)
env.conn_update = context.update_notifier:connect(function(ctx)
-- A. [ToneFallback] 执行声调压缩
local t_state = env.tone_state or "idle"
env.tone_state = "idle"
local input = ctx.input or ""
if t_state == "compress" and input ~= "" then
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 left_new, changed = compress_runs_keep_last(left)
if changed then
if caret > 0 then ctx:pop_input(caret) end
if #left_new > 0 then ctx:push_input(left_new) end
end
end
-- B. [SuperSegmentation] 缓存数据
local seg = ctx.composition:back()
local cand = seg and seg:get_selected_candidate() or nil
local pre = cand and cand.preedit or nil
env.seg_last_preedit_lens = lens_from_string(pre, env.seg_manual_delim, env.seg_auto_delim)
env.seg_last_input_caret = input
env.seg_last_caret_pos = ctx.caret_pos
-- C. [LetterSelector] 缓存激活状态
env.ls_active = false
if not ctx.composition:empty() then
local s = ctx.composition:back()
if s and (s:has_tag("number") or s:has_tag("Ndate")) then
env.ls_active = true
end
end
-- D. [KpNumber] 缓存状态
env.kp_is_composing = ctx:is_composing()
env.kp_has_menu = ctx:has_menu()
-- E. [QuickSymbol] 自动上屏逻辑
local qkey = string.match(input, env.qs_trigger)
if qkey then
qkey = string.lower(qkey)
local symbol = env.qs_mapping[qkey]
if symbol and symbol ~= "" then
if type(symbol)=="string" and symbol:lower()=="repeat" then
if env.qs_last_commit ~= "" then
engine:commit_text(env.qs_last_commit)
ctx:clear()
end
else
engine:commit_text(symbol)
ctx:clear()
end
end
end
end)
-- [3] 统一 Commit Notifier (记录上屏)
env.conn_commit = context.commit_notifier:connect(function(ctx)
local t = ctx:get_commit_text()
if t ~= "" then env.qs_last_commit = t end
end)
end
function M.fini(env)
if env.conn_update then env.conn_update:disconnect(); env.conn_update = nil end
if env.conn_commit then env.conn_commit:disconnect(); env.conn_commit = nil end
env.memory = nil
end
-- 4. 逻辑分发处理 (Handlers)
-- [QuickSymbol] 拦截触发键,防止进入 Speller
local function handle_quick_symbol_intercept(key, env, ctx)
local input = ctx.input or ""
local matched = string.match(input, env.qs_trigger)
if matched then
local k = string.lower(matched)
if env.qs_mapping[k] and env.qs_mapping[k] ~= "" then
return true -- Accepted
end
end
return false
end
-- [SuperSegmentation] 处理分词符 '
local function handle_segmentation(key, env, ctx)
if key.keycode ~= string.byte(env.seg_manual_delim) then
env.seg_core, env.seg_start_idx, env.seg_N, env.seg_base = nil, nil, nil, nil
return false
end
if ctx.composition:empty() then return false end
local last_input = env.seg_last_input_caret or ctx.input or ""
local last_caret = env.seg_last_caret_pos
if not last_caret or last_caret ~= ulen(last_input) then
env.seg_core, env.seg_start_idx, env.seg_N, env.seg_base = nil, nil, nil, nil
return false
end
local md = env.seg_manual_delim
local before = ctx.input or ""
local after = before .. md
local tlen = count_trailing(after, md)
local head = strip_trailing(after, md)
local core = strip_delims(head, md, env.seg_auto_delim)
local N = #core
local conf = SEG_PATTERNS[N]
-- 大于 10 码动态构建分词在2、3码之间循环
if N > 10 then
local groups_2 = {}
for i = 1, math.floor(N / 2) do table.insert(groups_2, 2) end
if N % 2 ~= 0 then table.insert(groups_2, N % 2) end
local groups_3 = {}
for i = 1, math.floor(N / 3) do table.insert(groups_3, 3) end
if N % 3 ~= 0 then table.insert(groups_3, N % 3) end
conf = { all = { groups_2, groups_3 } }
end
if env.seg_core ~= core or env.seg_N ~= N then
env.seg_core = core
env.seg_N = N
env.seg_start_idx = nil
env.seg_base = nil
end
if env.seg_base == nil then env.seg_base = head end
if conf and env.seg_start_idx == nil then
local start_idx = 0
local L = get_cached_lens(env, ctx, md, env.seg_auto_delim)
if not (L and sum(L)==N) then L = lens_from_string(head, md, env.seg_auto_delim) 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.seg_start_idx = start_idx
end
if tlen == 1 then
ctx.input = after
return true
end
if not conf then
ctx.input = after
return true
end
local m = #conf.all
local k = tlen - 1
local function restore()
ctx.input = (env.seg_base or head) .. md
env.seg_core, env.seg_start_idx, env.seg_N, env.seg_base = nil, nil, nil, nil
env.seg_core = core; env.seg_N = N
end
if env.seg_start_idx and env.seg_start_idx ~= 0 then
local cycle_len = m
local r = k % cycle_len
if r == 0 then restore(); return true end
local idx = ((env.seg_start_idx - 1 + r) % m) + 1
local rebuilt = build_by_groups(core, md, conf.all[idx])
ctx.input = rebuilt .. md:rep(tlen)
return true
else
local cycle_len = m + 1
local r = k % cycle_len
if r == 0 then restore(); return true end
local idx = ((r - 1) % m) + 1
local rebuilt = build_by_groups(core, md, conf.all[idx])
ctx.input = rebuilt .. md:rep(tlen)
return true
end
end
-- [Backspace Limit] 退格限制
local function handle_backspace(key, env, ctx)
local kc = key.keycode
if not key:release() and ctx.composition:empty() then
if kc == 0xff0d or kc == 0xff8d or kc == 0x20 then
ctx:set_property("english_spacing", "true") --记下来按键状态给英文自动加空格用
end
end
if kc ~= 0xFF08 or key:release() then
env.bs_sequence = false
env.bs_prev_len = -1
return false
end
local cur_len = ctx.input and #ctx.input or 0
if env.bs_sequence then
if not wanxiang.is_mobile_device() then
if env.bs_prev_len == 1 and cur_len == 0 then
return true
end
end
env.bs_prev_len = cur_len
return false
end
env.bs_sequence = true
env.bs_prev_len = cur_len
return false
end
-- [Limit Repeated] 重复输入限制
local function handle_limit_repeat(key, env, ctx)
local kc = key.keycode
if not (kc >= 0x61 and kc <= 0x7A) then return false end
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 = string.char(kc)
local input = ctx.input or ""
local nxt = input .. ch
local last, rep_n = tail_rep(nxt)
if last:match(INITIALS) and rep_n > MAX_REPEAT then
prompt(ctx, " 〔已超最大重复声母〕")
return true
end
if segs >= MAX_SEGMENTS then
prompt(ctx, " 〔已超最大输入长度〕")
return true
end
return false
end
-- [Letter Selector] 字母选词
local function handle_letter_select(key, env, ctx)
if not env.ls_active then return false end
if key:ctrl() or key:alt() or key:super() then return false end
local idx = LETTER_SEL_MAP[key.keycode]
if not idx then return false end
if ctx.composition:empty() then return false end
local seg = ctx.composition:back()
if not seg or not seg.menu then return false end
local count = seg.menu:prepare(9)
if idx < 1 or idx > count then return false end
ctx:select(idx - 1)
return true
end
-- [Select Character] 以词定字逻辑 (New!)
local function handle_select_character(key, env, ctx)
-- 1. 检查配置是否存在
if not (env.sc_first_key or env.sc_last_key) then return false end
-- 2. 状态检查:必须在输入中或有候选菜单
if not (ctx:is_composing() or ctx:has_menu()) then return false end
-- 3. 键值匹配
local repr = key:repr()
local is_first = (repr == env.sc_first_key)
local is_last = (repr == env.sc_last_key)
if not (is_first or is_last) then return false end
-- 4. 获取当前选中的候选词或输入
local text = ctx.input
local cand = ctx:get_selected_candidate()
if cand then text = cand.text end
-- 5. 执行上屏
if utf8.len(text) > 1 then
if is_first then
-- 上屏第一个字 (sub: 1 到 第二个字偏移量-1)
env.engine:commit_text(text:sub(1, utf8.offset(text, 2) - 1))
ctx:clear()
return true -- Accepted
elseif is_last then
-- 上屏最后一个字 (sub: 最后一个字偏移量)
env.engine:commit_text(text:sub(utf8.offset(text, -1)))
ctx:clear()
return true -- Accepted
end
end
return false
end
-- [KpNumber & ToneFallback] 数字键综合逻辑
local function handle_number_logic(key, env, ctx)
local kc = key.keycode
local input = ctx.input or ""
-- A. 小键盘处理 (KpNumber)
local kp_num = KP_MAP[kc]
if kp_num ~= nil then
if key:ctrl() or key:alt() or key:super() or key:shift() then return false end
-- ToneFallback: 小键盘必须跳过压缩
env.tone_state = "skip"
local ch = tostring(kp_num)
-- 1. 正则拦截
if is_function_code_after_digit(env, ctx, ch) then
if ctx.push_input then ctx:push_input(ch) else ctx.input = input .. ch end
return true
end
-- 2. 模式处理
if env.kp_mode == "auto" then
if env.kp_is_composing then
if ctx.push_input then ctx:push_input(ch) else ctx.input = input .. ch end
else
return false -- Noop
end
else -- compose mode
if ctx.push_input then ctx:push_input(ch) else ctx.input = input .. ch end
end
return true
end
-- B. 主键盘数字
local r = key:repr() or ""
if r:match("^[0-9]$") then
if key:ctrl() or key:alt() or key:super() then return false end
-- ToneFallback: 标记回退意图
local is_func_mode = false
if wanxiang.is_function_mode_active then
is_func_mode = wanxiang.is_function_mode_active(ctx)
end
-- 如果是反查模式 OR 功能模式,状态设为 idle (不回退)
if input:find(env.lookup_key, 1, true) or is_func_mode then
env.tone_state = "idle"
-- 这里不 return因为数字键可能还有“正则拦截”或“候选选词”的任务
else
env.tone_state = "compress"
local caret = (ctx.caret_pos ~= nil) and ctx.caret_pos or #input
if caret > #input then caret = #input end
local left = (caret > 0) and input:sub(1, caret) or ""
local _, changed = compress_runs_keep_last(left)
if changed then return true end
end
-- 正则拦截
if is_function_code_after_digit(env, ctx, r) then
if ctx.push_input then ctx:push_input(r) else ctx.input = input .. r end
return true
end
-- 候选选词
if env.kp_has_menu then
local d = tonumber(r)
if d == 0 then d = 10 end
if d and d >= 1 and d <= env.kp_page_size then
local comp = ctx.composition
if comp and not comp:empty() then
local seg = comp:back()
local menu = seg and seg.menu
if menu and not menu:empty() then
local sel_index = seg.selected_index or 0
local page_start = math.floor(sel_index / env.kp_page_size) * env.kp_page_size
local index = page_start + (d - 1)
if index < menu:candidate_count() then
if ctx:select(index) then return true end
end
end
end
end
return false
end
else
env.tone_state = "idle"
end
return false
end
-- 5. 主入口函数 (Main Logic Flow)
function M.func(key, env)
local ctx = env.engine.context
-- 1. 优先处理按键释放
if key:release() then
handle_backspace(key, env, ctx)
return K_NOOP
end
local kc = key.keycode
-- 2. QuickSymbol 拦截 (a-z + /)
if handle_quick_symbol_intercept(key, env, ctx) then
return K_ACCEPT
end
-- 3. Backspace 退格防止删除已上屏内容
if kc == 0xFF08 then
if handle_backspace(key, env, ctx) then return K_ACCEPT end
end
-- 4. Select Character 以词定字 (New!)
-- 它的优先级很高,因为是针对当前候选的操作
-- 但必须在 Backspace 之后,防止误操作
if handle_select_character(key, env, ctx) then
return K_ACCEPT
end
-- 5. 分词符 ' [SuperSegmentation] 处理分词符 '
if kc == 0x27 then
if handle_segmentation(key, env, ctx) then return K_ACCEPT end
end
-- 6. 字母键 (a-z)[Limit Repeated] 重复输入限制
if kc >= 0x61 and kc <= 0x7A then
if handle_limit_repeat(key, env, ctx) then return K_ACCEPT end
end
-- 7. (q-o + 特定 Tag)[Letter Selector] 字母选词
if env.ls_active and (LETTER_SEL_MAP[kc] ~= nil) then
if handle_letter_select(key, env, ctx) then return K_ACCEPT end
end
-- 8. 数字键 (小键盘 + 声调 + 选词)[KpNumber & ToneFallback] 数字键综合逻辑
if (kc >= 0xFFB0 and kc <= 0xFFB9) or (kc >= 0x30 and kc <= 0x39) then
if handle_number_logic(key, env, ctx) then return K_ACCEPT end
else
-- 非数字键,重置声调状态
env.tone_state = "idle"
end
return K_NOOP
end
return M

View File

@@ -1,250 +0,0 @@
-- 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 }

View File

@@ -1,140 +0,0 @@
-- 欢迎使用万象拼音方案
-- @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 config = env.engine.schema.config
env.lookup_key = config:get_string('wanxiang_lookup/key') or '`'
local ctx = env.engine and env.engine.context
if not ctx or not ctx.update_notifier then return end
env.tone_fallback_update_connection = ctx.update_notifier:connect(function(c)
if should_ignore(c) then
env.tone_state = "idle"
return
end
-- 只在当前按键对应的一次更新里消费 state
local state = env.tone_state or "idle"
env.tone_state = "idle" -- 立刻消费,避免“下一次才生效”
-- 小键盘数字:标记为 skip本次直接不压缩
if state == "skip" then
return
end
-- 非压缩状态:直接不动
if state ~= "compress" then
return
end
-- 压缩逻辑
local input = c.input
local caret = (c.caret_pos ~= nil) and c.caret_pos or #input
if caret < 0 then caret = 0 end
if caret > #input then caret = #input end
-- 仅处理光标左侧;右侧保持不变
local left = (caret > 0) and input:sub(1, caret) or ""
local right = (caret < #input) and input:sub(caret + 1) or ""
local left_new, changed = compress_runs_keep_last(left)
if not changed then return end
-- 只改左侧,避免干扰右侧;并精确设置 caret_pos
if caret > 0 then c:pop_input(caret) end
if #left_new > 0 then c:push_input(left_new) end
if c.caret_pos ~= nil then c.caret_pos = #left_new end
-- 右侧 right 不需处理Rime 会保持不变
end)
end
function P.fini(env)
if env.tone_fallback_update_connection then
env.tone_fallback_update_connection:disconnect()
env.tone_fallback_update_connection = nil
end
env.tone_state = "idle"
end
---@return ProcessResult
function P.func(key, env)
local ctx = env.engine.context
if should_ignore(ctx) then
env.tone_state = "idle"
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
-- 小键盘数字:标记这次按键为 skip本轮 update_notifier 不压缩
local kc = key.keycode
if is_kp_digit_keycode(kc) then
env.tone_state = "skip"
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
-- 主键盘数字 09标记为 compress
local r = key:repr() or ""
if r:match("^[0-9]$") then
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 ""
if left:find(env.lookup_key, 1, true) then
env.tone_state = "idle"
return wanxiang.RIME_PROCESS_RESULTS.kNoop
end
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

View File

@@ -49,16 +49,10 @@ switches:
# 输入引擎
engine:
processors:
- lua_processor@*select_character #以词定字,默认左中括号上屏一个词的前一个字,右中括号上屏一个词的后一个
- lua_processor@*super_processor #KP小键盘、字母选词、符号快打、超强分词、重复限制、退格限制、声调回退、以词定
- lua_processor@*partial_commit #通过ctrl+1~0局部提交10个字以内的句子的前几个字一般为正确的前几个使用时要遵循合理的分词结构能促进后续编码打出正确的词汇
- lua_processor@*letter_selector #在N模式R模式下输入数字被视为编码那么如何上屏呢现在除了方向键还提供qwertyuio对照1-9来选词
- lua_processor@*quick_symbol_text #快符引导以及重复上屏配合quick_symbol_text顶层配置清单定义扩展按键
- lua_processor@*super_tips #超级提示模块:表情、简码、翻译、化学式、等等靠你想象
- lua_processor@*tone_fallback #声调辅助回退,当你输入声调数字错误时,继续输入正确的而不用回退删除
- lua_processor@*super_sequence*P #手动排序,高亮候选 ctrl+j左移动 ctrl+k 右移动 ctrl+l 移除位移 ctrl+p 置顶
- lua_processor@*limit_repeated #用于限制最大候选长度以及最大重复输入声母编码长度,避免性能异常
- lua_processor@*backspace_limit #防止连续 Backspace 在编码为空时删除已上屏内容
- lua_processor@*kp_number_processor #管理小键盘的处理逻辑,有输入中数字不上屏和数字一直不上屏设置可选
- ascii_composer #处理英文模式及中英文切换
- recognizer #与 matcher 搭配,处理符合特定规则的输入码,如网址、反查等 tags
- key_binder #在特定条件下将按键绑定到其他按键,如重定义逗号、句号为候选翻页、开关快捷键等
@@ -100,7 +94,7 @@ engine:
- lua_filter@*super_filter #comment前,相关功能见Lua文件
- lua_filter@*super_english #comment前,负责英文方案及中英混输中英文单词格式化,语句流,自动加空格等策略
- lua_filter@*super_comment_preedit #OpenCC前超级注释模块、超级preedit支持错词提示、辅助码显示部件组字读音注释有声调、无声调全拼编码的转换支持个性化配置和关闭相应的功能详情搜索super_comment_preedit进行详细配置
- lua_filter@*super_replacer #用来替代OpenCC的处理器
- lua_filter@*super_replacer #OpenCC替代器,更灵活的处理方式,更自由的自定义方式
- lua_filter@*super_sequence*F #手动排序,对高亮候选 ctrl+j左移动 ctrl+k 右移动 ctrl+0 移除位移
- uniquifier # 去重

View File

@@ -48,10 +48,7 @@ engine:
processors:
- t9_processor #元书T9处理器
- lua_processor@*super_tips #超级提示模块:表情、简码、翻译、化学式、等等靠你想象
- lua_processor@*partial_commit #通过ctrl+1~0局部提交10个字以内的句子的前几个字一般为正确的前几个使用时要遵循合理的分词结构能促进后续编码打出正确的词汇
- lua_processor@*super_sequence*P #手动排序,高亮候选 ctrl+j左移动 ctrl+k 右移动 ctrl+l 移除位移 ctrl+p 置顶
- lua_processor@*limit_repeated #用于限制最大候选长度以及最大重复输入声母编码长度,避免性能异常
- lua_processor@*backspace_limit #防止连续 Backspace 在编码为空时删除已上屏内容
- ascii_composer #处理英文模式及中英文切换
- recognizer #与 matcher 搭配,处理符合特定规则的输入码,如网址、反查等 tags
- key_binder #在特定条件下将按键绑定到其他按键,如重定义逗号、句号为候选翻页、开关快捷键等