Files
msime-windows/engine/user_dictionary/personal_dictionary.cpp
T
fanlusky a7779d2b05 refactor(engine): 将 engine 从 submodule 内嵌为仓内一等组件
`vendor/MetasequoiaImeEngine` submodule 改为 `engine/` 目录,导入的是
product-lock.json 原本钉住的提交 c810d201,逐文件核对过 blob,因此这是一次
搬迁而不是引擎升级。引擎自己的嵌套 submodule(googlepinyinime-rev、utfcpp、
miniaudio)按各自 gitlink 一并展开,来源与提交见 engine/UPSTREAM.md。

Windows 专门化裁掉了不属于本产品的部分:whisper.cpp 与本地推理 provider、
macOS 示例宿主、整个 dictionary/ 建库流水线、上游 CI 与 release-please 配置。
词库仍按 product-lock.json 在构建时取发布产物。

引擎不再进 product-lock.json:没有 gitlink 就没有可比对的东西,锁里写一个上游
commit 等于宣称这棵树仍等于上游,而第一个 Windows 专门化改动落地时这句话就假了。
verify-contracts / verify-checkout 随之移除,verify-published 保留。

CI 与工具链:引擎源码并入根 scripts/format.sh(删掉引擎自己的那份,避免两套排除
列表分叉);引擎 voice.yml 里的 silero_vad.onnx 摘要校验移到本仓 lock job;
engine-bump-triage 工作流与其测试删除;gitsubmodule 的 dependabot 频率由 daily
改为 monthly。

README、README.en、AGENTS.md、CONTRIBUTING、docs/product-release.md 及各子目录
文档同步说明新的位置与边界。
2026-09-12 03:40:48 +08:00

71 lines
3.3 KiB
C++

#include <metasequoia/personal_dictionary.h>
#include "../quanpin/quanpin_utils.h"
#include <utf8.h>
#include <algorithm>
namespace metasequoia
{
PersonalDictionaryValidation validate_personal_dictionary_entry(PersonalDictionaryEntry entry)
{
const auto invalid = [](const char *message) { return PersonalDictionaryValidation{std::nullopt, message}; };
if (entry.weight < 1 || entry.weight > 100000000)
return invalid("Weight must be between 1 and 100000000");
if (entry.key.empty() || entry.key.size() > 512 || entry.value.empty() || entry.value.size() > 4096 ||
!utf8::is_valid(entry.value.begin(), entry.value.end()))
return invalid("A valid, bounded UTF-8 word and input code are required");
const bool quick = entry.kind == PersonalDictionaryKind::QuickPhrase;
for (unsigned char ch : entry.value)
if ((ch < 32 && !(quick && (ch == '\n' || ch == '\t'))) || ch == 127)
return invalid("The word contains an unsupported control character");
for (char &ch : entry.key)
if (ch >= 'A' && ch <= 'Z')
ch = static_cast<char>(ch - 'A' + 'a');
auto letters = [](unsigned char ch) { return ch >= 'a' && ch <= 'z'; };
switch (entry.kind)
{
case PersonalDictionaryKind::Pinyin: {
// Require explicit syllables; guessing a split here could store a different pronunciation.
std::replace(entry.key.begin(), entry.key.end(), ' ', '\'');
std::size_t start = 0, count = 0;
while (start <= entry.key.size())
{
const auto end = entry.key.find('\'', start);
const auto syllable = entry.key.substr(start, end == std::string::npos ? end : end - start);
if (!quanpin::intact_pinyin_set().count(syllable))
return invalid("Use complete pinyin syllables separated by apostrophes or spaces");
++count;
if (end == std::string::npos)
break;
start = end + 1;
}
if (count > 64 || count != static_cast<std::size_t>(utf8::distance(entry.value.begin(), entry.value.end())))
return invalid("Each character must have one pinyin syllable (maximum 64)");
break;
}
case PersonalDictionaryKind::Wubi:
if (entry.key.size() > 4 || !std::all_of(entry.key.begin(), entry.key.end(), letters))
return invalid("Wubi codes contain one to four letters");
break;
case PersonalDictionaryKind::QuickPhrase:
if (entry.key.size() > 32 || !std::all_of(entry.key.begin(), entry.key.end(), [&](unsigned char ch) {
return letters(ch) || (ch >= '0' && ch <= '9');
}))
return invalid("Quick-phrase codes contain one to 32 letters or digits");
break;
case PersonalDictionaryKind::English: {
std::string normalized = entry.value;
for (char &ch : normalized)
if (ch >= 'A' && ch <= 'Z')
ch = static_cast<char>(ch - 'A' + 'a');
if (entry.key.size() > 64 || !std::all_of(entry.key.begin(), entry.key.end(), letters) ||
normalized != entry.key)
return invalid("English code must match the word's letters, ignoring case");
break;
}
default:
return invalid("Unsupported dictionary kind");
}
return {std::move(entry), {}};
}
} // namespace metasequoia