符号数据改用 rime-wanxiang 的符号表与 typst/codex 符号名,源数据放在 msime-dictionary 的 sources/symbols/,替换原来的 piliapp 符号表。 - 建库:新增 symbols_db.py,生成表情面板用的 symbol_catalog(中文分类)和 S 模式、符号混输用的 symbol_code;剔除 wanxiang-unrenderable.txt 列出的 Windows 自带字体显示不了的符号,以及与 ① ❶ 看不出区别的 Dingbats 带圈数字 - 新增 scripts/check-symbol-rendering.py,用 Windows 字体生成上面的剔除清单 - Shift+S 进入符号模式,按万象分组码(xl、sx、szq)、数字读音(yi → 一 ① ⑴) 或符号名(alpha、infinity)检索 - 符号混输:整串精确等于编码时混入,首个紧跟首选;读得成拼音的编码只混数字 序号写法;双拼先按方案换算成全拼再查(小鹤 sj → san → ③) - 表情面板符号页:中文分类,搜索把整词命中的符号列在「最佳匹配」 - 配置:utility.symbol_mode(默认开)、general.symbol_mixed_input(默认关) Refs #795
191 lines
6.8 KiB
C++
191 lines
6.8 KiB
C++
#include "../contracts/assets/assets.h"
|
|
#include "quick_phrase_query.h"
|
|
#include "local_database.h"
|
|
|
|
#include "../core/data_path.h"
|
|
|
|
#include <sqlite3.h>
|
|
|
|
#include <algorithm>
|
|
#include <iterator>
|
|
#include <memory>
|
|
|
|
namespace metasequoia::local_modes
|
|
{
|
|
namespace
|
|
{
|
|
struct StatementCloser
|
|
{
|
|
void operator()(sqlite3_stmt *statement) const
|
|
{
|
|
if (statement != nullptr)
|
|
{
|
|
sqlite3_finalize(statement);
|
|
}
|
|
}
|
|
};
|
|
|
|
using Statement = std::unique_ptr<sqlite3_stmt, StatementCloser>;
|
|
|
|
bool valid_code(const std::string &code)
|
|
{
|
|
return !code.empty() && std::all_of(code.begin(), code.end(),
|
|
[](unsigned char character) { return character >= 'a' && character <= 'z'; });
|
|
}
|
|
|
|
QuickPhraseQueryResult query_failure(const char *diagnostic)
|
|
{
|
|
return {{}, std::string(diagnostic)};
|
|
}
|
|
// 前缀查询给 K 模式用,精确查询给全拼/双拼混排用;上界拼 0x7f 让 key 落在 [code, code+0x7f)。
|
|
QuickPhraseQueryResult query_quick_phrase_rows(const std::string &code, const std::filesystem::path &database_path,
|
|
int limit, bool exact)
|
|
{
|
|
if (!valid_code(code) || limit <= 0)
|
|
{
|
|
return {};
|
|
}
|
|
if (database_path.empty())
|
|
{
|
|
return query_failure("Quick phrase database is unavailable.");
|
|
}
|
|
|
|
const std::shared_ptr<sqlite3> database = open_local_database(database_path);
|
|
if (!database)
|
|
{
|
|
return query_failure("Quick phrase database is unavailable.");
|
|
}
|
|
|
|
constexpr const char *kPrefixSql = "SELECT key,value,weight FROM quick_parases WHERE key>=?1 AND key<?2 "
|
|
"ORDER BY weight DESC,key,value LIMIT ?3";
|
|
constexpr const char *kExactSql = "SELECT key,value,weight FROM quick_parases WHERE key=?1 AND key=?2 "
|
|
"ORDER BY weight DESC,value LIMIT ?3";
|
|
sqlite3_stmt *raw_statement = nullptr;
|
|
if (sqlite3_prepare_v2(database.get(), exact ? kExactSql : kPrefixSql, -1, &raw_statement, nullptr) != SQLITE_OK)
|
|
{
|
|
return query_failure("Quick phrase database could not be queried.");
|
|
}
|
|
Statement statement(raw_statement);
|
|
std::string upper_bound = code;
|
|
if (!exact)
|
|
{
|
|
upper_bound.push_back(static_cast<char>(0x7f));
|
|
}
|
|
if (sqlite3_bind_text(statement.get(), 1, code.c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK ||
|
|
sqlite3_bind_text(statement.get(), 2, upper_bound.c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK ||
|
|
sqlite3_bind_int(statement.get(), 3, limit) != SQLITE_OK)
|
|
{
|
|
return query_failure("Quick phrase database could not be queried.");
|
|
}
|
|
|
|
QuickPhraseQueryResult result;
|
|
int step_result = SQLITE_ROW;
|
|
while ((step_result = sqlite3_step(statement.get())) == SQLITE_ROW)
|
|
{
|
|
const auto *key = reinterpret_cast<const char *>(sqlite3_column_text(statement.get(), 0));
|
|
const auto *value = reinterpret_cast<const char *>(sqlite3_column_text(statement.get(), 1));
|
|
if (key != nullptr && value != nullptr)
|
|
{
|
|
result.candidates.emplace_back(key, value, sqlite3_column_int64(statement.get(), 2),
|
|
CandidateSource::QuickPhrase);
|
|
}
|
|
}
|
|
if (step_result != SQLITE_DONE)
|
|
{
|
|
return query_failure("Quick phrase database could not be queried.");
|
|
}
|
|
return result;
|
|
}
|
|
} // namespace
|
|
|
|
QuickPhraseQueryResult query_quick_phrases(const std::string &prefix, int limit)
|
|
{
|
|
return query_quick_phrases(prefix, data_file_path(metasequoia::assets::main_dictionary), limit);
|
|
}
|
|
|
|
QuickPhraseQueryResult query_quick_phrases(const std::string &prefix, const std::filesystem::path &database_path,
|
|
int limit)
|
|
{
|
|
return query_quick_phrase_rows(prefix, database_path, limit, false);
|
|
}
|
|
|
|
QuickPhraseQueryResult query_quick_phrases_by_code(const std::string &code, int limit)
|
|
{
|
|
return query_quick_phrases_by_code(code, data_file_path(metasequoia::assets::main_dictionary), limit);
|
|
}
|
|
|
|
QuickPhraseQueryResult query_quick_phrases_by_code(const std::string &code, const std::filesystem::path &database_path,
|
|
int limit)
|
|
{
|
|
return query_quick_phrase_rows(code, database_path, limit, true);
|
|
}
|
|
|
|
bool counts_toward_quick_phrase_slot(const WordItem &item)
|
|
{
|
|
if (item.fixed_position > 0)
|
|
{
|
|
return false;
|
|
}
|
|
switch (item.source)
|
|
{
|
|
case CandidateSource::QuickPhrase:
|
|
case CandidateSource::CloudSuggestion:
|
|
case CandidateSource::AiSuggestion:
|
|
case CandidateSource::EnglishDictionary:
|
|
case CandidateSource::Emoji:
|
|
case CandidateSource::Kaomoji:
|
|
case CandidateSource::Symbol:
|
|
case CandidateSource::DateTime:
|
|
return false;
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
void place_quick_phrases(std::vector<WordItem> &candidates, std::vector<WordItem> phrases, std::size_t slot)
|
|
{
|
|
candidates.erase(std::remove_if(candidates.begin(), candidates.end(),
|
|
[&](const WordItem &item) {
|
|
return item.source == CandidateSource::QuickPhrase ||
|
|
std::any_of(phrases.begin(), phrases.end(), [&](const WordItem &phrase) {
|
|
return phrase.word == item.word;
|
|
});
|
|
}),
|
|
candidates.end());
|
|
if (phrases.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
std::vector<WordItem> fixed;
|
|
candidates.erase(std::remove_if(candidates.begin(), candidates.end(),
|
|
[&](WordItem &item) {
|
|
if (item.fixed_position <= 0)
|
|
return false;
|
|
fixed.push_back(std::move(item));
|
|
return true;
|
|
}),
|
|
candidates.end());
|
|
|
|
std::size_t index = 0;
|
|
for (std::size_t counted = 0; index < candidates.size() && counted < slot; ++index)
|
|
{
|
|
if (counts_toward_quick_phrase_slot(candidates[index]))
|
|
{
|
|
++counted;
|
|
}
|
|
}
|
|
candidates.insert(candidates.begin() + static_cast<std::ptrdiff_t>(index), std::make_move_iterator(phrases.begin()),
|
|
std::make_move_iterator(phrases.end()));
|
|
|
|
std::stable_sort(fixed.begin(), fixed.end(), [](const WordItem &left, const WordItem &right) {
|
|
return left.fixed_position < right.fixed_position;
|
|
});
|
|
for (auto &item : fixed)
|
|
{
|
|
const std::size_t position = (std::min)(static_cast<std::size_t>(item.fixed_position - 1), candidates.size());
|
|
candidates.insert(candidates.begin() + static_cast<std::ptrdiff_t>(position), std::move(item));
|
|
}
|
|
}
|
|
} // namespace metasequoia::local_modes
|