UNPKG

@satodai42/firis

Version:

Input support module for Japanese typing software

631 lines (525 loc) 23.1 kB
const { instructionSet } = require("./instructionSet.js"); const RESULT = Object.freeze({ I_OK: 0, I_NG: 1, I_END: 2, I_ERR: -1 }) /** * StringCell クラスは、かな文字と対応するローマ字を管理します。 * 各セルは入力する文字の特性や入力済みのローマ字を保持します。 * * Typeの文字特性は /src/instructionSet.js のコメントを参照 */ class StringCell { constructor(kana,romaji,type) { this.Fixation = false //現在のローマ字以外の入力を許さないフラグ this.Kana = kana //かな文字 this.Romaji = romaji //ローマ字 this.Type = type //タイプ情報 this.EnteredRomaji = "" //入力済みのローマ字 } setFixationRomaji(romaji) { this.Fixation = true this.Romaji = romaji } setKana(kana){ this.Kana = structuredClone(kana) } setRomaji(romajiList){ let uniqueArray = romajiList.filter((item, index) => romajiList.indexOf(item) === index);//重複をまず除去 this.Romaji = structuredClone(uniqueArray) } setType(type){ this.Type = structuredClone(type) } setEnteredRomaji(enteredRomaji){ this.EnteredRomaji = enteredRomaji } deleteRomaji(romajiList){ let filteredList = this.Romaji.filter(item => { return romajiList.some(char => !item.indexOf(char)); }); this.Romaji = filteredList } bringToFrontRomaji(romaji){ let filtered = this.Romaji.filter(word => !word.indexOf(romaji)) let rest = this.Romaji.filter(word => word.indexOf(romaji)) this.Romaji = filtered.concat(rest) } } /** * StringContainer クラスは、StringCellのリストを管理します。 * またStringCellを操作するためのメソッドを提供します。 */ class StringContainer { constructor(){ this.StringCellList = [] this.CurrentlyCellNum = 0 //今どのセルを入力中か this.CurrentlyRomajiNum = 0 //セルの中の何番目のローマ字を入力中か this.EnteredCellRomaji = "" //セルの中で入力済みの文字 this.EnteredRomaji = "" //すべての入力済みの文字 } addStringCell(stringCell){ this.StringCellList.push(stringCell) } isStringCellListEnd(){ //最後のセルを過ぎたかどうかをチェックする if(this.CurrentlyCellNum +1 > this.StringCellList.length){ return true } return false } getStringCells_Romaji(){ return this.StringCellList.map( (e) => { return e.Romaji[0] }) } getInputRomajiString(){ let output = "" let i = 0 //すでに入力済みのセルを取得 for(i=0 ; i < this.CurrentlyCellNum ; i++){ output += this.StringCellList[i].EnteredRomaji } //未入力のセルを取得 for(; i < this.StringCellList.length ; i++){ output += this.StringCellList[i].Romaji[0] } return output } getStringCells_EnteredRomaji(){ return this.StringCellList.map( (e) => { return e.EnteredRomaji }) } getStringCells_EnteredKana(){ let output = [] //すでに入力済みのセルを取得 for(let i=0 ; i < this.CurrentlyCellNum ; i++){ output.push(this.StringCellList[i].Kana) } return output } getStringCells_Kana(){ return this.StringCellList.map( (e) => { return e.Kana }) } setCurrentlyStringCell_Romaji(romajiList){ this.StringCellList[this.CurrentlyCellNum].Romaji = structuredClone(romajiList) } getCurrentlyStringCell(){ if(this.isStringCellListEnd()){ return null } return this.StringCellList[this.CurrentlyCellNum] } getCurrentlyStringCell_Romaji(){ let cell = this.getCurrentlyStringCell() if(this.CurrentlyRomajiNum > cell.Romaji.length){ return null } let tmp = cell.Romaji.substring(this.CurrentlyRomajiNum,this.CurrentlyRomajiNum + 1) return tmp } getNextStringCell(){ if(this.CurrentlyCellNum +1 > this.StringCellList.length){ return null } return this.StringCellList[this.CurrentlyCellNum + 1] } overwriteCurrentlyStringCell(kana,romaji,type,entered){ this.StringCellList[this.CurrentlyCellNum] = new StringCell(kana,romaji,type) this.StringCellList[this.CurrentlyCellNum].EnteredRomaji = entered } overwriteNextStringCell(num,kana,romaji,type,entered){ this.StringCellList[this.CurrentlyCellNum+num] = new StringCell(kana,romaji,type) this.StringCellList[this.CurrentlyCellNum+num].EnteredRomaji = entered } insertStringCell(kana,romaji,type){ this.StringCellList.splice(this.CurrentlyCellNum+1,0,new StringCell(kana,romaji,type)) } insertNextStringCell(num,kana,romaji,type){ this.StringCellList.splice(this.CurrentlyCellNum+num,0,new StringCell(kana,romaji,type)) } proceedNextStringCell(){ this.CurrentlyRomajiNum = 0 this.CurrentlyCellNum += 1 this.EnteredCellRomaji = "" } } /** * Firis クラスはタイピングのメイン処理を行うクラスです。 */ class Firis { constructor(){ this.InstructionMaster = [] this.StringContainerList = [] this.#readInstructions() //インストラクションを読み込む } /** * かなとローマ字の変換パターンを読み込み、リストを作成 */ #readInstructions(){ //リストをハッシュ化する {kana: ,type: ,romaji:[]} this.InstructionMaster = instructionSet.map((inst) => { //romajiリストを作成 let tmpRomaji = [] for(let i=2;i<inst.length;++i){ tmpRomaji.push(inst[i]) } //typeをビットから連想配列に変換 let tmpType = parseInt(inst[1],10) let setType = {t1:false,t2:false,t3:false,t4:false,t5:false,t6:false} //各ビットを抽出 typeの意味は /src/instructionSet.js のコメントを参照 if ((tmpType & 0b000001) != 0){ //1bit setType.t1 = true } if ((tmpType & 0b000010) != 0){ //2bit setType.t2 = true } if ((tmpType & 0b000100) != 0){ //3bit setType.t3 = true } if ((tmpType & 0b001000) != 0){ //4bit setType.t4 = true } if ((tmpType & 0b010000) != 0){ //5bit setType.t5 = true } if ((tmpType & 0b100000) != 0){ //6bit setType.t6 = true } return {Kana: inst[0],Type: setType,Romaji: tmpRomaji} }) } /** * 文字に対応するハッシュを返却する * @param {*} searchStr * @returns */ #searchInstruction(searchStr){ let result = this.InstructionMaster.find((instruction) => instruction.Kana === searchStr) return result } /** * str2のリストの中にstr1のリストの1文字目が含まれているかチェック * @param {*} str1 * @param {*} str2 * @returns */ #searchStrInStr(str1 , str2){ let ret = false str2.forEach( e1 => { str1.forEach( e2 => { if (e1.includes(e2.substring(0,1))){ ret = true } }) }) return ret } /** * 文字コンテナを作成 * すでに作成済みの場合は初期化して再作成 * @param {*} inputStr * @returns true:成功 false:失敗 */ createStringContainer(inputStr){ //初期化 配列を空にする this.StringContainerList = [] let inputStrList = inputStr.split("") let stringContainer = new StringContainer() for (let index=0 ; index < inputStrList.length ; ++index){ let str = inputStrList[index] let instruction = this.#searchInstruction(str) if(instruction == undefined){ //見つからなかった場合 console.error("指定された文字列に対応するローマ字が見つかりませんでした。") return false } let stringCell = new StringCell(instruction.Kana,instruction.Romaji,instruction.Type) //特殊パターンをチェック if(index !== inputStrList.length){ //末尾でない場合 if(instruction.Type.t1){ //後ろに小さい文字がつく可能性をチェック let mergeStr = instruction.Kana + inputStrList[index+1] //次の字と今の文字を結合 //再探索 const mergeResult = this.#searchInstruction(mergeStr) if(mergeResult != undefined){ //結合文字が見つかった instruction = mergeResult //結果を上書き stringCell.setKana(instruction.Kana) stringCell.setRomaji(instruction.Romaji) index++ //2文字処理したのですすめる } } else if (instruction.Type.t3){ //小さい「っ」チェック const instructionSmall_tsu = this.#searchInstruction("っ") const baseSmall_tsu = instructionSmall_tsu.Romaji if(index+1 !== inputStrList.length){ //末尾でない場合 //次の文字を読み込み let nextStr = inputStrList[index+1] const nextInstruction = this.#searchInstruction(nextStr) if(nextInstruction.Type.t3){ //次の文字がまた「っ」だった場合 // xtuで確定 なんもしない } else if(nextInstruction.Type.t1){ //次の文字が後ろに小さい文字がつく文字の場合 if (index+2 !== inputStrList.length){ //2文字先が末尾でない場合 //次の次の文字を読み込み let nextnextStr = inputStrList[index+2] if(nextInstruction.Type.t1){ //次の文字について後ろに小さい文字がつく可能性をチェック let mergeStr = nextInstruction.Kana + nextnextStr //次の次の字と次の文字を結合 const mergeResult2 = this.#searchInstruction(mergeStr) if(mergeResult2 != undefined){ //結合文字が見つかった let add = mergeResult2.Romaji.map(w => w.substring(0,1)) stringCell.setRomaji(add.concat(baseSmall_tsu)) }else { //見つからなかったので1文字目の頭文字で作成 if(!nextInstruction.Type.t4 && !nextInstruction.Type.t5){ //次の文字が連打禁止または記号でない場合 let add = nextInstruction.Romaji.map(w => w.substring(0,1)) stringCell.setRomaji(add.concat(baseSmall_tsu)) } } } }else{ if(!nextInstruction.Type.t4 && !nextInstruction.Type.t5){ //次の文字が連打禁止または記号でない場合 let add = nextInstruction.Romaji.map(w => w.substring(0,1)) stringCell.setRomaji(add.concat(baseSmall_tsu)) } } } else if(!nextInstruction.Type.t4 && !nextInstruction.Type.t5){ //次の文字が連打禁止または記号でない場合 let add = nextInstruction.Romaji.map(w => w.substring(0,1)) stringCell.setRomaji(add.concat(baseSmall_tsu)) } } } } stringContainer.addStringCell(stringCell) } this.StringContainerList.push(stringContainer) return true } /** * タイピングのメイン処理関数 * @param {*} input * @param {*} stringContainer * @returns */ #processTyping(input,stringContainer){ if (stringContainer.isStringCellListEnd()){ //すでに末尾 return RESULT.I_END } let result = RESULT.I_NG while(true){ //入力済み文字を組み立てて前方一致で検索 let check = stringContainer.EnteredCellRomaji + input let stringCell = stringContainer.getCurrentlyStringCell() let first_match = stringCell.Romaji.filter( e => !e.indexOf(check)) if(first_match.length > 0){ //結果がいずれかの入力に一致した result = RESULT.I_OK //マッチしなかったローマ字はStringCellから削除する stringCell.deleteRomaji(first_match) stringContainer.EnteredCellRomaji += input stringContainer.EnteredRomaji += input stringCell.setEnteredRomaji(stringContainer.EnteredCellRomaji) if(stringCell.Type.t3){ //小さい「っ」のセルだった場合は次を固定 let nextStringCell = stringContainer.getNextStringCell() //次の文字の頭と入力した文字が一致しているかチェック let fixationRomaji = nextStringCell.Romaji.filter( e => !e.indexOf(check)) //「x」or「l」入力 かつ 次が小さい文字でない場合に次の文字を固定化 if (fixationRomaji.length > 0){ //気を利かせる if(nextStringCell.Type.t2){ nextStringCell.bringToFrontRomaji(fixationRomaji) } if(!nextStringCell.Type.t2 && !nextStringCell.Type.t3 && !nextStringCell.Type.t4 && !nextStringCell.Type.t5 ){ nextStringCell.setFixationRomaji(fixationRomaji) } } } if(first_match.length == 1 && first_match[0].length == stringContainer.EnteredCellRomaji.length){ stringContainer.proceedNextStringCell() //次のセルへ } }else{ //通常の入力パターンでは見つからなかった //「しゃ」とかの2文字構成だった場合 if(stringCell.Type.t1){ //最初の1文字(「しゃ」の場合は「し」)を切り取って1文字で一致するかチェック let word = stringCell.Kana.substring(0,1) const firstCharInstruction = this.#searchInstruction(word) let second_match = firstCharInstruction.Romaji.filter( e => !e.indexOf(check)) if(second_match.length > 0 ){ //他のパターンが見つかった if(stringCell.Fixation && !this.#searchStrInStr(stringCell.Romaji,second_match)){ //入力文字制限のため入力NG「っちゃ」などで起きる } else { result = RESULT.I_OK //今のinstructionを上書き stringContainer.overwriteCurrentlyStringCell(firstCharInstruction.Kana,firstCharInstruction.Romaji,firstCharInstruction.Type,check) //マッチしなかったローマ字はStringCellから削除する stringContainer.getCurrentlyStringCell().deleteRomaji([check]) //次の位置にinstructionを追加 let word2 = stringCell.Kana.substring(1,2) //2文字目を切り取り const secondCharInstruction = this.#searchInstruction(word2) stringContainer.insertStringCell(secondCharInstruction.Kana,secondCharInstruction.Romaji,secondCharInstruction.Type,) //入力済み文字そセット stringContainer.EnteredCellRomaji += input stringContainer.EnteredRomaji += input stringCell.setEnteredRomaji(stringContainer.EnteredCellRomaji) if(second_match.length == 1 && second_match[0].length == stringContainer.EnteredCellRomaji.length){ stringContainer.proceedNextStringCell() //次のセルへ } } } } else if (stringCell.Type.t3) { //小さな「っ」処理 //次のセルを取得 let nextStringCell = stringContainer.getNextStringCell() if (nextStringCell != null && !nextStringCell.Type.t4 && !nextStringCell.Type.t5 && !nextStringCell.Type.t6){ //次のセルがあるとき //次のセルの頭と一致するか検索 let xtu_match = nextStringCell.Romaji.filter( e => !e.indexOf(check)) if(xtu_match.length > 0){ //次の文字の別パターンで「っ」を打った result = RESULT.I_OK //ローマ字を上書き stringCell.setRomaji([check]) //入力済み文字そセット stringContainer.EnteredCellRomaji += input stringContainer.EnteredRomaji += input stringCell.setEnteredRomaji(stringContainer.EnteredCellRomaji) //次のセルを固定化 nextStringCell.setFixationRomaji(xtu_match) stringContainer.proceedNextStringCell() //次のセルへ } else if (nextStringCell.Type.t2){ //次のセルが小さな文字の場合は「xx」と入力された場合に次セルへ飛ぶ if(check === "xx" || check === "ll"){ //現在のセルにxかlをつめる stringContainer.setCurrentlyStringCell_Romaji([check.substring(0,1)]) stringContainer.proceedNextStringCell() //次のセルへ input = check.charAt(check.length - 1) //最後の1文字を取得 continue //もう一度処理を走らせる } } else if (nextStringCell.Type.t1){ // っふぁ のパターンで、hhuxa で打てるようにするための処理 // 次の文字の1文字目を切り取って一致しているかをチェック let word = nextStringCell.Kana.substring(0,1) const firstCharInstruction = this.#searchInstruction(word) let match = firstCharInstruction.Romaji.filter( e => !e.indexOf(check)) if(match.length > 0 ){ // ローマ字を上書き stringCell.setRomaji([check]) //つぎのセルを「ふ」と「ぁ」に分けて、「ふ」は固定化する stringContainer.overwriteNextStringCell(1,firstCharInstruction.Kana,match,firstCharInstruction.Type,"") word = nextStringCell.Kana.substring(1,2) //「ぁ」を取得 const secondCharInstruction = this.#searchInstruction(word) //「ぁ」を次の次のセルに挿入 stringContainer.insertNextStringCell(2,secondCharInstruction.Kana,secondCharInstruction.Romaji,secondCharInstruction.Type,"") continue } } } } else if (stringCell.Type.t6) { //「ん」をn一つで打ち終えたい場合 if(!check.indexOf("n")){ //nで始まる入力をしている //次のセルを取得 let nextStringCell = stringContainer.getNextStringCell() if (nextStringCell != null){ //次のセルがあるとき if (!nextStringCell.Type.t4 && !nextStringCell.Type.t5 && !nextStringCell.Type.t6 ){ let tail = check.charAt(check.length - 1) //最後の1文字を取得 //次の文字の頭と一致しているかチェック let headCheck = nextStringCell.Romaji.filter( e => !e.indexOf(tail)) if(headCheck.length > 0){ //次の文字の頭と一致している //現在のセルにnをつめる stringContainer.setCurrentlyStringCell_Romaji(["n"]) input = tail stringContainer.proceedNextStringCell() //次のセルへ continue //もう一度処理を走らせる } } } } } } break } //末尾まで入力した if (stringContainer.isStringCellListEnd()){ result = RESULT.I_END } return result } /** * 文字コンテナが存在するかどうかを返却 * @returns */ isAvailable(){ if (this.StringContainerList.length > 0) return true return false } /** * 文字コンテナからローマ字の文字リストを取得 * @returns 文字リスト 例:["a", "i", "u", "e", "o"] */ getRomaji(){ if (!this.isAvailable()) return "" return this.StringContainerList[0].getStringCells_Romaji() } /** * 文字コンテナからかな文字取得 * @returns 文字リスト 例:["あ", "い", "う", "え", "お"] */ getKana(){ if (!this.isAvailable()) return "" return this.StringContainerList[0].getStringCells_Kana() } /** * 文字コンテナから入力済みのローマ字を取得 * @returns 文字リスト 例:["a", "i", "u"] */ getEnteredRomaji() { if (!this.isAvailable()) return "" let array = this.StringContainerList[0].getStringCells_EnteredRomaji() // そのままだと空要素が入り、「x,,」のようになるので空要素を削除 let filteredArray = array.filter(item => item !== '' && item !== null && item !== undefined) return filteredArray } /** * 文字コンテナから入力済みのかな文字を取得 * @returns 文字リスト 例:["あ", "い", "う"] */ getEnteredKana() { if (!this.isAvailable()) return "" let array = this.StringContainerList[0].getStringCells_EnteredKana() // そのままだと空要素が入り、「x,,」のようになるので空要素を削除 let filteredArray = array.filter(item => item !== '' && item !== null && item !== undefined) return filteredArray } /** * 文字コンテナをクリア * @returns */ clearStringContainer() { this.StringContainerList = [] return true } /** * 入力した英数字を1文字渡してタイピング処理を実行 * @param {string} input * @returns 0:入力成功 1:入力失敗(タイプミス)2:文章の最後まで入力成功 -1:エラー */ inputKey(input) { //2文字以上だったら不正なのでエラー if (input.length != 1 ){ console.error("inputKeyに渡す値は1文字である必要があります") return RESULT.I_ERR } //StringContainerが空の場合はエラー if (!this.isAvailable()){ console.error("inputKeyを呼び出す前にcreateStringcontainerで入力文字列を設定してください") return RESULT.I_ERR } return this.#processTyping (input,this.StringContainerList[0]) } } module.exports = Firis