Merge pull request #526 from fanlusky/feat/builtin-shuangpin-schemes

feat(engine): 内置搜狗、紫光、智能ABC、国标和拼音加加双拼方案
This commit is contained in:
Lu Fan(SonnyCalcr) authored and GitHub committed 2026-09-29 03:53:53 +08:00
commit f51d7fbfd4
18 files changed
+542 -25

No files matched your search

+1 -1
View File
@@ -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
+2 -2
View File
@@ -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`;`↑` / `↓` 用于移动高亮候选项。
+2 -1
View File
@@ -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".
+4 -4
View File
@@ -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<ImeKeyCode>(std::toupper(unsigned_character)));
: (semicolon_final ? ImeKey::Semicolon : static_cast<ImeKeyCode>(std::toupper(unsigned_character)));
engine_.handle_key(key_code, 0, static_cast<ImeCharacter>(unsigned_character));
update_mixed_candidates();
const bool handled = preedit() != previous_preedit;
+2 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
}
+1 -1
View File
@@ -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 += ';';
+233
View File
@@ -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();
+9
View File
@@ -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);
+1 -1
View File
@@ -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 版待词库发布后另开任务接线
+1 -1
View File
@@ -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 版待词库发布后另开任务接线
+2
View File
@@ -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);
+8 -1
View File
@@ -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;
}
+3 -3
View File
@@ -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 = ';';
+1 -1
View File
@@ -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");
+1 -1
View File
@@ -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()
+265 -6
View File
@@ -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 <algorithm>
#include <cstdio>
#include <utility>
#include <vector>
@@ -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<std::pair<const ShuangpinProfile *, std::string>> 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<std::pair<std::string, std::string>> 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<std::pair<std::string, std::string>> 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<std::pair<std::string, std::string>> 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<std::pair<std::string, std::string>> 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<std::pair<std::string, std::string>> 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<std::pair<const ShuangpinProfile *, std::vector<std::pair<std::string, std::string>>>> 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<std::string> 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"));
@@ -63,6 +63,11 @@
<div class="dropdown-item" data-value="ziranma">自然码双拼</div>
<div class="dropdown-item" data-value="shoudao">首道双拼</div>
<div class="dropdown-item" data-value="microsoft">微软双拼</div>
<div class="dropdown-item" data-value="sogou">搜狗双拼</div>
<div class="dropdown-item" data-value="ziguang">紫光双拼</div>
<div class="dropdown-item" data-value="zhinengabc">智能ABC</div>
<div class="dropdown-item" data-value="guobiao">国标双拼</div>
<div class="dropdown-item" data-value="pinyinjiajia">拼音加加</div>
</div>
</div>
</div>