feat: 开源蟹蟹输入RTOS适配改版并采用MIT商用许可

这个提交包含在:
sydxky 于 2026-11-14 12:10:10 +08:00 提交
当前提交 29374caffd
116 个文件已更改
+351523

没有与搜索条件相匹配的文件

@@ -0,0 +1,109 @@
/* RTOS adaptation of CrabKeyboard, MIT. Original authors kqakqakqa and crabKun.
* Full V5 dictionary, prefix consumption and local preference ordering retained.
* New: T9 decoding, paged complete candidates and Unicode-safe editing.
*/
const GROUPS = ['abc','def','ghi','jkl','mno','pqrs','tuv','wxyz']
// Adjacent QWERTY pairs used by the public 14-key layout. One token per key.
export const KEY14_GROUPS = ['qw','er','ty','ui','op','as','df','gh','jk','l','zx','cv','bn','m']
const KEY14_CODES='0123456789abcd'
export function code14Of(text){
let result=''
for(const ch of String(text||'').toLowerCase()){
const index=KEY14_GROUPS.findIndex(group=>group.indexOf(ch)>=0)
if(index<0)return ''
result+=KEY14_CODES[index]
}
return result
}
export function display14(raw){return (raw.length>6?'… ':'')+Array.from(raw.slice(-6)).map(code=>KEY14_GROUPS[KEY14_CODES.indexOf(code)]||'').join(' ').toUpperCase()}
export function digitsOf(text) {
let result=''
for (const ch of text.toLowerCase()) {
const index=GROUPS.findIndex(group=>group.indexOf(ch)>=0)
if(index<0)return ''
result+=String(index+2)
}
return result
}
export function chars(text){return Array.from(String(text||''))}
export function edit(text,cursor,insertion,maxLength) {
const value=chars(text), at=Math.max(0,Math.min(value.length,cursor))
const room=Math.max(0,maxLength-value.length), added=chars(insertion).slice(0,room)
return {text:value.slice(0,at).concat(added,value.slice(at)).join(''),cursor:at+added.length,limited:added.length<chars(insertion).length}
}
export function erase(text,cursor) {
const value=chars(text),at=Math.max(0,Math.min(value.length,cursor))
if(at)value.splice(at-1,1)
return {text:value.join(''),cursor:Math.max(0,at-1)}
}
export function preview(text,cursor,secret) {
const value=chars(text),at=Math.max(0,Math.min(value.length,cursor))
const left=value.slice(Math.max(0,at-15),at),right=value.slice(at,at+7)
return (at>15?'…':'')+(secret?'•'.repeat(left.length):left.join(''))+'│'+(secret?'•'.repeat(right.length):right.join(''))+(at+7<value.length?'…':'')
}
export function sanitizePreferences(input) {
const result={}
if(!input||typeof input!=='object'||Array.isArray(input))return result
Object.keys(input).filter(key=>/^(?:[a-z2-9]{1,32}|k14:[0-9a-d]{1,32})$/.test(key)).slice(-100).forEach(key=>{
if(!Array.isArray(input[key]))return
result[key]=input[key].filter(x=>x&&typeof x.word==='string'&&chars(x.word).length<=32&&Number.isInteger(x.consume)&&x.consume>0&&x.consume<=32).slice(0,3).map(x=>({word:x.word,consume:x.consume}))
})
return result
}
export function learn(preferences,input,candidate) {
const result=sanitizePreferences(preferences)
const previous=Array.isArray(result[input])?result[input]:[]
delete result[input]
result[input]=[{word:candidate.word,consume:candidate.consume}].concat(previous.filter(x=>x.word!==candidate.word||x.consume!==candidate.consume)).slice(0,3)
const keys=Object.keys(result)
if(keys.length>100)delete result[keys[0]]
return result
}
export function createDictionary(read) {
// Small shards and a bounded LRU avoid parsing the whole lexicon on an MCU.
const cache=[],pending={};let manifest=null
async function load(name) {
const existing=cache.find(x=>x.name===name)
if(existing)return existing.data
if(pending[name])return pending[name]
pending[name]=(async()=>{
const text=await read('/assets/ime-dict/'+name+'.json')
const data=typeof text==='string'?JSON.parse(text):text
const size=typeof text==='string'?text.length:0
cache.push({name,data,size});while(cache.length>4||cache.reduce((sum,x)=>sum+x.size,0)>90000)cache.shift()
return data
})()
try{return await pending[name]}finally{delete pending[name]}
}
return {
async lookup(raw,t9,preferences) {
const input=String(raw||'').toLowerCase()
const dual=t9==='14',ambiguous=!!t9
if(!(dual?/^[0-9a-d]{1,32}$/:t9?/^[2-9]{1,32}$/:/^[a-z]{1,32}$/).test(input))return []
const digits=ambiguous?input:digitsOf(input)
const merged={},frequencies={}
function merge(data){Object.assign(merged,data.entries);Object.assign(frequencies,data.frequencies)}
if(!manifest){const meta=await read('/assets/ime-dict/meta.json');manifest=typeof meta==='string'?JSON.parse(meta):meta}
const names=[]
const prefixLength=dual?3:4
for(let length=1;length<=Math.min(prefixLength-1,digits.length);length++)names.push((dual?'ds':'s')+digits.slice(0,length))
if(digits.length>=prefixLength)names.push((dual?'d':'')+digits.slice(0,prefixLength))
for(const name of names)if(manifest.shards.indexOf(name)>=0)try{merge(await load(name))}catch(error){throw new Error('词库读取失败')}
const matching=Object.keys(merged).filter(key=>input.indexOf(dual?code14Of(key):t9?digitsOf(key):key)===0)
matching.sort((a,b)=>b.length-a.length||(a<b?-1:a>b?1:0))
const result=[],seen={}
for(const key of matching)for(const word of merged[key]){
const identity=word+'@'+key.length
if(!seen[identity]){seen[identity]=true;result.push({word,pinyin:key,consume:key.length})}
}
const preferenceKey=dual?'k14:'+input:input
const prefs=preferences&&Array.isArray(preferences[preferenceKey])?preferences[preferenceKey]:[]
const rank=item=>{const i=prefs.findIndex(x=>x.word===item.word&&x.consume===item.consume);return i<0?100:i}
// Rank only real dictionary results: a damaged preference cannot inject text.
return result.map((item,index)=>({item,index})).sort((a,b)=>rank(a.item)-rank(b.item)||(ambiguous?(b.item.consume-a.item.consume||(frequencies[b.item.word]||0)-(frequencies[a.item.word]||0)):0)||a.index-b.index).map(x=>x.item)
},
clear(){cache.length=0;manifest=null},
get cacheSize(){return cache.length}
}
}