Merge pull request #568 from fanlusky/fix/quanpin-alias-preedit-followup

fix(engine): 全拼别名改写后的 preedit 保留合法切分与手动分隔符
This commit is contained in:
Lu Fan(SonnyCalcr) authored and GitHub committed 2026-10-02 00:43:37 +08:00
commit 7834fefbb3
2 files changed
+84 -20

No files matched your search

+65 -16
View File
@@ -220,16 +220,16 @@ std::string CutSyllableLetters(const quanpin::AutocorrectCut &cut)
return FoldQuanpinAutocorrectLetters(letters);
}
// Rebuilds the preedit from the cased input letters with separators at the raw
// spans the correction BFS reports. Letters (case included) are preserved
// verbatim; manual delimiters are replaced by the actual cut positions.
std::string RebuildQuanpinDisplayFromCut(const std::string &cased_input, const quanpin::AutocorrectCut &cut)
// Rebuilds the preedit from the cased input letters with a separator after
// each listed letter count (ascending; the final segment end is excluded).
// Letters (case included) are preserved verbatim; manual delimiters are
// replaced by the given boundaries.
std::string RebuildQuanpinDisplayAtBoundaries(const std::string &cased_input, const std::vector<size_t> &boundaries)
{
std::string display;
display.reserve(cased_input.size() + cut.segments.size());
display.reserve(cased_input.size() + boundaries.size());
size_t letter_index = 0;
size_t boundary_index = 0;
const size_t boundary_count = cut.segments.empty() ? 0 : cut.segments.size() - 1;
for (const char ch : cased_input)
{
if (ch == '\'')
@@ -238,8 +238,7 @@ std::string RebuildQuanpinDisplayFromCut(const std::string &cased_input, const q
}
display.push_back(ch);
++letter_index;
if (boundary_index < boundary_count &&
letter_index == cut.segments[boundary_index].start + cut.segments[boundary_index].raw_text.size())
if (boundary_index < boundaries.size() && letter_index == boundaries[boundary_index])
{
display.push_back('\'');
++boundary_index;
@@ -248,13 +247,55 @@ std::string RebuildQuanpinDisplayFromCut(const std::string &cased_input, const q
return display;
}
// Separators at the raw spans the correction BFS reports.
std::string RebuildQuanpinDisplayFromCut(const std::string &cased_input, const quanpin::AutocorrectCut &cut)
{
std::vector<size_t> boundaries;
for (size_t i = 0; i + 1 < cut.segments.size(); ++i)
{
boundaries.push_back(cut.segments[i].start + cut.segments[i].raw_text.size());
}
return RebuildQuanpinDisplayAtBoundaries(cased_input, boundaries);
}
// Separators for a spelling that passed the jianpin guard, read the way the
// guard reads it: complete syllables plus at most one trailing letter
// ("dongan" -> dong'an, "haoyongg" -> hao'yong'g). Only the typed letters are
// cut, so the result is independent of any alias reading that won the query.
// Returns an empty string when no such reading exists.
std::string RebuildQuanpinDisplayFromLegalSpelling(const std::string &cased_input, const std::string &raw_input)
{
auto segments = quanpin::cut_one_piece_min_segments(raw_input, true);
if (segments.empty() && raw_input.size() > 1)
{
segments = quanpin::cut_one_piece_min_segments(raw_input.substr(0, raw_input.size() - 1), true);
if (!segments.empty())
{
segments.push_back(raw_input.substr(raw_input.size() - 1));
}
}
if (segments.empty())
{
return {};
}
std::vector<size_t> boundaries;
size_t end = 0;
for (size_t i = 0; i + 1 < segments.size(); ++i)
{
end += segments[i].size();
boundaries.push_back(end);
}
return RebuildQuanpinDisplayAtBoundaries(cased_input, boundaries);
}
// The preedit must always show the letters the user actually typed (PRD R5).
// Two layers can rewrite them into canonical pinyin: the scheme alias table
// (sahng -> shang, baked into raw_segmentation) and the dictionary correction
// BFS (shabg -> shang, which only re-separates). Both are rebuilt here from
// the raw letters with separators at the actual cut positions; when the BFS
// cannot explain a rewrite (length-changing aliases such as mihng -> ming) the
// raw letters are shown without separators. The rebuild is deliberately
// input is shown as typed, with no scheme separators. The rebuild is deliberately
// switch-independent: the alias layer rewrites letters regardless of the
// autocorrect switches, and AC1 only constrains the candidate list.
std::string BuildQuanpinAutocorrectDisplay(const QueryRequest &request)
@@ -286,12 +327,19 @@ std::string BuildQuanpinAutocorrectDisplay(const QueryRequest &request)
if (quanpin::looks_like_syllable_with_jianpin_tail(request.raw_input))
{
// The typed spelling is legal, but the scheme alias table may still
// have re-segmented it into a reading with copied letters (dongua ->
// dong'gua, haoyonga -> hao'yong'ga). The dictionary ranks those
// have re-segmented it into a reading with copied letters (dongan ->
// dong'gan, haoyonga -> hao'yong'ga). The dictionary ranks those
// readings against the exact one by frequency, yet the preedit must
// show the typed letters (PRD R5), so a rewritten base may never
// reach it verbatim.
return letters_rewritten ? QuanpinLettersWithoutDelimiters(cased) : base;
// reach it verbatim. Separate the typed letters by their own legal
// reading instead, so one more letter does not drop every separator
// (hao'yong -> hao'yong'a, not haoyonga).
if (!letters_rewritten)
{
return base;
}
const std::string display = RebuildQuanpinDisplayFromLegalSpelling(cased, request.raw_input);
return display.empty() ? cased : display;
}
const auto cut = quanpin::autocorrect_cut_detail(folded_input, types);
@@ -307,9 +355,10 @@ std::string BuildQuanpinAutocorrectDisplay(const QueryRequest &request)
return RebuildQuanpinDisplayFromCut(cased, cut);
}
// The BFS cannot explain the input (e.g. a length-changing alias such as
// mihng -> ming): fall back to the plain raw letters when the letters were
// rewritten, otherwise keep the scheme segmentation untouched.
return letters_rewritten ? QuanpinLettersWithoutDelimiters(cased) : base;
// mihng -> ming): fall back to the input exactly as typed when the letters
// were rewritten, otherwise keep the scheme segmentation untouched. Manual
// delimiters are part of what was typed, so they stay ("mihng'").
return letters_rewritten ? cased : base;
}
} // namespace
+19 -4
View File
@@ -827,14 +827,16 @@ int run_test()
// PRD R5 regression: a fully legal spelling whose alias reading won the
// query (haoyonga -> hao'yong'ga) must still preedit the typed letters.
// The rewritten segmentation used to leak into the preedit verbatim,
// adding a phantom letter and shifting the TSF caret (haoyongg|a).
struct LegalSpellingCase
// adding a phantom letter and shifting the TSF caret (haoyongg|a). The
// typed letters keep separators from their own legal reading, so one
// more letter does not drop the separators already shown.
struct PreeditCase
{
const char *pinyin;
const char *expected_preedit;
};
const std::array<LegalSpellingCase, 4> legal_spelling_preedit_cases = {
{{"haoyonga", "haoyonga"}, {"dongua", "dongua"}, {"zhonguo", "zhonguo"}, {"dongan", "dongan"}}};
const std::array<PreeditCase, 3> legal_spelling_preedit_cases = {
{{"haoyong", "hao'yong"}, {"haoyonga", "hao'yong'a"}, {"dongan", "dong'an"}}};
for (const auto &test_case : legal_spelling_preedit_cases)
{
metasequoia::InputSession session;
@@ -843,6 +845,19 @@ int run_test()
"A legal spelling with an alias reading must preedit the typed letters.");
}
// Inputs the guard does not cover fall back to the input as typed when
// the alias layer rewrote the letters: no phantom letter, and a manual
// delimiter the user typed stays in place.
const std::array<PreeditCase, 4> typed_fallback_preedit_cases = {
{{"dongua", "dongua"}, {"zhonguo", "zhonguo"}, {"haoyonga'", "haoyonga'"}, {"hao'yonga", "hao'yonga"}}};
for (const auto &test_case : typed_fallback_preedit_cases)
{
metasequoia::InputSession session;
type(session, test_case.pinyin);
require(session.get_pinyin_segmentation_with_cases() == test_case.expected_preedit,
"An alias-rewritten input must preedit exactly what was typed.");
}
metasequoia::InputSession no_autocorrect_session(SchemeType::Quanpin, 0u);
require(no_autocorrect_session.quanpin_autocorrect_types() == 0,
"The requested pinyin autocorrect setting was not retained.");