diff --git a/README.en.md b/README.en.md index ae345a33..60c5d774 100644 --- a/README.en.md +++ b/README.en.md @@ -27,7 +27,7 @@ You need the **Microsoft Visual C++ 2015–2022 Redistributable (x64)**. The ser ## What it does -- Chinese input: full pinyin, double pinyin (Xiaohe / Ziranma / Shoudao / Microsoft), Wubi 86 +- Chinese input: full pinyin, double pinyin (Xiaohe / Ziranma / Shoudao / Microsoft / Sogou / Ziguang / Zhineng ABC / Guobiao / Pinyin Jiajia), Wubi 86 - Japanese input: romaji, with hiragana, katakana and dictionary candidates; also available as a temporary mode from Chinese - Helpcode (形码) filtering on pinyin schemes: six tables, single- and double-code - Optional cloud candidates, and AI suggestions via DeepSeek / OpenAI / SiliconFlow / Groq diff --git a/README.md b/README.md index 7c153491..07a94563 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ Windows 端的全部一方源码都在本仓,一次 clone 就能拿到完整 ## 功能简介 -- 中文输入:全拼、双拼(小鹤 / 自然码 / 首道 / 微软)、86 五笔。 +- 中文输入:全拼、双拼(小鹤 / 自然码 / 首道 / 微软 / 搜狗 / 紫光 / 智能ABC / 国标 / 拼音加加)、86 五笔。 - 日文输入:罗马字方案,提供平假名、片假名和日语词库候选;中文状态下也可用临时日语(R 模式)。 - 拼音方案支持辅助码(蓝天小雨点、自然码、首右 2.0、首右 Plus、小鹤、拼音加加),也可以导入自己的辅助码方案。 - 支持谷歌云候选、AI 联想(DeepSeek / OpenAI / SiliconFlow / Groq)。 @@ -106,7 +106,7 @@ Windows 端的全部一方源码都在本仓,一次 clone 就能拿到完整 在设置 → 输入中切换中文或日文模式。中文方案包括: - **全拼**:按完整拼音输入。 -- **双拼**:用两个按键组合声母和韵母。可选小鹤双拼、自然码双拼、首道双拼和微软双拼。 +- **双拼**:用两个按键组合声母和韵母。可选小鹤双拼、自然码双拼、首道双拼、微软双拼、搜狗双拼、紫光双拼、智能ABC、国标双拼和拼音加加。微软、搜狗和紫光把 ing 放在分号键上。 - **五笔**:当前提供 86 五笔。 翻页按键可在设置中勾选:`-` / `=`、`,` / `.`、`Shift + Tab` / `Tab`、`Page Up` / `Page Down`;`↑` / `↓` 用于移动高亮候选项。 diff --git a/engine/contracts/windows_ipc.h b/engine/contracts/windows_ipc.h index 81f4809b..8701265f 100644 --- a/engine/contracts/windows_ipc.h +++ b/engine/contracts/windows_ipc.h @@ -362,7 +362,8 @@ constexpr std::uint32_t InsertText = 10; constexpr std::uint32_t SmartPunctuationChanged = 11; // Auto-complete opening paired punctuation and leave the caret inside. Payload "0"/"1". constexpr std::uint32_t PairedPunctuationChanged = 12; -// Whether ';' is an input key for the Microsoft shuangpin profile. +// Whether ';' is an input key for the configured shuangpin profile (Microsoft, Sogou and +// Ziguang put the ing final there). The name predates the other profiles. constexpr std::uint32_t MicrosoftShuangpinChanged = 13; // Revert a converted ASCII punctuation back to Chinese when the same // punctuation key is pressed within the revert window. Payload "0"/"1". diff --git a/engine/core/input_session.cpp b/engine/core/input_session.cpp index b7ee6603..dda51511 100644 --- a/engine/core/input_session.cpp +++ b/engine/core/input_session.cpp @@ -160,12 +160,12 @@ KeyResult InputSession::handle_character(char character, bool shift_only) } const bool lowercase_letter = character >= 'a' && character <= 'z'; - const bool microsoft_final = - character == ';' && scheme() == SchemeType::Shuangpin && shuangpin_profile_.name == "microsoft"; + const bool semicolon_final = + character == ';' && scheme() == SchemeType::Shuangpin && ShuangpinProfileUsesSemicolonFinal(shuangpin_profile_); const bool active_helpcode = character >= 'A' && character <= 'Z' && has_composition() && ((scheme() == SchemeType::Quanpin && quanpin_helpcode_enabled_) || (scheme() == SchemeType::Shuangpin && shuangpin_helpcode_enabled_)); - if (!lowercase_letter && !active_helpcode && character != '\'' && !microsoft_final) + if (!lowercase_letter && !active_helpcode && character != '\'' && !semicolon_final) { return {}; } @@ -179,7 +179,7 @@ KeyResult InputSession::handle_character(char character, bool shift_only) const ImeKeyCode key_code = character == '\'' ? ImeKey::Apostrophe - : (microsoft_final ? ImeKey::Semicolon : static_cast(std::toupper(unsigned_character))); + : (semicolon_final ? ImeKey::Semicolon : static_cast(std::toupper(unsigned_character))); engine_.handle_key(key_code, 0, static_cast(unsigned_character)); update_mixed_candidates(); const bool handled = preedit() != previous_preedit; diff --git a/engine/core/input_session_editing.cpp b/engine/core/input_session_editing.cpp index 616c6a52..aa35ce0d 100644 --- a/engine/core/input_session_editing.cpp +++ b/engine/core/input_session_editing.cpp @@ -190,7 +190,8 @@ KeyResult InputSession::insert_at_caret(char character) case LocalInputMode::None: accepted = lower || (upper && ((scheme() == SchemeType::Quanpin && quanpin_helpcode_enabled_) || (scheme() == SchemeType::Shuangpin && shuangpin_helpcode_enabled_))); - if (character == ';' && scheme() == SchemeType::Shuangpin && shuangpin_profile_.name == "microsoft") + if (character == ';' && scheme() == SchemeType::Shuangpin && + ShuangpinProfileUsesSemicolonFinal(shuangpin_profile_)) { const auto separator = caret == 0 ? std::string::npos : text.rfind('\'', caret - 1); const auto start = separator == std::string::npos ? 0 : separator + 1; diff --git a/engine/schemes/shuangpin_scheme.cpp b/engine/schemes/shuangpin_scheme.cpp index 7936808e..ceee0a9f 100644 --- a/engine/schemes/shuangpin_scheme.cpp +++ b/engine/schemes/shuangpin_scheme.cpp @@ -12,7 +12,7 @@ bool is_alpha_vk(ImeKeyCode vk) bool is_microsoft_ing_key(ImeKeyCode vk, ImeCharacter wch, const std::string &raw_input, const ShuangpinProfile &profile) { - if (profile.name != "microsoft" || vk != ImeKey::Semicolon || wch != u';') + if (!ShuangpinProfileUsesSemicolonFinal(profile) || vk != ImeKey::Semicolon || wch != u';') { return false; } diff --git a/engine/shuangpin/shuangpin_dictionary.cpp b/engine/shuangpin/shuangpin_dictionary.cpp index 4d400823..addbe4fc 100644 --- a/engine/shuangpin/shuangpin_dictionary.cpp +++ b/engine/shuangpin/shuangpin_dictionary.cpp @@ -567,7 +567,7 @@ int ShuangpinDictionary::handleVkCode(ImeKeyCode vk, ImeModifierMask modifiers_d _pinyin_sequence_with_cases += lowerAlpha; } } - else if (profile_.name == "microsoft" && vk == ImeKey::Semicolon && wch == u';' && + else if (ShuangpinProfileUsesSemicolonFinal(profile_) && vk == ImeKey::Semicolon && wch == u';' && _pinyin_sequence.size() % 2 == 1) { _pinyin_sequence += ';'; diff --git a/engine/shuangpin/shuangpin_profile.cpp b/engine/shuangpin/shuangpin_profile.cpp index 0ac1d9e8..17c23240 100644 --- a/engine/shuangpin/shuangpin_profile.cpp +++ b/engine/shuangpin/shuangpin_profile.cpp @@ -137,8 +137,241 @@ const ShuangpinProfile &GetMicrosoftShuangpinProfile() return profile; } +const ShuangpinProfile &GetSogouShuangpinProfile() +{ + static const ShuangpinProfile profile{ + "sogou", + { + {"sh", "u"}, + {"ch", "i"}, + {"zh", "v"}, + }, + { + {"a", "oa"}, + {"ai", "ol"}, + {"an", "oj"}, + {"ang", "oh"}, + {"ao", "ok"}, + {"e", "oe"}, + {"ei", "oz"}, + {"en", "of"}, + {"eng", "og"}, + {"er", "or"}, + {"o", "oo"}, + {"ou", "ob"}, + }, + { + {"iu", "q"}, {"ia", "w"}, {"ua", "w"}, {"e", "e"}, {"uan", "r"}, {"ue", "t"}, {"ve", "t"}, + {"uai", "y"}, {"v", "y"}, {"u", "u"}, {"i", "i"}, {"o", "o"}, {"uo", "o"}, {"un", "p"}, + {"a", "a"}, {"iong", "s"}, {"ong", "s"}, {"iang", "d"}, {"uang", "d"}, {"en", "f"}, {"eng", "g"}, + {"ang", "h"}, {"an", "j"}, {"ao", "k"}, {"ai", "l"}, {"ing", ";"}, {"ei", "z"}, {"ie", "x"}, + {"iao", "c"}, {"ui", "v"}, {"ou", "b"}, {"in", "n"}, {"ian", "m"}, + }, + }; + return profile; +} + +const ShuangpinProfile &GetZiguangShuangpinProfile() +{ + static const ShuangpinProfile profile{ + "ziguang", + { + {"sh", "i"}, + {"ch", "a"}, + {"zh", "u"}, + }, + { + {"a", "oa"}, + {"ai", "op"}, + {"an", "or"}, + {"ang", "os"}, + {"ao", "oq"}, + {"e", "oe"}, + {"ei", "ok"}, + {"en", "ow"}, + {"eng", "ot"}, + {"er", "oj"}, + {"o", "oo"}, + {"ou", "oz"}, + }, + { + {"ao", "q"}, {"en", "w"}, {"e", "e"}, {"an", "r"}, {"eng", "t"}, {"in", "y"}, {"uai", "y"}, + {"u", "u"}, {"i", "i"}, {"o", "o"}, {"uo", "o"}, {"ai", "p"}, {"a", "a"}, {"ang", "s"}, + {"ie", "d"}, {"ian", "f"}, {"iang", "g"}, {"uang", "g"}, {"ong", "h"}, {"iong", "h"}, {"iu", "j"}, + {"ei", "k"}, {"uan", "l"}, {"ing", ";"}, {"ou", "z"}, {"ia", "x"}, {"ua", "x"}, {"v", "v"}, + {"iao", "b"}, {"ui", "n"}, {"ue", "n"}, {"ve", "n"}, {"un", "m"}, + }, + }; + return profile; +} + +const ShuangpinProfile &GetZhinengAbcShuangpinProfile() +{ + static const ShuangpinProfile profile{ + "zhinengabc", + { + {"sh", "v"}, + {"ch", "e"}, + {"zh", "a"}, + }, + { + {"a", "oa"}, + {"ai", "ol"}, + {"an", "oj"}, + {"ang", "oh"}, + {"ao", "ok"}, + {"e", "oe"}, + {"ei", "oq"}, + {"en", "of"}, + {"eng", "og"}, + {"er", "or"}, + {"o", "oo"}, + {"ou", "ob"}, + }, + { + {"ei", "q"}, + {"ian", "w"}, + {"e", "e"}, + {"iu", "r"}, + {"iang", "t"}, + {"uang", "t"}, + {"ing", "y"}, + {"u", "u"}, + {"i", "i"}, + {"o", "o"}, + {"uo", "o"}, + {"uan", "p"}, + {"a", "a"}, + {"ong", "s"}, + {"iong", "s"}, + {"ia", "d"}, + {"ua", "d"}, + {"en", "f"}, + {"eng", "g"}, + {"ang", "h"}, + {"an", "j"}, + {"ao", "k"}, + {"ai", "l"}, + {"iao", "z"}, + {"ie", "x"}, + {"in", "c"}, + {"uai", "c"}, + {"v", "v"}, + {"ou", "b"}, + {"un", "n"}, + {"ue", "m"}, + {"ui", "m"}, + // The chart also prints "ve" on V, but then lv/nv would decode as both lü and lüe. + // Standard ABC types lüe/nüe on M together with ue. + {"ve", "m"}, + }, + }; + return profile; +} + +const ShuangpinProfile &GetGuobiaoShuangpinProfile() +{ + static const ShuangpinProfile profile{ + "guobiao", + { + {"sh", "u"}, + {"ch", "i"}, + {"zh", "v"}, + }, + { + {"a", "aa"}, + {"ai", "ak"}, + {"an", "af"}, + {"ang", "ag"}, + {"ao", "ac"}, + {"e", "ae"}, + {"ei", "ab"}, + {"en", "ar"}, + {"eng", "ah"}, + {"er", "al"}, + {"o", "ao"}, + {"ou", "ap"}, + }, + { + // "van"/"vn" on W/Z are the same keys as uan/un after j/q/x/y, so they need no entry. + {"ia", "q"}, {"ua", "q"}, {"uan", "w"}, {"e", "e"}, {"en", "r"}, {"ie", "t"}, {"iu", "y"}, + {"uai", "y"}, {"u", "u"}, {"i", "i"}, {"o", "o"}, {"uo", "o"}, {"ou", "p"}, {"a", "a"}, + {"ong", "s"}, {"iong", "s"}, {"ian", "d"}, {"an", "f"}, {"ang", "g"}, {"eng", "h"}, {"ing", "j"}, + {"ai", "k"}, {"in", "l"}, {"un", "z"}, {"ue", "x"}, {"ve", "x"}, {"ao", "c"}, {"v", "v"}, + {"ui", "v"}, {"ei", "b"}, {"iang", "n"}, {"uang", "n"}, {"iao", "m"}, + }, + }; + return profile; +} + +const ShuangpinProfile &GetPinyinJiajiaShuangpinProfile() +{ + static const ShuangpinProfile profile{ + "pinyinjiajia", + { + {"sh", "i"}, + {"ch", "u"}, + {"zh", "v"}, + }, + { + {"a", "aa"}, + {"ai", "as"}, + {"an", "af"}, + {"ang", "ag"}, + {"ao", "ad"}, + {"e", "ee"}, + {"ei", "ew"}, + {"en", "er"}, + {"eng", "et"}, + {"er", "eq"}, + {"o", "oo"}, + {"ou", "op"}, + }, + { + {"ing", "q"}, {"ei", "w"}, {"e", "e"}, {"en", "r"}, {"eng", "t"}, {"ong", "y"}, {"iong", "y"}, + {"u", "u"}, {"i", "i"}, {"o", "o"}, {"uo", "o"}, {"ou", "p"}, {"a", "a"}, {"ai", "s"}, + {"ao", "d"}, {"an", "f"}, {"ang", "g"}, {"iang", "h"}, {"uang", "h"}, {"ian", "j"}, {"iao", "k"}, + {"in", "l"}, {"un", "z"}, {"ue", "x"}, {"ve", "x"}, {"uai", "x"}, {"uan", "c"}, {"ui", "v"}, + {"v", "v"}, {"ia", "b"}, {"ua", "b"}, {"iu", "n"}, {"ie", "m"}, + }, + }; + return profile; +} + +bool ShuangpinProfileUsesSemicolonFinal(const ShuangpinProfile &profile) +{ + for (const auto &pair : profile.finals) + { + if (pair.second == ";") + { + return true; + } + } + return false; +} + const ShuangpinProfile &GetShuangpinProfile(std::string_view name) { + if (name == "sogou") + { + return GetSogouShuangpinProfile(); + } + if (name == "ziguang") + { + return GetZiguangShuangpinProfile(); + } + if (name == "zhinengabc") + { + return GetZhinengAbcShuangpinProfile(); + } + if (name == "guobiao") + { + return GetGuobiaoShuangpinProfile(); + } + if (name == "pinyinjiajia") + { + return GetPinyinJiajiaShuangpinProfile(); + } if (name == "ziranma") { return GetZiranmaShuangpinProfile(); diff --git a/engine/shuangpin/shuangpin_profile.h b/engine/shuangpin/shuangpin_profile.h index 8d2421aa..40c3d6d5 100644 --- a/engine/shuangpin/shuangpin_profile.h +++ b/engine/shuangpin/shuangpin_profile.h @@ -17,4 +17,13 @@ const ShuangpinProfile &GetXiaoheShuangpinProfile(); const ShuangpinProfile &GetZiranmaShuangpinProfile(); const ShuangpinProfile &GetShoudaoShuangpinProfile(); const ShuangpinProfile &GetMicrosoftShuangpinProfile(); +const ShuangpinProfile &GetSogouShuangpinProfile(); +const ShuangpinProfile &GetZiguangShuangpinProfile(); +const ShuangpinProfile &GetZhinengAbcShuangpinProfile(); +const ShuangpinProfile &GetGuobiaoShuangpinProfile(); +const ShuangpinProfile &GetPinyinJiajiaShuangpinProfile(); const ShuangpinProfile &GetShuangpinProfile(std::string_view name); + +// Microsoft, Sogou and Ziguang put the "ing" final on ';', so ';' is an input key +// in the second position of a syllable for those profiles. +bool ShuangpinProfileUsesSemicolonFinal(const ShuangpinProfile &profile); diff --git a/installer/default_config/config.default.toml b/installer/default_config/config.default.toml index a4b305ff..a3345c47 100644 --- a/installer/default_config/config.default.toml +++ b/installer/default_config/config.default.toml @@ -228,7 +228,7 @@ schema = "quanpin" # quanpin -> 预编辑显示转换后的全拼分词 # shuangpin -> 预编辑显示原始双拼输入串 shuangpin_preedit_mode = "shuangpin" -# 双拼方案,可选:"xiaohe", "ziranma", "shoudao", "microsoft" +# 双拼方案,可选:"xiaohe", "ziranma", "shoudao", "microsoft", "sogou", "ziguang", "zhinengabc", "guobiao", "pinyinjiajia" # shuangpin_schema = "xiaohe" shuangpin_schema = "xiaohe" # 五笔方案,当前仅支持 "wubi86";98 版待词库发布后另开任务接线 diff --git a/server/assets/config/config.toml b/server/assets/config/config.toml index 1a8b8916..246817f5 100644 --- a/server/assets/config/config.toml +++ b/server/assets/config/config.toml @@ -228,7 +228,7 @@ schema = "shuangpin" # quanpin -> 预编辑显示转换后的全拼分词 # shuangpin -> 预编辑显示原始双拼输入串 shuangpin_preedit_mode = "shuangpin" -# 双拼方案,可选:"xiaohe", "ziranma", "shoudao", "microsoft" +# 双拼方案,可选:"xiaohe", "ziranma", "shoudao", "microsoft", "sogou", "ziguang", "zhinengabc", "guobiao", "pinyinjiajia" # shuangpin_schema = "xiaohe" shuangpin_schema = "xiaohe" # 五笔方案,当前仅支持 "wubi86";98 版待词库发布后另开任务接线 diff --git a/server/src/config/ime_config.h b/server/src/config/ime_config.h index 805c6ece..b4f3f0a1 100644 --- a/server/src/config/ime_config.h +++ b/server/src/config/ime_config.h @@ -205,6 +205,8 @@ bool SetConfiguredSwitchLanguageCtrlEnabled(bool enabled); bool GetConfiguredSwitchLanguageCtrlAltSpaceEnabled(); bool SetConfiguredSwitchLanguageCtrlAltSpaceEnabled(bool enabled); const std::string &GetConfiguredShuangpinSchema(); +// 当前双拼方案是否把 ing 放在分号键上(微软、搜狗、紫光),此时分号在音节第二位是输入键。 +bool IsConfiguredShuangpinSemicolonFinal(); bool SetConfiguredShuangpinSchema(const std::string &schema); const std::string &GetConfiguredWubiSchema(); bool SetConfiguredWubiSchema(const std::string &schema); diff --git a/server/src/config/ime_config_input.cpp b/server/src/config/ime_config_input.cpp index d21d75de..0bcc278a 100644 --- a/server/src/config/ime_config_input.cpp +++ b/server/src/config/ime_config_input.cpp @@ -8,6 +8,7 @@ #include "defines/defines.h" #include "engine/common/helpcode_utils.h" #include "engine/core/data_path.h" +#include "engine/shuangpin/shuangpin_profile.h" using namespace ime_config_detail; @@ -179,9 +180,15 @@ const std::string &GetConfiguredShuangpinSchema() return g_shuangpin_schema; } +bool IsConfiguredShuangpinSemicolonFinal() +{ + return ShuangpinProfileUsesSemicolonFinal(GetShuangpinProfile(g_shuangpin_schema)); +} + bool SetConfiguredShuangpinSchema(const std::string &schema) { - if (schema != "xiaohe" && schema != "ziranma" && schema != "shoudao" && schema != "microsoft") + // The resolver falls back to Xiaohe for unknown names, so a name round-trip means "built in". + if (GetShuangpinProfile(schema).name != schema) { return false; } diff --git a/server/src/ipc/event_listener_keys.cpp b/server/src/ipc/event_listener_keys.cpp index aa1c9169..86b7c372 100644 --- a/server/src/ipc/event_listener_keys.cpp +++ b/server/src/ipc/event_listener_keys.cpp @@ -118,8 +118,8 @@ bool IsManualPinyinSeparatorKey(UINT keycode, WCHAR wch) bool IsMicrosoftShuangpinIngKey(UINT keycode, WCHAR wch, const std::string &raw_input) { - if (keycode != VK_OEM_1 || wch != L';' || GetConfiguredShuangpinSchema() != "microsoft" || - g_inputSession == nullptr || g_inputSession->current_scheme_type() != SchemeType::Shuangpin) + if (keycode != VK_OEM_1 || wch != L';' || !IsConfiguredShuangpinSemicolonFinal() || g_inputSession == nullptr || + g_inputSession->current_scheme_type() != SchemeType::Shuangpin) { return false; } @@ -344,7 +344,7 @@ bool ApplyCompositionEditKey(UINT keycode, WCHAR wch, UINT modifiers_down, bool { input = '\''; } - else if (keycode == VK_OEM_1 && wch == L';' && GetConfiguredShuangpinSchema() == "microsoft" && + else if (keycode == VK_OEM_1 && wch == L';' && IsConfiguredShuangpinSemicolonFinal() && g_inputSession->current_scheme_type() == SchemeType::Shuangpin) { input = ';'; diff --git a/server/src/ipc/event_listener_pipes.cpp b/server/src/ipc/event_listener_pipes.cpp index 95ea60ad..19bbe11b 100644 --- a/server/src/ipc/event_listener_pipes.cpp +++ b/server/src/ipc/event_listener_pipes.cpp @@ -838,7 +838,7 @@ void RegisteredPipeMonitorThread(HANDLE clientPipe, UINT pipeRole, uint64_t hand GetConfiguredPairedPunctuationEnabled() ? L"1" : L"0"); SendToTsfWorkerThreadClientViaNamedpipe( hello.client_id, Global::DataFromServerMsgTypeToTsfWorkerThread::MicrosoftShuangpinChanged, - GetConfiguredShuangpinSchema() == "microsoft" ? L"1" : L"0"); + IsConfiguredShuangpinSemicolonFinal() ? L"1" : L"0"); SendToTsfWorkerThreadClientViaNamedpipe(hello.client_id, Global::DataFromServerMsgTypeToTsfWorkerThread::InputModeChanged, GetConfiguredInputMode() == "japanese" ? L"1" : L"0"); diff --git a/server/src/window/ime_windows.cpp b/server/src/window/ime_windows.cpp index 76049841..517bfe2a 100644 --- a/server/src/window/ime_windows.cpp +++ b/server/src/window/ime_windows.cpp @@ -271,7 +271,7 @@ void ApplyConfiguredShuangpinSchema() { FanyNamedPipe::EnqueueReloadInputSessionTask(); BroadcastToTsfWorkerThreadViaNamedpipe(Global::DataFromServerMsgTypeToTsfWorkerThread::MicrosoftShuangpinChanged, - GetConfiguredShuangpinSchema() == "microsoft" ? L"1" : L"0"); + IsConfiguredShuangpinSemicolonFinal() ? L"1" : L"0"); } LRESULT RegisterCandidateWindowMessage() diff --git a/server/tests/src/test_shuangpin_scheme.cpp b/server/tests/src/test_shuangpin_scheme.cpp index 79638cd4..2dced8ab 100644 --- a/server/tests/src/test_shuangpin_scheme.cpp +++ b/server/tests/src/test_shuangpin_scheme.cpp @@ -2,6 +2,9 @@ #include "engine/core/ime_session.h" #include "engine/schemes/shuangpin_scheme.h" #include "engine/shuangpin/shuangpin_query.h" +#include "engine/quanpin/quanpin_utils.h" +#include +#include #include #include @@ -36,8 +39,10 @@ const ShuangpinProfile &GetTestShuangpinProfile() TEST_CASE(ShuangpinProfilesDecodeYoAsOneSyllable) { - for (const auto *profile : {&GetXiaoheShuangpinProfile(), &GetZiranmaShuangpinProfile(), - &GetShoudaoShuangpinProfile(), &GetMicrosoftShuangpinProfile()}) + for (const auto *profile : + {&GetXiaoheShuangpinProfile(), &GetZiranmaShuangpinProfile(), &GetShoudaoShuangpinProfile(), + &GetMicrosoftShuangpinProfile(), &GetSogouShuangpinProfile(), &GetZiguangShuangpinProfile(), + &GetZhinengAbcShuangpinProfile(), &GetGuobiaoShuangpinProfile(), &GetPinyinJiajiaShuangpinProfile()}) { ShuangpinScheme scheme(*profile); InputKey(scheme, 'Y', L'y'); @@ -238,10 +243,11 @@ TEST_CASE(MicrosoftProfileDecodesZeroInitialSyllables) TEST_CASE(ShuangpinProfilesDecodeKei) { const std::vector> cases{ - {&GetXiaoheShuangpinProfile(), "kw"}, - {&GetZiranmaShuangpinProfile(), "kz"}, - {&GetShoudaoShuangpinProfile(), "km"}, - {&GetMicrosoftShuangpinProfile(), "kz"}, + {&GetXiaoheShuangpinProfile(), "kw"}, {&GetZiranmaShuangpinProfile(), "kz"}, + {&GetShoudaoShuangpinProfile(), "km"}, {&GetMicrosoftShuangpinProfile(), "kz"}, + {&GetSogouShuangpinProfile(), "kz"}, {&GetZiguangShuangpinProfile(), "kk"}, + {&GetZhinengAbcShuangpinProfile(), "kq"}, {&GetGuobiaoShuangpinProfile(), "kb"}, + {&GetPinyinJiajiaShuangpinProfile(), "kw"}, }; for (const auto &[profile, input] : cases) @@ -274,8 +280,261 @@ TEST_CASE(MicrosoftSchemeAcceptsSemicolonAsIngFinalOnlyInSecondPosition) REQUIRE(!scheme.build_request().valid); } +TEST_CASE(SogouProfileDecodesKeyboardLayout) +{ + const auto &profile = GetSogouShuangpinProfile(); + const std::vector> cases{ + {"jq", "jiu"}, {"xw", "xia"}, {"gw", "gua"}, {"gr", "guan"}, {"xt", "xue"}, {"lt", "lve"}, + {"gy", "guai"}, {"ly", "lv"}, {"lo", "luo"}, {"dp", "dun"}, {"js", "jiong"}, {"ds", "dong"}, + {"xd", "xiang"}, {"gd", "guang"}, {"hf", "hen"}, {"dg", "deng"}, {"dh", "dang"}, {"dj", "dan"}, + {"dk", "dao"}, {"dl", "dai"}, {"m;", "ming"}, {"fz", "fei"}, {"dx", "die"}, {"dc", "diao"}, + {"dv", "dui"}, {"db", "dou"}, {"ln", "lin"}, {"lm", "lian"}, {"vh", "zhang"}, {"ih", "chang"}, + {"uh", "shang"}, + }; + + for (const auto &[input, expected] : cases) + { + REQUIRE_EQ(shuangpin::normalize_input(input, profile), expected); + } +} + +TEST_CASE(ZiguangProfileDecodesKeyboardLayout) +{ + const auto &profile = GetZiguangShuangpinProfile(); + const std::vector> cases{ + {"dq", "dao"}, {"hw", "hen"}, {"he", "he"}, {"dr", "dan"}, {"dt", "deng"}, {"ly", "lin"}, + {"gy", "guai"}, {"du", "du"}, {"li", "li"}, {"lo", "luo"}, {"dp", "dai"}, {"da", "da"}, + {"ds", "dang"}, {"dd", "die"}, {"df", "dian"}, {"xg", "xiang"}, {"gg", "guang"}, {"dh", "dong"}, + {"jh", "jiong"}, {"jj", "jiu"}, {"fk", "fei"}, {"gl", "guan"}, {"m;", "ming"}, {"dz", "dou"}, + {"xx", "xia"}, {"gx", "gua"}, {"lv", "lv"}, {"db", "diao"}, {"dn", "dui"}, {"jn", "jue"}, + {"ln", "lve"}, {"dm", "dun"}, {"us", "zhang"}, {"as", "chang"}, {"is", "shang"}, + }; + + for (const auto &[input, expected] : cases) + { + REQUIRE_EQ(shuangpin::normalize_input(input, profile), expected); + } +} + +TEST_CASE(ZhinengAbcProfileDecodesKeyboardLayout) +{ + const auto &profile = GetZhinengAbcShuangpinProfile(); + const std::vector> cases{ + {"fq", "fei"}, {"dw", "dian"}, {"he", "he"}, {"jr", "jiu"}, {"xt", "xiang"}, {"gt", "guang"}, + {"my", "ming"}, {"du", "du"}, {"li", "li"}, {"lo", "luo"}, {"gp", "guan"}, {"da", "da"}, + {"ds", "dong"}, {"js", "jiong"}, {"xd", "xia"}, {"gd", "gua"}, {"hf", "hen"}, {"dg", "deng"}, + {"dh", "dang"}, {"dj", "dan"}, {"dk", "dao"}, {"dl", "dai"}, {"dz", "diao"}, {"dx", "die"}, + {"lc", "lin"}, {"gc", "guai"}, {"lv", "lv"}, {"db", "dou"}, {"dn", "dun"}, {"jm", "jue"}, + {"lm", "lve"}, {"dm", "dui"}, {"ah", "zhang"}, {"eh", "chang"}, {"vh", "shang"}, {"ee", "che"}, + }; + + for (const auto &[input, expected] : cases) + { + REQUIRE_EQ(shuangpin::normalize_input(input, profile), expected); + } +} + +TEST_CASE(GuobiaoProfileDecodesKeyboardLayout) +{ + const auto &profile = GetGuobiaoShuangpinProfile(); + const std::vector> cases{ + {"xq", "xia"}, {"gq", "gua"}, {"gw", "guan"}, {"jw", "juan"}, {"he", "he"}, {"hr", "hen"}, + {"dt", "die"}, {"jy", "jiu"}, {"gy", "guai"}, {"du", "du"}, {"li", "li"}, {"lo", "luo"}, + {"dp", "dou"}, {"da", "da"}, {"ds", "dong"}, {"js", "jiong"}, {"dd", "dian"}, {"df", "dan"}, + {"dg", "dang"}, {"dh", "deng"}, {"mj", "ming"}, {"dk", "dai"}, {"ll", "lin"}, {"dz", "dun"}, + {"jz", "jun"}, {"xx", "xue"}, {"lx", "lve"}, {"dc", "dao"}, {"lv", "lv"}, {"dv", "dui"}, + {"fb", "fei"}, {"xn", "xiang"}, {"gn", "guang"}, {"dm", "diao"}, {"vg", "zhang"}, {"ig", "chang"}, + {"ug", "shang"}, + }; + + for (const auto &[input, expected] : cases) + { + REQUIRE_EQ(shuangpin::normalize_input(input, profile), expected); + } +} + +TEST_CASE(PinyinJiajiaProfileDecodesKeyboardLayout) +{ + const auto &profile = GetPinyinJiajiaShuangpinProfile(); + const std::vector> cases{ + {"mq", "ming"}, {"fw", "fei"}, {"he", "he"}, {"hr", "hen"}, {"dt", "deng"}, {"dy", "dong"}, + {"jy", "jiong"}, {"du", "du"}, {"li", "li"}, {"lo", "luo"}, {"dp", "dou"}, {"da", "da"}, + {"ds", "dai"}, {"dd", "dao"}, {"df", "dan"}, {"dg", "dang"}, {"xh", "xiang"}, {"gh", "guang"}, + {"dj", "dian"}, {"dk", "diao"}, {"ll", "lin"}, {"dz", "dun"}, {"xx", "xue"}, {"lx", "lve"}, + {"gx", "guai"}, {"gc", "guan"}, {"dv", "dui"}, {"lv", "lv"}, {"xb", "xia"}, {"gb", "gua"}, + {"jn", "jiu"}, {"dm", "die"}, {"vg", "zhang"}, {"ug", "chang"}, {"ig", "shang"}, + }; + + for (const auto &[input, expected] : cases) + { + REQUIRE_EQ(shuangpin::normalize_input(input, profile), expected); + } +} + +TEST_CASE(NewProfilesDecodeZeroInitialSyllables) +{ + // 搜狗、紫光、智能ABC:O + 韵母所在键;国标:A + 韵母所在键;拼音加加:韵母首字母 + 韵母所在键。 + const std::vector>>> cases{ + {&GetSogouShuangpinProfile(), + {{"oa", "a"}, + {"ol", "ai"}, + {"oj", "an"}, + {"oh", "ang"}, + {"ok", "ao"}, + {"oe", "e"}, + {"oz", "ei"}, + {"of", "en"}, + {"og", "eng"}, + {"or", "er"}, + {"oo", "o"}, + {"ob", "ou"}}}, + {&GetZiguangShuangpinProfile(), + {{"oa", "a"}, + {"op", "ai"}, + {"or", "an"}, + {"os", "ang"}, + {"oq", "ao"}, + {"oe", "e"}, + {"ok", "ei"}, + {"ow", "en"}, + {"ot", "eng"}, + {"oj", "er"}, + {"oo", "o"}, + {"oz", "ou"}}}, + {&GetZhinengAbcShuangpinProfile(), + {{"oa", "a"}, + {"ol", "ai"}, + {"oj", "an"}, + {"oh", "ang"}, + {"ok", "ao"}, + {"oe", "e"}, + {"oq", "ei"}, + {"of", "en"}, + {"og", "eng"}, + {"or", "er"}, + {"oo", "o"}, + {"ob", "ou"}}}, + {&GetGuobiaoShuangpinProfile(), + {{"aa", "a"}, + {"ak", "ai"}, + {"af", "an"}, + {"ag", "ang"}, + {"ac", "ao"}, + {"ae", "e"}, + {"ab", "ei"}, + {"ar", "en"}, + {"ah", "eng"}, + {"al", "er"}, + {"ao", "o"}, + {"ap", "ou"}}}, + {&GetPinyinJiajiaShuangpinProfile(), + {{"aa", "a"}, + {"as", "ai"}, + {"af", "an"}, + {"ag", "ang"}, + {"ad", "ao"}, + {"ee", "e"}, + {"ew", "ei"}, + {"er", "en"}, + {"et", "eng"}, + {"eq", "er"}, + {"oo", "o"}, + {"op", "ou"}}}, + }; + + for (const auto &[profile, syllables] : cases) + { + for (const auto &[input, expected] : syllables) + { + REQUIRE_EQ(shuangpin::normalize_input(input, *profile), expected); + } + } +} + +TEST_CASE(EveryProfileRoundTripsEveryEncodableSyllable) +{ + // Encoding each syllable with the profile's own tables and decoding it back catches two + // finals sharing a key where both would form a valid syllable with the same initial. + const std::vector excluded{"chua", "den", "fiao", "jve", "lo", "lue", "nou", + "nue", "nun", "qve", "xve", "yve", "zhei"}; + for (const auto *profile : + {&GetXiaoheShuangpinProfile(), &GetZiranmaShuangpinProfile(), &GetShoudaoShuangpinProfile(), + &GetMicrosoftShuangpinProfile(), &GetSogouShuangpinProfile(), &GetZiguangShuangpinProfile(), + &GetZhinengAbcShuangpinProfile(), &GetGuobiaoShuangpinProfile(), &GetPinyinJiajiaShuangpinProfile()}) + { + size_t checked = 0; + for (const auto &syllable : quanpin::intact_pinyin_set()) + { + // j/q/x/y spell ü as "u"; a "v" spelling of those is not a syllable the decoder produces. + if (std::find(excluded.begin(), excluded.end(), syllable) != excluded.end() || + (std::string("jqxy").find(syllable[0]) != std::string::npos && syllable.find('v') != std::string::npos)) + { + continue; + } + std::string code; + if (const auto zero = profile->zero_initials.find(syllable); zero != profile->zero_initials.end()) + { + code = zero->second; + } + else + { + const size_t initial_length = syllable.size() > 2 && syllable[1] == 'h' && + std::string("zcs").find(syllable[0]) != std::string::npos + ? 2 + : 1; + if (std::string("aeo").find(syllable[0]) != std::string::npos || syllable.size() <= initial_length) + { + continue; + } + const std::string initial = syllable.substr(0, initial_length); + const auto final = profile->finals.find(syllable.substr(initial_length)); + if (final == profile->finals.end()) + { + continue; + } + const auto initial_key = profile->initials.find(initial); + code = (initial_key == profile->initials.end() ? initial : initial_key->second) + final->second; + } + if (shuangpin::normalize_input(code, *profile) != syllable) + { + std::fprintf(stderr, "%s: %s -> %s decodes as %s\n", profile->name.c_str(), syllable.c_str(), + code.c_str(), shuangpin::normalize_input(code, *profile).c_str()); + } + REQUIRE_EQ(shuangpin::normalize_input(code, *profile), syllable); + ++checked; + } + REQUIRE(checked > 390); + } +} + +TEST_CASE(SemicolonFinalFollowsProfileLayout) +{ + REQUIRE(ShuangpinProfileUsesSemicolonFinal(GetMicrosoftShuangpinProfile())); + REQUIRE(ShuangpinProfileUsesSemicolonFinal(GetSogouShuangpinProfile())); + REQUIRE(ShuangpinProfileUsesSemicolonFinal(GetZiguangShuangpinProfile())); + REQUIRE(!ShuangpinProfileUsesSemicolonFinal(GetXiaoheShuangpinProfile())); + REQUIRE(!ShuangpinProfileUsesSemicolonFinal(GetZhinengAbcShuangpinProfile())); + REQUIRE(!ShuangpinProfileUsesSemicolonFinal(GetGuobiaoShuangpinProfile())); + REQUIRE(!ShuangpinProfileUsesSemicolonFinal(GetPinyinJiajiaShuangpinProfile())); + + for (const auto *profile : {&GetSogouShuangpinProfile(), &GetZiguangShuangpinProfile()}) + { + ShuangpinScheme scheme(*profile); + InputKey(scheme, 'X', L'x'); + InputKey(scheme, VK_OEM_1, L';'); + const auto request = scheme.build_request(); + REQUIRE_EQ(request.raw_input, std::string("x;")); + REQUIRE_EQ(request.normalized_segmentation, std::string("xing")); + } +} + TEST_CASE(ShuangpinProfileResolverSelectsNamedProfileAndFallsBackToXiaohe) { + for (const char *name : + {"xiaohe", "ziranma", "shoudao", "microsoft", "sogou", "ziguang", "zhinengabc", "guobiao", "pinyinjiajia"}) + { + REQUIRE_EQ(GetShuangpinProfile(name).name, std::string(name)); + } REQUIRE_EQ(GetShuangpinProfile("ziranma").name, std::string("ziranma")); REQUIRE_EQ(GetShuangpinProfile("shoudao").name, std::string("shoudao")); REQUIRE_EQ(GetShuangpinProfile("microsoft").name, std::string("microsoft")); diff --git a/ui-html/webview2/settings/ime-settings/src/partials/input.html b/ui-html/webview2/settings/ime-settings/src/partials/input.html index cbd8b459..3a7db8b1 100644 --- a/ui-html/webview2/settings/ime-settings/src/partials/input.html +++ b/ui-html/webview2/settings/ime-settings/src/partials/input.html @@ -63,6 +63,11 @@ + + + + +