* feat(engine,server,windows,ui-html,installer): U 模式重构为部件拆字反查 (#971) 把 U 模式从 Unicode 十六进制码位输入改成部件拆字反查:Shift+U 后输入各 组成部分的拼音连写(晶 = ririri)即可出字,候选上反显注音。 - 引擎:新增 local_modes/radical_query(前缀匹配、字频降序),数据表 engine/radical/decomposition.txt 与生成脚本;LocalInputMode::Unicode 改为 Radical;删除 unicode_query。 - Server:g_radical_mode_triggered + IsRadicalCompositionActive + 候选分发, 用 cpp-pinyin 反显注音;删除 Unicode 码位路径。 - 前端 TSF:删除 U 模式的十六进制/数字特例与影子预判,按普通组合处理。 - 配置:utility.unicode_mode 改为 utility.radical_mode。 - 部署:assets 登记 radical_decomposition,安装器与 CI 拷表。 数据来源 mirtlecn/rime-radical-pinyin (GPL-3.0),授权见 engine/radical/NOTICE.md。 * feat(server): U 模式反显拼音改为带调风格(jīng) * fix(server,installer): 修复 U 模式 CI(拆字表打包夹具与 clang-format) * fix(engine,server): 修正 U 模式按键错位、重复候选与配置迁移 - Server 在 U 模式不再吞掉撇号、分号、斜杠、大写字母等键:TSF 已把它们记进按键缓冲, 吞掉又不重同步会让两侧串和光标错位。照常插入,含非 a-z 的码查不出候选 - 拆字查询全码命中排在仅前缀命中之前,同一个字的几种拆法只出一次 - 升级合并把旧键 utility.unicode_mode 的值带到 utility.radical_mode,读取时也兜底旧键 - 生成脚本只收汉字,重建拆字表去掉 abc 码下的 51 行数字、字母与符号 - 开着 U 模式时在后台预热拆字表与 cpp-pinyin 词典,避开首个按键的冷加载 - Server 按会话的 RuntimePaths 查拆字表,删掉走全局数据目录的便捷重载 - cpp-pinyin 初始化合并到 utils/hanzi_pinyin,设置页导入注音共用;U 模式与快捷短语的收键分支合并 --------- Co-authored-by: fanlusky <1730976608@qq.com>
209 lines
8.7 KiB
C++
209 lines
8.7 KiB
C++
#include "candidate_queries.h"
|
|
#include "data_path.h"
|
|
#include "../contracts/assets/assets.h"
|
|
#include "../local_modes/emoji_query.h"
|
|
#include "../local_modes/jianpin_query.h"
|
|
#include "../local_modes/kaomoji_query.h"
|
|
#include "../local_modes/quick_phrase_query.h"
|
|
#include "../local_modes/radical_query.h"
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <iterator>
|
|
#include <unordered_set>
|
|
namespace metasequoia
|
|
{
|
|
local_modes::LocalQueryResult CandidateQueries::local(LocalInputMode mode, const std::string &preedit,
|
|
SchemeType scheme,
|
|
const std::function<local_modes::LocalDateTime()> &clock,
|
|
const std::vector<WordItem> &engine_candidates)
|
|
{
|
|
local_modes::LocalQueryResult result;
|
|
|
|
switch (mode)
|
|
{
|
|
case LocalInputMode::Radical:
|
|
// 拆字表是会话级资源:按会话的 RuntimePaths 解析,和 other_dictionary、translations 一致。
|
|
result.candidates =
|
|
local_modes::query_radical(paths_.resource(assets::radical_decomposition), preedit.substr(1));
|
|
return result;
|
|
case LocalInputMode::DateTime: {
|
|
const local_modes::LocalDateTime now = clock ? clock() : local_modes::current_local_date_time();
|
|
result.candidates = local_modes::query_date_time(preedit.substr(1), &now);
|
|
return result;
|
|
}
|
|
case LocalInputMode::QuickPhrase: {
|
|
local_modes::QuickPhraseQueryResult query =
|
|
local_modes::query_quick_phrases(preedit.substr(1), paths_.dictionary(assets::main_dictionary));
|
|
result.candidates = std::move(query.candidates);
|
|
result.diagnostic = std::move(query.diagnostic);
|
|
return result;
|
|
}
|
|
case LocalInputMode::Emoji: {
|
|
local_modes::LocalQueryResult query = local_modes::query_emoji(
|
|
preedit.substr(1), scheme, paths_.resource(assets::other_dictionary), 10, shuangpin_profile_);
|
|
result.candidates = std::move(query.candidates);
|
|
result.diagnostic = std::move(query.diagnostic);
|
|
return result;
|
|
}
|
|
case LocalInputMode::Kaomoji: {
|
|
local_modes::LocalQueryResult query = local_modes::query_kaomoji(
|
|
preedit.substr(1), scheme, paths_.resource(assets::other_dictionary), 10, shuangpin_profile_);
|
|
result.candidates = std::move(query.candidates);
|
|
result.diagnostic = std::move(query.diagnostic);
|
|
return result;
|
|
}
|
|
case LocalInputMode::SuperJianpin: {
|
|
const std::string code = preedit.substr(1);
|
|
const int limit = code.size() == 1 ? 24 : 100;
|
|
local_modes::LocalQueryResult query = local_modes::query_jianpin(
|
|
code, scheme, paths_.dictionary(assets::main_dictionary), limit, shuangpin_profile_);
|
|
result.candidates = std::move(query.candidates);
|
|
result.diagnostic = std::move(query.diagnostic);
|
|
return result;
|
|
}
|
|
case LocalInputMode::TemporaryEnglish: {
|
|
const std::string raw = preedit.substr(1);
|
|
result.candidates.clear();
|
|
if (raw.empty())
|
|
{
|
|
return result;
|
|
}
|
|
result.candidates.emplace_back("", raw, 0, CandidateSource::Generated);
|
|
std::string prefix = raw;
|
|
std::transform(prefix.begin(), prefix.end(), prefix.begin(),
|
|
[](unsigned char character) { return static_cast<char>(std::tolower(character)); });
|
|
auto completions = english_dictionary().query_prefix(prefix, 1000);
|
|
completions.erase(std::remove_if(completions.begin(), completions.end(),
|
|
[&](const WordItem &candidate) {
|
|
if (candidate.word.size() != raw.size())
|
|
{
|
|
return false;
|
|
}
|
|
return std::equal(candidate.word.begin(), candidate.word.end(),
|
|
raw.begin(),
|
|
[](unsigned char left, unsigned char right) {
|
|
return std::tolower(left) == std::tolower(right);
|
|
});
|
|
}),
|
|
completions.end());
|
|
result.candidates.insert(result.candidates.end(), std::make_move_iterator(completions.begin()),
|
|
std::make_move_iterator(completions.end()));
|
|
return result;
|
|
}
|
|
case LocalInputMode::TemporaryJapanese:
|
|
result.candidates = engine_candidates;
|
|
return result;
|
|
case LocalInputMode::None:
|
|
result.candidates.clear();
|
|
return result;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::vector<WordItem> CandidateQueries::mixed(std::vector<WordItem> candidates, const std::string &prefix,
|
|
SchemeType scheme, EnglishInputOptions english_options,
|
|
MixedExpressiveOptions expressive_options, bool dedicated_english,
|
|
LocalInputMode local_mode)
|
|
{
|
|
|
|
if ((!english_options.mixed_candidates && !expressive_options.emoji_candidates &&
|
|
!expressive_options.kaomoji_candidates) ||
|
|
dedicated_english || local_mode != LocalInputMode::None ||
|
|
(scheme != SchemeType::Quanpin && scheme != SchemeType::Shuangpin))
|
|
{
|
|
return candidates;
|
|
}
|
|
|
|
if (prefix.empty())
|
|
{
|
|
return candidates;
|
|
}
|
|
|
|
std::unordered_set<std::string> seen;
|
|
for (const auto &candidate : candidates)
|
|
{
|
|
seen.insert(candidate.word);
|
|
}
|
|
|
|
const auto collect_unique = [&](std::vector<WordItem> candidates) {
|
|
std::vector<WordItem> unique;
|
|
unique.reserve(candidates.size());
|
|
for (auto &candidate : candidates)
|
|
{
|
|
if (seen.insert(candidate.word).second)
|
|
{
|
|
unique.push_back(std::move(candidate));
|
|
}
|
|
}
|
|
return unique;
|
|
};
|
|
|
|
std::vector<WordItem> english_candidates;
|
|
const bool lower_ascii_prefix = std::all_of(
|
|
prefix.begin(), prefix.end(), [](unsigned char character) { return character >= 'a' && character <= 'z'; });
|
|
if (english_options.mixed_candidates && prefix.size() >= english_options.minimum_prefix && lower_ascii_prefix)
|
|
{
|
|
english_candidates = collect_unique(english_dictionary().query_prefix(prefix, 5));
|
|
}
|
|
|
|
std::vector<WordItem> emoji_candidates;
|
|
if (expressive_options.emoji_candidates && prefix.size() >= 2)
|
|
{
|
|
emoji_candidates = collect_unique(
|
|
local_modes::query_emoji(prefix, scheme, paths_.resource(assets::other_dictionary), 3, shuangpin_profile_)
|
|
.candidates);
|
|
}
|
|
|
|
std::vector<WordItem> kaomoji_candidates;
|
|
if (expressive_options.kaomoji_candidates && prefix.size() >= 2)
|
|
{
|
|
kaomoji_candidates = collect_unique(
|
|
local_modes::query_kaomoji(prefix, scheme, paths_.resource(assets::other_dictionary), 3, shuangpin_profile_)
|
|
.candidates);
|
|
}
|
|
|
|
std::size_t priority_slot = std::min<std::size_t>(1, candidates.size());
|
|
const auto has_source = [&](CandidateSource source) {
|
|
return std::any_of(candidates.begin(), candidates.end(),
|
|
[source](const WordItem &candidate) { return candidate.source == source; });
|
|
};
|
|
if (has_source(CandidateSource::CloudSuggestion))
|
|
{
|
|
priority_slot = std::min<std::size_t>(2, candidates.size());
|
|
}
|
|
if (has_source(CandidateSource::AiSuggestion))
|
|
{
|
|
priority_slot = std::min<std::size_t>(3, candidates.size());
|
|
}
|
|
const auto insert_leading = [&](std::vector<WordItem> &source) {
|
|
if (source.empty())
|
|
{
|
|
return;
|
|
}
|
|
candidates.insert(candidates.begin() + static_cast<std::ptrdiff_t>(priority_slot), std::move(source.front()));
|
|
source.erase(source.begin());
|
|
++priority_slot;
|
|
};
|
|
insert_leading(english_candidates);
|
|
|
|
for (auto *source : {&english_candidates, &emoji_candidates, &kaomoji_candidates})
|
|
{
|
|
candidates.insert(candidates.end(), std::make_move_iterator(source->begin()),
|
|
std::make_move_iterator(source->end()));
|
|
}
|
|
return candidates;
|
|
}
|
|
|
|
EnglishDictionary &CandidateQueries::english_dictionary()
|
|
{
|
|
if (!english_dictionary_)
|
|
{
|
|
english_dictionary_ =
|
|
std::make_unique<EnglishDictionary>(path_to_utf8(paths_.dictionary(assets::english_dictionary)), false,
|
|
path_to_utf8(paths_.resource(assets::translations)));
|
|
}
|
|
return *english_dictionary_;
|
|
}
|
|
|
|
} // namespace metasequoia
|