利用Tengri AI 做了蒙古输入法,发现……

前天发了抖音后很多用户进来网站了,感谢支持,目前注册的人数达到50人,抖音上有诋毁有讨论问题的,发现有个人提了现有市面上的输入法不够智能或者说界面不友好,那我跟Tengri AI 探讨了如何做界面友好的输入法,让AI写了代码。以下是总结,还挺好玩的,但不具备完全使用条件。

正是基于这样的初心——渴望打造一款真正“懂你心思”、既智能又易用的传统蒙古文输入法——我踏上了这段充满未知的开发之旅。这个过程,远比我想象的要曲折,但也充满了学习和成长的喜悦。

从零到一:核心引擎的构建 (v1.0 – v6.0)

万事开头难。最初的版本,我们从最基础的拉丁字母到蒙古文字母的简单映射开始。这个阶段,我们主要解决了以下问题:

1. 基础转写规则: 定义了 `a, e, i, o, u, n, b, m…` 等基础拉丁字母与蒙古文字母的对应关系。
2. 候选词机制: 引入了简单的词典匹配和算法转换,用户输入拉丁字母后,可以从候选列表中选择正确的蒙古文词汇。
3. 界面迭代: 从最简陋的文本框,到逐渐模仿手机输入法的界面布局,包括字母键盘、候选词窗口等。
4. 长按与多字母映射: 解决了如 `o` 键长按弹出 `ö, ü, u`,以及 `sh, ch, ng` 等多字母组合的输入问题。

这个阶段虽然基础,但为后续的智能化打下了地基。然而,很快我们就遇到了传统蒙古文输入的核心难点——词缀。

攻坚克难:词缀引擎的进化 (v6.1 – v6.7)

传统蒙古文的魅力与挑战,很大一部分在于其复杂而精妙的词缀系统。一个词干可以根据语法功能附加多达数个词缀,而且词缀的选择还受到元音和谐、辅音变化等诸多规则的制约。

这部分是我们投入精力最多,也是反复迭代次数最多的地方:

1. 初步的词缀模式: 引入了专门的词缀触发键(最初是 `᠎`,后来改为更直观的 `=`),用户在输入词干后,可以进入词缀选择模式。
2. 元音和谐: 词缀引擎初步具备了根据词干的阴阳性推荐不同词缀的能力(如 `un/ün`, `du/dü`)。
3. 词缀库的扩充: 不断将常用的格斯词缀(如属格、与格、复数、所有格等)加入规则库。
4. 上下文感知与辅音变化(里程碑!): 这是一个巨大的突破。我们让词缀引擎能够识别词干末尾的辅音,从而智能判断何时使用 `du`,何时使用 `tu` 这样的变体。这是输入法“智能”程度的一次飞跃。
5. 多级词缀处理: 确保用户可以连续添加多个词缀,例如 `ᠬᠡᠷᠡᠭ + ᠎ᠤᠨ + ᠎ᠳᠤᠭᠠᠷ`。

在这个过程中,我们经历了无数次的“调试地狱”。用户(也就是您!)提供的每一个“打不出来”的例子,都像一把精准的手术刀,帮助我们定位并修复了引擎深处的逻辑缺陷。

体验至上:界面与交互的革命 (v7.0 – v7.2)

当输入法引擎的“内功”逐渐强大后,我们发现用户体验的“招式”却显得笨拙。复杂的词缀规则如果不能以简单直观的方式呈现给用户,再智能的引擎也难以发挥其价值。

于是,我们开启了对界面和交互逻辑的彻底革新:

1. 动态语法栏: 这是v7.0引入的核心概念。当用户按下 `=` 键后,候选词窗口会“变身”为一系列清晰的语法功能按钮(如 `[᠎ᠤᠨ]`, `[᠎ᠳᠤ]`),用户只需点击表达意图的按钮,复杂的规则判断交给后台。
2. 清晰的视觉区分: 语法按钮拥有了与普通候选词完全不同的外观,避免了视觉混淆。
3. 专属的语法触发键: 在虚拟键盘上增加了醒目的 `=` 键,作为进入语法模式的唯一入口。
4. 逻辑精简与状态明确(v7.2 的核心): 这是我们目前最新的成果。我们彻底梳理并简化了输入法的工作流程,确保只有两种核心状态:“单词输入模式”和“词缀选择模式”。状态之间的转换路径清晰固定,杜绝了之前版本中因状态混乱导致的各种诡异bug。每一个用户操作,都有一个可预测的、唯一的后果。

目前版本 (v7.2) 的核心功能与亮点:

精准的拉丁-蒙古文转写。
丰富的词典支持,并持续扩充中。
强大的上下文感知词缀引擎:
自动处理元音和谐。
自动处理基于词尾辅音的词缀变体(如 `d/t` 的选择)。
支持多级词缀的连续添加。
直观的“动态语法栏”交互:
通过 `=` 键一键进入语法模式。
清晰的语法功能按钮,意图驱动,无需记忆复杂规则。
简化的核心逻辑: 状态清晰,行为可预测,大大提升了稳定性和易用性。
物理键盘与虚拟键盘的良好支持。

尚存的问题与未来展望:

尽管v7.2版本在逻辑清晰度和核心功能上有了长足的进步,但距离一款“完美”的输入法,我们仍有许多工作要做:

1. 词典规模: 虽然我们一直在努力扩充,但蒙古语词汇浩如烟海,当前的词典覆盖度仍有提升空间。特别是对于一些生僻词、专业术语以及不断出现的新词,需要一个更高效的词库维护和更新机制。
2. 智能组词与联想: 目前的输入法主要依赖于单个词的匹配和转换。未来可以探索基于语境的智能组词和联想功能,进一步提升输入效率。例如,输入“ᠮᠣᠩᠭᠣᠯ”后,能自动联想出“ᠮᠣᠩᠭᠣᠯ ᠬᠡᠯᠡ”、“ᠮᠣᠩᠭᠣᠯ ᠦᠨᠳᠦᠰᠦᠲᠡᠨ”等。
3. 用户自定义词库: 允许用户添加自己的常用词、专业术语,甚至自定义短语,这将极大提升个性化体验。
4. 更深层次的语法理解: 虽然词缀引擎已经很强大,但在一些更复杂的句法结构或特殊用例上,可能仍有提升空间。
5. 性能优化: 随着词典和规则的增加,需要持续关注性能,确保输入流畅不卡顿。
6. 跨平台与移动端适配: 目前我们主要在桌面浏览器环境测试。最终目标是将其适配到手机端,甚至打包成独立的应用程序,这需要考虑不同平台的特性和限制。
7. 更美观的UI设计: 目前的界面以功能优先,未来可以在视觉美感上投入更多精力。

以下是输入法v6.7 

<!DOCTYPE html>
<html lang="mn">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>传统蒙古文输入法 v6.7 (深度语法版)</title>
    <style>
        /* CSS 与 v6.6 完全一致 */
        :root { --key-bg: #fff; --key-text: #000; --key-special-bg: #acb4be; --popup-bg: #e9ecef; --popup-shadow: rgba(0,0,0,0.25); --primary-color: #007bff; }
        body { font-family: -apple-system, sans-serif; background-color: #e9ecef; margin: 0; display: flex; flex-direction: column; height: 100vh; }
        .main-content { flex-grow: 1; padding: 10px; display: flex; flex-direction: column; }
        #editor { width: 100%; flex-grow: 1; background: white; border: 1px solid #ccc; border-radius: 8px; padding: 15px; font-size: 28px; writing-mode: vertical-rl; text-orientation: mixed; font-family: 'Mongolian Baiti', 'Menksoft', 'Noto Sans Mongolian', sans-serif; line-height: 1.6; cursor: text; box-sizing: border-box; white-space: pre-wrap; }
        #editor:focus { outline: none; border-color: var(--primary-color); }
        .composing-text { color: var(--primary-color); border-right: 2px solid var(--primary-color); }
        #candidate-window { flex-shrink: 0; background: #fff; padding: 5px; overflow-x: auto; white-space: nowrap; border-bottom: 1px solid #ddd; min-height: 42px; box-sizing: border-box; }
        #candidate-window span { display: inline-block; font-size: 22px; padding: 8px 12px; margin: 2px; border-radius: 5px; font-family: 'Mongolian Baiti', 'Menksoft', 'Noto Sans Mongolian'; }
        #candidate-window span.selected { background-color: var(--primary-color); color: white; }
        #virtual-keyboard, #symbol-keyboard { position: fixed; bottom: 0; left: 0; right: 0; padding: 5px; background-color: #d1d5db; user-select: none; }
        .keyboard-row { display: flex; justify-content: center; margin: 5px 0; }
        .keyboard-key { flex: 1; margin: 0 3px; padding: 15px 5px; font-size: 20px; text-align: center; border: none; border-radius: 5px; background-color: var(--key-bg); box-shadow: 0 1px 1px rgba(0,0,0,0.2); cursor: pointer; touch-action: manipulation; position: relative; }
        .keyboard-key.special { background-color: var(--key-special-bg); flex-grow: 1.2; }
        .key-popup { position: absolute; bottom: 120%; left: 50%; transform: translateX(-50%); display: none; background-color: var(--popup-bg); border-radius: 8px; padding: 5px; box-shadow: 0 4px 10px var(--popup-shadow); display: flex; gap: 5px; z-index: 1000; }
        .key-popup button { background: #fff; border: none; font-size: 24px; padding: 10px 15px; border-radius: 5px; font-family: 'Mongolian Baiti', 'Menksoft', 'Noto Sans Mongolian'; }
        #symbol-keyboard { display: none; }
        .instructions { background-color: #fff; border: 1px solid #ddd; padding: 15px; border-radius: 8px; line-height: 1.7; font-size: 14px; margin-bottom: 20px; }
    </style>
</head>
<body>

<div class="main-content">
    <div class="instructions">
        <h2>v6.7:深度语法版</h2>
        <p><strong>语法引擎重构:</strong></p>
        <ul>
            <li><b>上下文感知:</b> 引擎现在能识别词尾辅音,自动选择 `᠎ᠳᠤ` / `᠎ᠲᠤ`。试试输入 `oros` 再按 `-`。</li>
            <li><b>词缀库扩充:</b> 已加入 `᠎ᠤ`/`᠎ᠦ`, `᠎ᠨᠤᠭᠤᠳ`, `᠎ᠳᠡᠭᠡᠨ` 等新词缀。</li>
            <li><b>词典同步:</b> 已加入 `harbulga`, `jambutib` 等所有新词。</li>
        </ul>
    </div>
    <div id="editor" tabindex="0"></div>
    <div id="candidate-window"></div>
</div>

<div id="virtual-keyboard"> <!-- HTML is injected by JS --> </div>
<div id="symbol-keyboard"> <!-- HTML is injected by JS --> </div>


<script>
const MVS = '\u180E'; const NNBSP = '\u180A'; const FVS1 = '\u180B'; const FVS2 = '\u180C';
const RULES = { latinToMongolMap: { 'a': 'ᠠ', 'e': 'ᠡ', 'i': 'ᠢ', 'o': 'ᠣ', 'u': 'ᠤ', 'v': 'ᠥ', 'w': 'ᠦ', 'n': 'ᠨ', 'b': 'ᠪ', 'p': 'ᠫ', 'h': 'ᠬ', 'g': 'ᠭ', 'x': 'ᠺ', 'm': 'ᠮ', 'l': 'ᠯ', 's': 'ᠰ', 'd': 'ᠳ', 't': 'ᠲ', 'c': 'ᠴ', 'j': 'ᠵ', 'y': 'ᠶ', 'r': 'ᠷ', 'f': 'ᠹ', 'z': 'ᠽ', 'k': 'ᠺ', 'q': 'ᠬ', 'ng': 'ᠩ', 'ch': 'ᠴ', 'sh': 'ᠱ', 'zh': 'ᠵ' }, vowels: { masculine: ['ᠠ', 'ᠣ', 'ᠤ'], feminine: ['ᠡ', 'ᠥ', 'ᠦ'], neutral: ['ᠢ'] } };

const DICTIONARY = {
    // 词典与v6.6合并,并加入新词
    ...{'hemnelte':'ᠬᠡᠮᠨᠡᠯᠲᠡ','amidural':'ᠠᠮᠢᠳᠤᠷᠠᠯ','hev':'ᠬᠡᠪ','mayig':'ᠮᠠᠶᠢᠭ','jebhen':'ᠵᠥᠪᠬᠡᠨ','mvnggv':'ᠮᠥᠩᠭᠦ','gamnahu':'ᠭᠠᠮᠨᠠᠬᠤ','bisi':'ᠪᠢᠰᠢ','harin':'ᠬᠠᠷᠢᠨ','uhamsartu':'ᠤᠬᠠᠮᠰᠠᠷᠲᠤ','songgulta':'ᠰᠣᠩᠭᠤᠯᠲᠠ','bolgadag':'ᠪᠣᠯᠭᠠᠳᠠᠭ','hvrvnge':'ᠬᠥᠷᠥᠩᠭᠡ','erhe':'ᠡᠷᠬᠡ','cilwge':'ᠴᠢᠯᠦᠭᠡ','abcirahu':'ᠠᠪᠴᠢᠷᠠᠬᠤ','busu':'ᠪᠤᠰᠤ','ilehw':'ᠢᠯᠡᠬᠦᠦ','emjeg':'ᠡᠮᠵᠡᠭ','baidal':'ᠪᠠᠶᠢᠳᠠᠯ','hvrgehw':`ᠬᠦᠷᠭᠡᠬᠦ${FVS1}`,'eresdel':'ᠡᠷᠡᠰᠳᠡᠯ','buyu':'ᠪᠤᠶᠤ','ayul':'ᠠᠶᠤᠯ','bolhu':'ᠪᠣᠯᠬᠤ','oncalagsan':`ᠣᠨᠴᠠᠯᠠᠭ${FVS2}ᠰᠠᠨ`,'ed':'ᠡᠳ᠋','jasag':'ᠵᠠᠰᠠᠭ','bulun':'ᠪᠤᠯᠤᠨ','sedxil':'ᠰᠡᠳᠬᠢᠯ','jvi':'ᠵᠦᠢ','tala':'ᠲᠠᠯ᠎ᠠ','vilgehw':'ᠦᠢᠯᠡᠬᠦ᠋','cihula':'ᠴᠢᠬᠤᠯᠠ','bvgved':'ᠪᠥᠭᠡᠳ','dergede':'ᠳᠡᠷᠭᠡᠳᠡ','mini':'ᠮᠢᠨᠢ','baiju':'ᠪᠠᠢᠵᠤ','hair-a':'ᠬᠠᠢᠷ᠎ᠠ','dengdegww':'ᠳᠡᠩᠳᠡᠭᠦᠦ','oirahan':'ᠣᠢᠷᠠᠬᠠᠨ','ci':'ᠴᠢ','hajagu':'ᠬᠠᠵᠠᠭᠤ','ene':'ᠡᠨᠡ','olan':'ᠣᠯᠠᠨ','jil':'ᠵᠢᠯ','cimai':'ᠴᠢᠮᠠᠢ','tanigsan':'ᠲᠠᠨᠢᠭᠰᠠᠨ','wgei':'ᠦᠭᠡᠢ','agucila':'ᠠᠭᠤᠴᠢᠯᠠ','nama':'ᠨᠠᠮᠠ','cinu':'ᠴᠢᠨᠤ'},
    // 本次新增
    'harbulga': 'ᠬᠠᠷᠪᠤᠯᠭ᠎ᠠ',
    'sairha': 'ᠰᠠᠢᠷᠬ᠎ᠠ',
    'wrgvlji': 'ᠦᠷᠭᠦᠯᠵᠢ',
    'hereg': 'ᠬᠡᠷᠡᠭ',
    'jambutib': 'ᠽᠠᠮᠪᠤᠲᠢᠪ',
    'oros': 'ᠣᠷᠣᠰ' // for testing
};

// v6.7 核心革命: 深度语法规则库
const SUFFIX_RULES = {
    // Dative-Locative (与格)
    'tu': { masculine: 'ᠲᠤ', feminine: 'ᠲᠦ', neutral: 'ᠲᠤ', endings: ['ᠭ', 'ᠪ', 'ᠰ', 'ᠷ'] }, // 辅音结尾的特殊情况
    'du': { masculine: 'ᠳᠤ', feminine: 'ᠳᠦ', neutral: 'ᠳᠤ' }, // 默认情况
    // Genitive (属格)
    'u': { masculine: 'ᠤ', feminine: 'ᠦ', neutral: 'ᠦ', endings: ['ᠭ', 'ᠪ', 'ᠳ', 'ᠰ', 'ᠷ', 'ᠯ', 'ᠮ', 'ᠨ'] }, // 辅音结尾
    'un': { masculine: 'ᠤᠨ', feminine: 'ᠦᠨ', neutral: 'ᠢᠨ' }, // 元音结尾
    'yin': { all: `᠎ᠶ᠋ᠢᠨ` },
    // Plural (复数)
    'nugud': { masculine: 'ᠨᠤᠭᠤᠳ', feminine: 'ᠨᠦᠭᠦᠳ' },
    'ud': { masculine: 'ᠤᠳ', feminine: 'ᠦᠳ', neutral: 'ᠤᠳ' }, 'nar': { all: 'ᠨᠠᠷ' },
    // Reflexive (反身领属格)
    'degen': { masculine: 'ᠳᠠᠭᠠᠨ', feminine: 'ᠳᠡᠭᠡᠨ' },
    // 其他格
    'aca': { masculine: 'ᠠᠴᠠ', feminine: 'ᠡᠴᠡ', neutral: 'ᠠᠴᠠ' },
    'bar': { masculine: 'ᠪᠠᠷ', feminine: 'ᠪᠡᠷ', neutral: 'ᠢᠶᠠᠷ' },
    'ban': { masculine: 'ᠪᠠᠨ', feminine: 'ᠪᠡᠨ', neutral: 'ᠢᠶᠠᠨ' },
    'tai': { masculine: 'ᠲᠠᠢ', feminine: 'ᠲᠡᠢ', neutral: 'ᠲᠠᠢ' },
    'ni': { all: 'ᠨᠢ' }, 'yi': { all: `᠎ᠶ᠋ᠢ` }, 'i': { all: 'ᠢ' }
};

class MongolianIME {
    constructor(rules, dictionary, suffixRules) { this.rules = rules; this.dictionary = dictionary; this.suffixRules = suffixRules; this.dictKeys = Object.keys(dictionary); this.resetComposition(); }
    resetComposition() { this.composingLatin = ''; this.composingMongol = ''; this.candidates = []; this.selectedCandidateIndex = 0; this.isSuffixMode = false; }
    getWordGender(word) { for (let i = word.length - 1; i >= 0; i--) { const char = word[i]; if (this.rules.vowels.masculine.includes(char)) return 'masculine'; if (this.rules.vowels.feminine.includes(char)) return 'feminine'; } return 'neutral'; }
    convertWordByAlgo(latin) { let mongolWord = ''; let tempLatin = latin; const ligatures = ['ng', 'ch', 'sh', 'zh']; ligatures.forEach(lig => { tempLatin = tempLatin.replace(new RegExp(lig, 'g'), this.rules.latinToMongolMap[lig]); }); for (const char of tempLatin) { mongolWord += this.rules.latinToMongolMap[char] || ''; } return mongolWord.replace(/(.)\1/g, (match, char) => (Object.values(this.rules.vowels).flat().includes(char)) ? char + MVS + char : match); }
    processInput(key) { this.isSuffixMode = false; if (key === 'Backspace') { this.composingLatin = this.composingLatin.slice(0, -1); } else { this.composingLatin += key.toLowerCase(); } this.updateCandidates(); }
    commitCandidate(index) { const word = this.candidates[index]; if (!word) return null; const result = { content: word, isSuffix: this.isSuffixMode }; if (!this.isSuffixMode) { this.resetComposition(); } return result; }
    activateSuffixMode(lastWord) { this.isSuffixMode = true; this.composingLatin = ''; this.updateCandidates(lastWord); }
    navigateCandidates(direction) { if (this.candidates.length === 0) return; if (direction === 'ArrowRight') { this.selectedCandidateIndex = (this.selectedCandidateIndex + 1) % this.candidates.length; } else if (direction === 'ArrowLeft') { this.selectedCandidateIndex = (this.selectedCandidateIndex - 1 + this.candidates.length) % this.candidates.length; } this.composingMongol = this.candidates[this.selectedCandidateIndex] || ''; }
    updateCandidates(lastWord = '') { if (this.isSuffixMode) { this.candidates = this.generateSuffixCandidates(lastWord); } else { if (this.composingLatin === '') { this.resetComposition(); return; } let candidates = new Set(); if (this.dictionary[this.composingLatin]) { candidates.add(this.dictionary[this.composingLatin]); } const mainAlgoResult = this.convertWordByAlgo(this.composingLatin); if (mainAlgoResult) { candidates.add(mainAlgoResult); } const genderSwaps = { 'o': 'v', 'u': 'w', 'g': 'x', 'v': 'o', 'w': 'u', 'x': 'g' }; let swappedLatin = this.composingLatin; let hasSwapped = false; for(const [original, swap] of Object.entries(genderSwaps)) { if(swappedLatin.includes(original)) { swappedLatin = swappedLatin.replace(new RegExp(original, 'g'), swap); hasSwapped = true; } } if(hasSwapped) { const swappedAlgoResult = this.convertWordByAlgo(swappedLatin); if (swappedAlgoResult) candidates.add(swappedAlgoResult); } this.dictKeys.filter(key => key.startsWith(this.composingLatin) && key !== this.composingLatin).forEach(key => candidates.add(this.dictionary[key])); this.candidates = Array.from(candidates); } this.selectedCandidateIndex = 0; this.composingMongol = this.candidates[0] || ''; }
    
    // v6.7 核心革命: 重构的词缀生成器
    generateSuffixCandidates(lastWord) {
        if (!lastWord) return [];
        const gender = this.getWordGender(lastWord);
        const lastChar = lastWord.slice(-1);
        const addedRules = new Set();
        const candidates = [];

        for (const key in this.suffixRules) {
            if (addedRules.has(key)) continue;

            const rule = this.suffixRules[key];
            let suffix = '';

            // 优先处理带辅音结尾规则的
            if (rule.endings && rule.endings.includes(lastChar)) {
                if (gender === 'feminine' && rule.feminine) suffix = rule.feminine;
                else if (rule.masculine) suffix = rule.masculine;
                else if (rule.all) suffix = rule.all;
            } 
            // 否则,处理默认的元音和谐规则
            else if (!rule.endings) {
                if (rule.all) suffix = rule.all;
                else if (gender === 'neutral' && rule.neutral) suffix = rule.neutral;
                else if (gender === 'feminine' && rule.feminine) suffix = rule.feminine;
                else if (rule.masculine) suffix = rule.masculine;
            }

            if (suffix) {
                candidates.push(NNBSP + suffix);
                addedRules.add(key);
            }
        }
        return candidates;
    }
}

const ime = new MongolianIME(RULES, DICTIONARY, SUFFIX_RULES);
const editor = document.getElementById('editor'); const candidateWindow = document.getElementById('candidate-window');
const mainKeyboard = document.getElementById('virtual-keyboard'); const symbolKeyboard = document.getElementById('symbol-keyboard');
let committedText = ''; let historyStack = [];

function setupKeyboards() { mainKeyboard.innerHTML = ` <div class="keyboard-row"> <button class="keyboard-key" data-key="a">a</button> <button class="keyboard-key" data-key="e">e</button> <button class="keyboard-key" data-key="i">i</button> <button class="keyboard-key" data-key="o" data-popup="v,u,w">o</button> </div> <div class="keyboard-row"> <button class="keyboard-key" data-key="n" data-popup="ng">n</button> <button class="keyboard-key" data-key="b" data-popup="p">b</button> <button class="keyboard-key" data-key="d" data-popup="t">d</button> <button class="keyboard-key" data-key="h" data-popup="g,x">h</button> </div> <div class="keyboard-row"> <button class="keyboard-key" data-key="s" data-popup="sh,ch,j,zh">s</button> <button class="keyboard-key" data-key="m">m</button> <button class="keyboard-key" data-key="l">l</button> <button class="keyboard-key" data-key="r">r</button> </div> <div class="keyboard-row"> <button class="keyboard-key special" data-key="Symbols">...</button> <button class="keyboard-key" data-key="y">y</button> <button class="keyboard-key special" data-key="Suffix">᠎</button> <button class="keyboard-key special" data-key="Space">空格</button> <button class="keyboard-key special" data-key="Backspace">⌫</button> </div>`; symbolKeyboard.innerHTML = ` <div class="keyboard-row"> <button class="keyboard-key" data-key="Symbol" data-value="《">《</button> <button class="keyboard-key" data-key="Symbol" data-value="》">》</button> <button class="keyboard-key" data-key="Symbol" data-value="᠄">᠄</button> <button class="keyboard-key" data-key="Symbol" data-value="᠃">᠃</button> </div> <div class="keyboard-row"> <button class="keyboard-key" data-key="Symbol" data-value="᠂">᠂</button> <button class="keyboard-key" data-key="Symbol" data-value="︖">︖</button> <button class="keyboard-key" data-key="Symbol" data-value="︕">︕</button> <button class="keyboard-key" data-key="Symbol" data-value="·">·</button> </div> <div class="keyboard-row"> <button class="keyboard-key" data-key="Symbol" data-value="🍽️">🍽️</button> <button class="keyboard-key" data-key="Symbol" data-value="✂️">✂️</button> <button class="keyboard-key" data-key="Symbol" data-value="🎭">🎭</button> <button class="keyboard-key" data-key="Symbol" data-value="✔️">✔️</button> </div> <div class="keyboard-row"> <button class="keyboard-key special" data-key="SwitchToAlpha">ABC</button> <button class="keyboard-key special" data-key="Space">空格</button> <button class="keyboard-key special" data-key="Backspace">⌫</button> </div>`; }
function render() { const composingSpan = `<span class="composing-text">${ime.composingMongol}</span>`; editor.innerHTML = committedText + (ime.composingLatin || ime.isSuffixMode ? composingSpan : ''); candidateWindow.innerHTML = ''; ime.candidates.forEach((candidate, index) => { const span = document.createElement('span'); span.textContent = candidate.replace(NNBSP, ''); if (index === ime.selectedCandidateIndex) { span.classList.add('selected'); } span.addEventListener('click', () => handleCommit(ime.commitCandidate(index))); candidateWindow.appendChild(span); }); }
function handleCommit(commitResult) { if (commitResult) { let textToAdd = commitResult.content; if (commitResult.isSuffix) { committedText = committedText.trimEnd() + textToAdd; historyStack.push({type: 'suffix', content: textToAdd}); ime.activateSuffixMode(committedText.trim().split(/[\s\u202F《》᠄᠃᠂︖︕·]+/).pop()); } else { textToAdd += ' '; committedText += textToAdd; historyStack.push({type: 'word', content: textToAdd}); } render(); } }
function handleKeyPress(key, value = null) { if (navigator.vibrate) { navigator.vibrate(20); } if (key === 'Suffix') { if (ime.composingLatin.length > 0) { handleCommit(ime.commitCandidate(0)); } const lastWord = committedText.trim().split(/[\s\u202F《》᠄᠃᠂︖︕·]+/).pop(); ime.activateSuffixMode(lastWord); } else if (key === 'Space') { if (ime.composingLatin.length > 0 || ime.isSuffixMode) { handleCommit(ime.commitCandidate(ime.selectedCandidateIndex)); ime.resetComposition(); committedText += ' '; historyStack.push({type: 'space', content: ' '}); } else { committedText += ' '; historyStack.push({type: 'space', content: ' '}); } } else if (key === 'Backspace') { if (ime.composingLatin.length > 0) { ime.processInput('Backspace'); } else if (ime.isSuffixMode) { ime.resetComposition(); } else if (historyStack.length > 0) { const lastChunk = historyStack.pop(); committedText = committedText.slice(0, -lastChunk.content.length); } } else if (key === 'Symbols') { mainKeyboard.style.display = 'none'; symbolKeyboard.style.display = 'block'; } else if (key === 'SwitchToAlpha') { mainKeyboard.style.display = 'block'; symbolKeyboard.style.display = 'none'; } else if (key === 'Symbol') { committedText += value; historyStack.push({type: 'symbol', content: value}); } else { ime.processInput(key); } render(); }
function setupEventListeners() { document.querySelectorAll('.keyboard-key').forEach(key => { key.addEventListener('click', (e) => { document.querySelectorAll('.key-popup').forEach(p => p.remove()); handleKeyPress(e.currentTarget.dataset.key, e.currentTarget.dataset.value); }); }); let longPressTimer; mainKeyboard.querySelectorAll('.keyboard-key[data-popup]').forEach(key => { const primaryKey = key.dataset.key; const popupKeys = key.dataset.popup; const showPopup = () => { document.querySelectorAll('.key-popup').forEach(p => p.remove()); if (!popupKeys) return; const popup = document.createElement('div'); popup.className = 'key-popup'; popupKeys.split(',').forEach(popupKey => { const button = document.createElement('button'); button.textContent = RULES.latinToMongolMap[popupKey] || popupKey.toUpperCase(); button.onclick = (e) => { e.stopPropagation(); handleKeyPress(popupKey); popup.remove(); }; popup.appendChild(button); }); key.appendChild(popup); popup.style.display = 'flex'; }; const handlePressStart = (e) => { e.preventDefault(); longPressTimer = setTimeout(showPopup, 350); }; const handlePressEnd = (e) => { e.preventDefault(); clearTimeout(longPressTimer); }; key.addEventListener('mousedown', handlePressStart); key.addEventListener('mouseup', handlePressEnd); key.addEventListener('touchstart', handlePressStart, { passive: false }); key.addEventListener('touchend', handlePressEnd); }); window.addEventListener('keydown', (e) => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; const key = e.key; if (key.length === 1 && key >= 'a' && key <= 'z') { e.preventDefault(); handleKeyPress(key); return; } switch(key) { case ' ': case 'Enter': e.preventDefault(); handleKeyPress('Space'); break; case 'Backspace': e.preventDefault(); handleKeyPress('Backspace'); break; case '-': e.preventDefault(); handleKeyPress('Suffix'); break; case '/': e.preventDefault(); const isSymbolMode = symbolKeyboard.style.display === 'block'; handleKeyPress(isSymbolMode ? 'SwitchToAlpha' : 'Symbols'); break; case 'ArrowLeft': case 'ArrowRight': e.preventDefault(); ime.navigateCandidates(key); render(); break; default: const num = parseInt(key, 10); if (num >= 1 && num <= 9 && ime.candidates.length > 0) { e.preventDefault(); handleCommit(ime.commitCandidate(num - 1)); } break; } }); }

setupKeyboards();
setupEventListeners();
render(); editor.focus();
</script>

</body>
</html>

以下是传统蒙古文输入法 v7.2 (逻辑精简版)

<!DOCTYPE html>
<html lang="mn">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>传统蒙古文输入法 v7.2 (逻辑精简版)</title>
    <style>
        /* CSS 与 v7.1 完全一致 */
        :root { --key-bg: #fff; --key-text: #000; --key-special-bg: #acb4be; --popup-bg: #e9ecef; --popup-shadow: rgba(0,0,0,0.25); --primary-color: #007bff; --grammar-btn-bg: #e9ecef; --grammar-btn-text: #495057; }
        body { font-family: -apple-system, sans-serif; background-color: #e9ecef; margin: 0; display: flex; flex-direction: column; height: 100vh; }
        .main-content { flex-grow: 1; padding: 10px; display: flex; flex-direction: column; }
        #editor { width: 100%; flex-grow: 1; background: white; border: 1px solid #ccc; border-radius: 8px; padding: 15px; font-size: 28px; writing-mode: vertical-rl; text-orientation: mixed; font-family: 'Mongolian Baiti', 'Menksoft', 'Noto Sans Mongolian', sans-serif; line-height: 1.6; cursor: text; box-sizing: border-box; white-space: pre-wrap; }
        #editor:focus { outline: none; border-color: var(--primary-color); }
        .composing-text { color: var(--primary-color); border-right: 2px solid var(--primary-color); }
        #candidate-window { flex-shrink: 0; background: #fff; padding: 5px; overflow-x: auto; white-space: nowrap; border-bottom: 1px solid #ddd; min-height: 42px; box-sizing: border-box; }
        #candidate-window .candidate { display: inline-block; font-size: 22px; padding: 8px 12px; margin: 2px; border-radius: 5px; font-family: 'Mongolian Baiti', 'Menksoft', 'Noto Sans Mongolian'; cursor: pointer; }
        #candidate-window .candidate.selected { background-color: var(--primary-color); color: white; }
        #candidate-window .grammar-btn { background-color: #f8f9fa; color: #212529; border: 1px solid #adb5bd; box-shadow: 0 1px 1px rgba(0,0,0,0.075); font-weight: 500; }
        #candidate-window .grammar-btn:hover { background-color: #e9ecef; }
        #virtual-keyboard, #symbol-keyboard { position: fixed; bottom: 0; left: 0; right: 0; padding: 5px; background-color: #d1d5db; user-select: none; }
        .keyboard-row { display: flex; justify-content: center; margin: 5px 0; }
        .keyboard-key { flex: 1; margin: 0 3px; padding: 15px 5px; font-size: 20px; text-align: center; border: none; border-radius: 5px; background-color: var(--key-bg); box-shadow: 0 1px 1px rgba(0,0,0,0.2); cursor: pointer; touch-action: manipulation; position: relative; }
        .keyboard-key.special { background-color: var(--key-special-bg); flex-grow: 1.2; }
        .key-popup { position: absolute; bottom: 120%; left: 50%; transform: translateX(-50%); display: none; background-color: var(--popup-bg); border-radius: 8px; padding: 5px; box-shadow: 0 4px 10px var(--popup-shadow); display: flex; gap: 5px; z-index: 1000; }
        .key-popup button { background: #fff; border: none; font-size: 24px; padding: 10px 15px; border-radius: 5px; font-family: 'Mongolian Baiti', 'Menksoft', 'Noto Sans Mongolian'; }
        #symbol-keyboard { display: none; }
        .instructions { background-color: #fff; border: 1px solid #ddd; padding: 15px; border-radius: 8px; line-height: 1.7; font-size: 14px; margin-bottom: 20px; }
    </style>
</head>
<body>

<div class="main-content">
    <div class="instructions">
        <h2>v7.2:逻辑精简版</h2>
        <p><strong>核心逻辑重构:</strong> 输入流程更清晰,状态转换更明确。</p>
        <ul>
            <li><b>单词输入:</b> 正常输入,空格/回车/数字键提交。</li>
            <li><b>进入词缀:</b> 按 <b>`=`</b> 键。</li>
            <li><b>选择词缀:</b> 点击词缀按钮,可连续选择。</li>
            <li><b>结束词缀:</b> 按空格/回车,词缀链最终确定并加空格。</li>
            <li><b>词缀中按字母:</b> 结束词缀并加空格,开始新词。</li>
        </ul>
    </div>
    <div id="editor" tabindex="0"></div>
    <div id="candidate-window"></div>
</div>

<div id="virtual-keyboard"> <!-- HTML is injected by JS --> </div>
<div id="symbol-keyboard"> <!-- HTML is injected by JS --> </div>


<script>
// --- 核心引擎、规则、词典 (与v7.1一致) ---
const MVS = '\u180E'; const NNBSP = '\u180A'; const FVS1 = '\u180B'; const FVS2 = '\u180C';
const RULES = { latinToMongolMap: { 'a': 'ᠠ', 'e': 'ᠡ', 'i': 'ᠢ', 'o': 'ᠣ', 'u': 'ᠤ', 'v': 'ᠥ', 'w': 'ᠦ', 'n': 'ᠨ', 'b': 'ᠪ', 'p': 'ᠫ', 'h': 'ᠬ', 'g': 'ᠭ', 'x': 'ᠺ', 'm': 'ᠮ', 'l': 'ᠯ', 's': 'ᠰ', 'd': 'ᠳ', 't': 'ᠲ', 'c': 'ᠴ', 'j': 'ᠵ', 'y': 'ᠶ', 'r': 'ᠷ', 'f': 'ᠹ', 'z': 'ᠽ', 'k': 'ᠺ', 'q': 'ᠬ', 'ng': 'ᠩ', 'ch': 'ᠴ', 'sh': 'ᠱ', 'zh': 'ᠵ' }, vowels: { masculine: ['ᠠ', 'ᠣ', 'ᠤ'], feminine: ['ᠡ', 'ᠥ', 'ᠦ'], neutral: ['ᠢ'] } };
const DICTIONARY = { ...{'hemnelte':'ᠬᠡᠮᠨᠡᠯᠲᠡ','amidural':'ᠠᠮᠢᠳᠤᠷᠠᠯ','hev':'ᠬᠡᠪ','mayig':'ᠮᠠᠶᠢᠭ','jebhen':'ᠵᠥᠪᠬᠡᠨ','mvnggv':'ᠮᠥᠩᠭᠦ','gamnahu':'ᠭᠠᠮᠨᠠᠬᠤ','bisi':'ᠪᠢᠰᠢ','harin':'ᠬᠠᠷᠢᠨ','uhamsartu':'ᠤᠬᠠᠮᠰᠠᠷᠲᠤ','songgulta':'ᠰᠣᠩᠭᠤᠯᠲᠠ','bolgadag':'ᠪᠣᠯᠭᠠᠳᠠᠭ','hvrvnge':'ᠬᠥᠷᠥᠩᠭᠡ','erhe':'ᠡᠷᠬᠡ','cilwge':'ᠴᠢᠯᠦᠭᠡ','abcirahu':'ᠠᠪᠴᠢᠷᠠᠬᠤ','busu':'ᠪᠤᠰᠤ','ilehw':'ᠢᠯᠡᠬᠦᠦ','emjeg':'ᠡᠮᠵᠡᠭ','baidal':'ᠪᠠᠶᠢᠳᠠᠯ','hvrgehw':`ᠬᠦᠷᠭᠡᠬᠦ${FVS1}`,'eresdel':'ᠡᠷᠡᠰᠳᠡᠯ','buyu':'᪤ᠤᠶᠤ','ayul':'ᠠᠶᠤᠯ','bolhu':'ᠪᠣᠯᠬᠤ','oncalagsan':`ᠣᠨᠴᠠᠯᠠᠭ${FVS2}ᠰᠠᠨ`,'ed':'ᠡᠳ᠋','jasag':'ᠵᠠᠰᠠᠭ','bulun':'ᠪᠤᠯᠤᠨ','sedxil':'ᠰᠡᠳᠬᠢᠯ','jvi':'ᠵᠦᠢ','tala':'ᠲᠠᠯ᠎ᠠ','vilgehw':'ᠦᠢᠯᠡᠬᠦ᠋','cihula':'ᠴᠢᠬᠤᠯᠠ','bvgved':'ᠪᠥᠭᠡᠳ','dergede':'ᠳᠡᠷᠭᠡᠳᠡ','mini':'ᠮᠢᠨᠢ','baiju':'ᠪᠠᠢᠵᠤ','hair-a':'ᠬᠠᠢᠷ᠎ᠠ','dengdegww':'ᠳᠡᠩᠳᠡᠭᠦᠦ','oirahan':'ᠣᠢᠷᠠᠬᠠᠨ','ci':'ᠴᠢ','hajagu':'ᠬᠠᠵᠠᠭᠤ','ene':'ᠡᠨᠡ','olan':'ᠣᠯᠠᠨ','jil':'ᠵᠢᠯ','cimai':'ᠴᠢᠮᠠᠢ','tanigsan':'ᠲᠠᠨᠢᠭᠰᠠᠨ','wgei':'ᠦᠭᠡᠢ','agucila':'ᠠᠭᠤᠴᠢᠯᠠ','nama':'ᠨᠠᠮᠠ','cinu':'ᠴᠢᠨᠤ', 'harbulga': 'ᠬᠠᠷᠪᠤᠯᠭ᠎ᠠ','sairha': 'ᠰᠠᠢᠷᠬ᠎ᠠ','wrgvlji': 'ᠦᠷᠭᠦᠯᠵᠢ','hereg': 'ᠬᠡᠷᠡᠭ','jambutib': 'ᠽᠠᠮᠪᠤᠲᠢᠪ','oros': 'ᠣᠷᠣᠰ'}, 'eji': 'ᠡᠵᠢ', 'harag': 'ᠬᠠᠷᠠᠭ' };
const SUFFIX_RULES = { ...{'tu': { masculine: 'ᠲᠤ', feminine: 'ᠲᠦ', neutral: 'ᠲᠤ', endings: ['ᠭ', 'ᠪ', 'ᠰ', 'ᠷ'] }, 'du': { masculine: 'ᠳᠤ', feminine: 'ᠳᠦ', neutral: 'ᠳᠤ' }, 'u': { masculine: 'ᠤ', feminine: 'ᠦ', neutral: 'ᠦ', endings: ['ᠭ', 'ᠪ', 'ᠳ', 'ᠰ', 'ᠷ', 'ᠯ', 'ᠮ', 'ᠨ'] }, 'un': { masculine: 'ᠤᠨ', feminine: 'ᠦᠨ', neutral: 'ᠢᠨ' }, 'yin': { all: `᠎ᠶ᠋ᠢᠨ` }, 'nugud': { masculine: 'ᠨᠤᠭᠤᠳ', feminine: 'ᠨᠦᠭᠦᠳ' }, 'ud': { masculine: 'ᠤᠳ', feminine: 'ᠦᠳ', neutral: 'ᠤᠳ' }, 'nar': { all: 'ᠨᠠᠷ' }, 'degen': { masculine: 'ᠳᠠᠭᠠᠨ', feminine: 'ᠳᠡᠭᠡᠨ' }, 'aca': { masculine: 'ᠠᠴᠠ', feminine: 'ᠡᠴᠡ', neutral: 'ᠠᠴᠠ' }, 'bar': { masculine: 'ᠪᠠᠷ', feminine: 'ᠪᠡᠷ', neutral: 'ᠢᠶᠠᠷ' }, 'ban': { masculine: 'ᠪᠠᠨ', feminine: 'ᠪᠡᠨ', neutral: 'ᠢᠶᠠᠨ' }, 'tai': { masculine: 'ᠲᠠᠢ', feminine: 'ᠲᠡᠢ', neutral: 'ᠲᠠᠢ' }, 'ni': { all: 'ᠨᠢ' }, 'yi': { all: `᠎ᠶ᠋ᠢ` }, 'i': { all: 'ᠢ' }}, 'a': { masculine: 'ᠠ', feminine: 'ᠡ', neutral: 'ᠠ'} };

class MongolianIME {
    constructor(rules, dictionary, suffixRules) {
        this.rules = rules; this.dictionary = dictionary; this.suffixRules = suffixRules;
        this.dictKeys = Object.keys(dictionary);
        this.resetAllState();
    }

    resetAllState() { // 更彻底的重置
        this.composingLatin = '';
        this.composingMongol = '';
        this.candidates = [];
        this.selectedCandidateIndex = 0;
        this.isSuffixMode = false;
        this.currentBaseWordForSuffix = ''; // 用于词缀模式的基础词
    }

    // --- 单词输入模式核心逻辑 ---
    processLetterInput(key) {
        if (this.isSuffixMode) { // 如果在词缀模式下按字母,则退出词缀模式,并开始新词
            this.exitSuffixModeAndPrepareNewWord();
        }
        this.composingLatin += key.toLowerCase();
        this.updateWordCandidates();
    }

    updateWordCandidates() {
        if (this.composingLatin === '') { this.resetAllState(); return; }
        let candidates = new Set();
        if (this.dictionary[this.composingLatin]) { candidates.add(this.dictionary[this.composingLatin]); }
        const mainAlgoResult = this._convertLatinToMongol(this.composingLatin);
        if (mainAlgoResult && !candidates.has(mainAlgoResult)) { candidates.add(mainAlgoResult); }
        // (智能候补逻辑可以暂时简化或移除,以确保核心流程稳定)
        this.dictKeys.filter(k => k.startsWith(this.composingLatin) && k !== this.composingLatin).forEach(k => {
            if (!candidates.has(this.dictionary[k])) candidates.add(this.dictionary[k]);
        });
        this.candidates = Array.from(candidates);
        this.selectedCandidateIndex = 0;
        this.composingMongol = this.candidates[0] || mainAlgoResult || ''; // 优先显示候选词
    }

    commitCurrentWord(addSpace = true) {
        let wordToCommit = '';
        if (this.candidates.length > 0 && this.candidates[this.selectedCandidateIndex]) {
            wordToCommit = this.candidates[this.selectedCandidateIndex];
        } else if (this.composingMongol) {
            wordToCommit = this.composingMongol;
        }

        if (wordToCommit) {
            committedText += wordToCommit + (addSpace ? ' ' : '');
            historyStack.push({ type: 'word', content: wordToCommit + (addSpace ? ' ' : '') });
        }
        this.resetAllState(); // 提交单词后,彻底重置
    }

    // --- 词缀选择模式核心逻辑 ---
    enterSuffixMode() {
        if (this.composingLatin) { // 如果有正在输入的词,先提交
            this.commitCurrentWord(false); // 提交词干,但不加空格
        }
        const allCommittedWords = committedText.trim().split(/[\s\u202F《》᠄᠃᠂︖︕·]+/);
        this.currentBaseWordForSuffix = allCommittedWords.pop() || '';

        if (this.currentBaseWordForSuffix) {
            this.isSuffixMode = true;
            this.composingLatin = ''; // 清空拉丁输入
            this.composingMongol = ''; // 清空蒙古文转换
            this.updateSuffixCandidates();
        }
    }

    updateSuffixCandidates() {
        if (!this.isSuffixMode || !this.currentBaseWordForSuffix) return;
        this.candidates = this._generateSuffixesForWord(this.currentBaseWordForSuffix);
        this.selectedCandidateIndex = 0; // 在词缀模式下,可以不强调“选中”,因为是按钮点击
    }

    commitSuffix(suffixWithNnbsp) { // suffixWithNnbsp 是类似 "᠎ᠤᠨ"
        if (!this.isSuffixMode || !suffixWithNnbsp) return;
        committedText += suffixWithNnbsp; // 直接附加带NNBSP的词缀
        historyStack.push({ type: 'suffix', content: suffixWithNnbsp });

        // 更新基础词,为下一级词缀做准备
        this.currentBaseWordForSuffix += suffixWithNnbsp;
        this.updateSuffixCandidates(); // 刷新词缀按钮
    }

    exitSuffixModeAndFinalize(addSpace = true) {
        if (this.isSuffixMode) {
            if (addSpace) {
                committedText += ' ';
                historyStack.push({ type: 'space', content: ' ' });
            }
            this.isSuffixMode = false;
            this.resetAllState(); // 退出词缀模式后,彻底重置
        }
    }
    
    exitSuffixModeAndPrepareNewWord() {
        this.exitSuffixModeAndFinalize(true); // 结束词缀并加空格
    }


    // --- 通用辅助函数 ---
    _convertLatinToMongol(latin) {
        let mongolWord = ''; let tempLatin = latin;
        const ligatures = ['ng', 'ch', 'sh', 'zh'];
        ligatures.forEach(lig => { tempLatin = tempLatin.replace(new RegExp(lig, 'g'), this.rules.latinToMongolMap[lig]); });
        for (const char of tempLatin) { mongolWord += this.rules.latinToMongolMap[char] || ''; }
        return mongolWord.replace(/(.)\1/g, (match, char) => (Object.values(this.rules.vowels).flat().includes(char)) ? char + MVS + char : match);
    }

    _getWordGender(word) {
        for (let i = word.length - 1; i >= 0; i--) {
            const char = word[i];
            if (this.rules.vowels.masculine.includes(char)) return 'masculine';
            if (this.rules.vowels.feminine.includes(char)) return 'feminine';
        }
        return 'neutral';
    }

    _generateSuffixesForWord(baseWord) {
        const gender = this._getWordGender(baseWord);
        const lastCharOfBase = baseWord.slice(-1); // 获取基础词的最后一个字符
        const suffixCandidates = [];
        const ruleGroups = {dative: ['du', 'tu'], genitive: ['un', 'u', 'yin'], plural: ['ud', 'nugud', 'nar'], reflexive: ['degen'], ablative: ['aca'], instrumental: ['bar'], comitative:['ban'], possessive: ['ni'], accusative: ['yi', 'i'], adjectival: ['tai'], verbal_a: ['a'] };

        for (const group in ruleGroups) {
            let appliedInGroup = false;
            for (const key of ruleGroups[group]) {
                if (appliedInGroup) continue;
                const rule = this.suffixRules[key];
                let suffixForm = '';
                if (rule.endings && rule.endings.includes(lastCharOfBase)) { // 优先匹配辅音结尾规则
                    if (gender === 'feminine' && rule.feminine) suffixForm = rule.feminine;
                    else if (rule.masculine) suffixForm = rule.masculine;
                    else if (rule.all) suffixForm = rule.all;
                } else if (!rule.endings) { // 默认元音和谐
                    if (rule.all) suffixForm = rule.all;
                    else if (gender === 'neutral' && rule.neutral) suffixForm = rule.neutral;
                    else if (gender === 'feminine' && rule.feminine) suffixForm = rule.feminine;
                    else if (rule.masculine) suffixForm = rule.masculine;
                }
                if (suffixForm) {
                    suffixCandidates.push(NNBSP + suffixForm);
                    appliedInGroup = true;
                }
            }
        }
        return suffixCandidates;
    }

    navigateCandidates(direction) { // 主要用于单词输入模式
        if (this.isSuffixMode || this.candidates.length === 0) return;
        if (direction === 'ArrowRight') { this.selectedCandidateIndex = (this.selectedCandidateIndex + 1) % this.candidates.length; }
        else if (direction === 'ArrowLeft') { this.selectedCandidateIndex = (this.selectedCandidateIndex - 1 + this.candidates.length) % this.candidates.length; }
        this.composingMongol = this.candidates[this.selectedCandidateIndex] || '';
    }
}

const ime = new MongolianIME(RULES, DICTIONARY, SUFFIX_RULES);
const editor = document.getElementById('editor'); const candidateWindow = document.getElementById('candidate-window');
const mainKeyboard = document.getElementById('virtual-keyboard'); const symbolKeyboard = document.getElementById('symbol-keyboard');
let committedText = ''; let historyStack = []; // historyStack 用于智能退格

// --- UI 渲染与事件处理 ---
function render() {
    const displayComposingMongol = ime.isSuffixMode ? '' : ime.composingMongol; // 词缀模式不显示composingMongol
    const composingSpan = `<span class="composing-text">${displayComposingMongol}</span>`;
    editor.innerHTML = committedText + (ime.composingLatin && !ime.isSuffixMode ? composingSpan : '');

    candidateWindow.innerHTML = '';
    if (ime.isSuffixMode) {
        ime.candidates.forEach((suffixCandidate, index) => { // suffixCandidate is like "᠎ᠤᠨ"
            const btn = document.createElement('span');
            btn.className = 'candidate grammar-btn';
            btn.textContent = suffixCandidate.replace(NNBSP, ''); // 显示时去掉NNBSP
            btn.onclick = () => {
                ime.commitSuffix(suffixCandidate); // 提交带NNBSP的词缀
                render(); // 点击后立刻刷新界面
            };
            candidateWindow.appendChild(btn);
        });
    } else {
        ime.candidates.forEach((wordCandidate, index) => {
            const span = document.createElement('span');
            span.className = 'candidate';
            if (index === ime.selectedCandidateIndex) { span.classList.add('selected'); }
            span.textContent = wordCandidate;
            span.onclick = () => {
                ime.selectedCandidateIndex = index; // 更新选中项
                ime.commitCurrentWord(true);
                render();
            };
            candidateWindow.appendChild(span);
        });
    }
}

// v7.2 核心: 全新的 handleKeyPress
function handleKeyPress(key, value = null) {
    if (navigator.vibrate) { navigator.vibrate(20); }

    // 字母键 (a-z)
    if (key.length === 1 && key >= 'a' && key <= 'z') {
        ime.processLetterInput(key);
    }
    // 功能键
    else {
        switch (key) {
            case 'Space':
            case 'Enter': // 将回车视为与空格相同的提交操作
                if (ime.isSuffixMode) {
                    ime.exitSuffixModeAndFinalize(true);
                } else if (ime.composingLatin) {
                    ime.commitCurrentWord(true);
                } else { // 无输入时,直接加空格
                    committedText += ' ';
                    historyStack.push({ type: 'space', content: ' ' });
                }
                break;
            case 'SuffixTrigger': // 即 '=' 键
                 if (ime.isSuffixMode) { // 如果已在词缀模式,再按=,则视为结束当前词缀链
                    ime.exitSuffixModeAndFinalize(true);
                } else {
                    ime.enterSuffixMode();
                }
                break;
            case 'Backspace':
                if (ime.composingLatin && !ime.isSuffixMode) {
                    ime.composingLatin = ime.composingLatin.slice(0, -1);
                    ime.updateWordCandidates();
                } else if (ime.isSuffixMode) {
                    ime.exitSuffixModeAndFinalize(false); // 退出词缀模式,但不加空格
                } else if (historyStack.length > 0) {
                    const lastChunk = historyStack.pop();
                    committedText = committedText.slice(0, -lastChunk.content.length);
                }
                break;
            case 'Symbols':
                mainKeyboard.style.display = 'none'; symbolKeyboard.style.display = 'block';
                break;
            case 'SwitchToAlpha':
                mainKeyboard.style.display = 'block'; symbolKeyboard.style.display = 'none';
                break;
            case 'Symbol':
                if (ime.isSuffixMode) ime.exitSuffixModeAndFinalize(false); // 如果在词缀模式输符号,先退出
                committedText += value; historyStack.push({ type: 'symbol', content: value });
                break;
            // 物理键盘数字选词 (仅在单词输入模式)
            default:
                if (!ime.isSuffixMode) {
                    const num = parseInt(key, 10);
                    if (num >= 1 && num <= 9 && ime.candidates.length >= num) {
                        ime.selectedCandidateIndex = num - 1;
                        ime.commitCurrentWord(true);
                    }
                }
                break;
        }
    }
    render();
}

function setupKeyboards() { mainKeyboard.innerHTML = ` <div class="keyboard-row"> <button class="keyboard-key" data-key="a">a</button> <button class="keyboard-key" data-key="e">e</button> <button class="keyboard-key" data-key="i">i</button> <button class="keyboard-key" data-key="o" data-popup="v,u,w">o</button> </div> <div class="keyboard-row"> <button class="keyboard-key" data-key="n" data-popup="ng">n</button> <button class="keyboard-key" data-key="b" data-popup="p">b</button> <button class="keyboard-key" data-key="d" data-popup="t">d</button> <button class="keyboard-key" data-key="h" data-popup="g,x">h</button> </div> <div class="keyboard-row"> <button class="keyboard-key" data-key="s" data-popup="sh,ch,j,zh">s</button> <button class="keyboard-key" data-key="m">m</button> <button class="keyboard-key" data-key="l">l</button> <button class="keyboard-key" data-key="r">r</button> </div> <div class="keyboard-row"> <button class="keyboard-key special" data-key="Symbols">...</button> <button class="keyboard-key" data-key="y">y</button> <button class="keyboard-key special" data-key="SuffixTrigger">=</button> <button class="keyboard-key special" data-key="Space">空格</button> <button class="keyboard-key special" data-key="Backspace">⌫</button> </div>`; symbolKeyboard.innerHTML = ` <div class="keyboard-row"> <button class="keyboard-key" data-key="Symbol" data-value="《">《</button> <button class="keyboard-key" data-key="Symbol" data-value="》">》</button> <button class="keyboard-key" data-key="Symbol" data-value="᠄">᠄</button> <button class="keyboard-key" data-key="Symbol" data-value="᠃">᠃</button> </div> <div class="keyboard-row"> <button class="keyboard-key" data-key="Symbol" data-value="᠂">᠂</button> <button class="keyboard-key" data-key="Symbol" data-value="︖">︖</button> <button class="keyboard-key" data-key="Symbol" data-value="︕">︕</button> <button class="keyboard-key" data-key="Symbol" data-value="·">·</button> </div> <div class="keyboard-row"> <button class="keyboard-key" data-key="Symbol" data-value="🍽️">🍽️</button> <button class="keyboard-key" data-key="Symbol" data-value="✂️">✂️</button> <button class="keyboard-key" data-key="Symbol" data-value="🎭">🎭</button> <button class="keyboard-key" data-key="Symbol" data-value="✔️">✔️</button> </div> <div class="keyboard-row"> <button class="keyboard-key special" data-key="SwitchToAlpha">ABC</button> <button class="keyboard-key special" data-key="Space">空格</button> <button class="keyboard-key special" data-key="Backspace">⌫</button> </div>`; }
function setupEventListeners() { document.querySelectorAll('.keyboard-key').forEach(key => { key.addEventListener('click', (e) => { document.querySelectorAll('.key-popup').forEach(p => p.remove()); handleKeyPress(e.currentTarget.dataset.key, e.currentTarget.dataset.value); }); }); let longPressTimer; mainKeyboard.querySelectorAll('.keyboard-key[data-popup]').forEach(key => { const primaryKey = key.dataset.key; const popupKeys = key.dataset.popup; const showPopup = () => { document.querySelectorAll('.key-popup').forEach(p => p.remove()); if (!popupKeys) return; const popup = document.createElement('div'); popup.className = 'key-popup'; popupKeys.split(',').forEach(popupKey => { const button = document.createElement('button'); button.textContent = RULES.latinToMongolMap[popupKey] || popupKey.toUpperCase(); button.onclick = (e) => { e.stopPropagation(); handleKeyPress(popupKey); popup.remove(); }; popup.appendChild(button); }); key.appendChild(popup); popup.style.display = 'flex'; }; const handlePressStart = (e) => { e.preventDefault(); longPressTimer = setTimeout(showPopup, 350); }; const handlePressEnd = (e) => { e.preventDefault(); clearTimeout(longPressTimer); }; key.addEventListener('mousedown', handlePressStart); key.addEventListener('mouseup', handlePressEnd); key.addEventListener('touchstart', handlePressStart, { passive: false }); key.addEventListener('touchend', handlePressEnd); });
    // 物理键盘监听
    window.addEventListener('keydown', (e) => {
        if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
        let keyToProcess = '';
        if (e.key.length === 1 && e.key >= 'a' && e.key <= 'z') { keyToProcess = e.key; }
        else if (e.key === ' ' || e.key === 'Enter') { keyToProcess = 'Space'; } // Enter 也视为 Space
        else if (e.key === 'Backspace') { keyToProcess = 'Backspace'; }
        else if (e.key === '=' || e.key === '-') { keyToProcess = 'SuffixTrigger'; }
        else if (e.key === '/') { keyToProcess = symbolKeyboard.style.display === 'block' ? 'SwitchToAlpha' : 'Symbols'; }
        else if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
            if (!ime.isSuffixMode) { // 仅在单词输入模式下导航候选词
                ime.navigateCandidates(e.key);
                render();
            }
            e.preventDefault(); return;
        } else { // 数字键选词
            const num = parseInt(e.key, 10);
            if (num >= 1 && num <= 9) {
                keyToProcess = e.key; // 将数字作为key传给handleKeyPress
            }
        }

        if (keyToProcess) {
            e.preventDefault();
            handleKeyPress(keyToProcess);
        }
    });
}

setupKeyboards();
setupEventListeners();
render();
editor.focus();
</script>

</body>
</html>
THE END
分享
评论 共2条
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片快捷回复
    • 头像兄兄0