feat: 开源蟹蟹输入RTOS适配改版并采用MIT商用许可
这个提交包含在:
当前提交
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}
|
||||
}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户