Files
Lu Fan(SonnyCalcr) 5de73de7a6 feat(engine,server,windows): 双拼末尾辅助码支持间接触发,句中辅助码单独成区 (#1068)
双拼辅助码新增「直接辅助码」开关(默认开,行为不变)。关掉后末尾辅助码改由引导键触发:
反引号、分号、大写字母可多选,触发键后接一到两个辅码(如 ulpb;xy、ulpbXy),一码重排、
两码筛选,只在输入末尾生效。触发键在 TSF 缓冲与 Server raw 里统一记成 |,与句中辅助码的
反引号段区分。

两组触发键互斥:一边勾上某个键会取消另一边的同一个键;手改配置两边都勾时归句中辅助码。
新增 Worker 消息 EndHelpcodeTriggersChanged(37),旧 DLL 忽略。

设置页把句中辅助码单独成区(仅双拼),其中间接(引导键)与直接(万象式)两种方式互斥。
新配置项同时写进出厂模板与开发配置。
2026-10-18 22:54:17 +08:00

487 lines
16 KiB
C++

#include "shuangpin_query.h"
#include "../common/helpcode_utils.h"
#include "shuangpin_utils.h"
#include <algorithm>
#include <boost/algorithm/string/replace.hpp>
#include <boost/algorithm/string/case_conv.hpp>
namespace shuangpin
{
namespace
{
std::string segment_chunk(const std::string &chunk, const ShuangpinProfile &profile)
{
return chunk.empty() ? std::string{} : ShuangpinUtil::pinyin_segmentation(chunk, profile);
}
} // namespace
std::string segment_input(const std::string &raw_input, const ShuangpinProfile &profile)
{
if (raw_input.empty())
{
return {};
}
std::string result;
size_t segment_start = 0;
bool first_segment = true;
while (segment_start <= raw_input.size())
{
const size_t separator = raw_input.find('\'', segment_start);
const std::string chunk = separator == std::string::npos
? raw_input.substr(segment_start)
: raw_input.substr(segment_start, separator - segment_start);
if (!chunk.empty())
{
if (!first_segment)
{
result.push_back('\'');
}
result += segment_chunk(chunk, profile);
first_segment = false;
}
if (separator == std::string::npos)
{
break;
}
segment_start = separator + 1;
}
return result;
}
std::vector<std::size_t> segment_raw_boundaries(const std::string &raw_input, const ShuangpinProfile &profile)
{
std::vector<std::size_t> boundaries;
if (raw_input.empty())
{
return boundaries;
}
boundaries.push_back(0);
std::size_t chunk_start = 0;
while (chunk_start <= raw_input.size())
{
const std::size_t separator = raw_input.find('\'', chunk_start);
const std::size_t chunk_end = separator == std::string::npos ? raw_input.size() : separator;
std::size_t position = chunk_start;
while (position < chunk_end)
{
// Same forward-greedy rule as pinyin_segmentation: take two keys
// when they form an accepted syllable, otherwise one.
const bool two_key = (chunk_end - position) >= 2 &&
ShuangpinUtil::is_accepted_syllable_code(
boost::algorithm::to_lower_copy(raw_input.substr(position, 2)), profile);
position += two_key ? 2u : 1u;
boundaries.push_back(position);
}
if (separator == std::string::npos)
{
break;
}
chunk_start = separator + 1;
if (chunk_start < raw_input.size())
{
boundaries.push_back(chunk_start);
}
}
// A trailing delimiter starts no unit, but the end of the spelling always
// belongs to the last unit; collapse any duplicate a delimiter next to a
// chunk start produced.
if (boundaries.back() != raw_input.size())
{
boundaries.push_back(raw_input.size());
}
boundaries.erase(std::unique(boundaries.begin(), boundaries.end()), boundaries.end());
return boundaries;
}
std::string to_quanpin_segmentation(const std::string &segmented_input, const ShuangpinProfile &profile)
{
return ShuangpinUtil::convert_seg_shuangpin_to_seg_complete_pinyin(segmented_input, profile);
}
std::string normalize_input_with_delimiters(const std::string &raw_input, const ShuangpinProfile &profile)
{
return to_quanpin_segmentation(segment_input(raw_input, profile), profile);
}
std::string remove_manual_delimiters(const std::string &text)
{
return boost::replace_all_copy(text, "'", "");
}
std::string normalize_input(const std::string &raw_input, const ShuangpinProfile &profile)
{
return remove_manual_delimiters(normalize_input_with_delimiters(raw_input, profile));
}
size_t effective_input_length(const std::string &raw_input)
{
return remove_manual_delimiters(raw_input).size();
}
size_t raw_length_for_effective_prefix(const std::string &raw_input, size_t effective_length)
{
size_t raw_length = 0;
size_t effective_count = 0;
while (raw_length < raw_input.size() && effective_count < effective_length)
{
if (raw_input[raw_length] != '\'')
{
++effective_count;
}
++raw_length;
}
return raw_length;
}
std::string trim_trailing_letters_preserve_delimiters(const std::string &raw_input, size_t letter_count)
{
if (letter_count == 0 || raw_input.empty())
{
return raw_input;
}
size_t remaining = letter_count;
size_t pos = raw_input.size();
while (pos > 0)
{
--pos;
if (raw_input[pos] == '\'')
{
continue;
}
--remaining;
if (remaining == 0)
{
return raw_input.substr(0, pos);
}
}
return {};
}
size_t detect_active_double_helpcode_length(const std::string &raw_input, const std::string &raw_input_with_cases,
const ShuangpinProfile &profile)
{
const std::string effective_input = remove_manual_delimiters(raw_input);
const std::string effective_input_with_cases =
remove_manual_delimiters(raw_input_with_cases.empty() ? raw_input : raw_input_with_cases);
if (!ShuangpinUtil::IsFullHelpMode(effective_input_with_cases, profile))
{
return 0;
}
const size_t base_length = effective_input.size() - 2;
const size_t raw_base_length = raw_length_for_effective_prefix(raw_input, base_length);
if (raw_base_length < raw_input.size() && raw_input[raw_base_length] == '\'')
{
return 0;
}
return is_complete_input(raw_input.substr(0, raw_base_length), profile) ? 2 : 0;
}
bool is_complete_input(const std::string &raw_input, const ShuangpinProfile &profile)
{
if (raw_input.empty() || raw_input.front() == '\'' || raw_input.back() == '\'' ||
raw_input.find("''") != std::string::npos)
{
return false;
}
size_t segment_start = 0;
while (segment_start <= raw_input.size())
{
const size_t separator = raw_input.find('\'', segment_start);
const std::string chunk = separator == std::string::npos
? raw_input.substr(segment_start)
: raw_input.substr(segment_start, separator - segment_start);
if (chunk.empty() ||
!ShuangpinUtil::is_all_complete_pinyin(chunk, ShuangpinUtil::pinyin_segmentation(chunk, profile)))
{
return false;
}
if (separator == std::string::npos)
{
break;
}
segment_start = separator + 1;
}
return true;
}
std::string apply_segmentation_cases(const std::string &segmented_input, const std::string &raw_input_with_cases)
{
if (segmented_input.empty() || raw_input_with_cases.empty())
{
return {};
}
std::string extracted_input;
extracted_input.reserve(segmented_input.size());
for (const char ch : segmented_input)
{
if (ch != '\'')
{
extracted_input.push_back(ch);
}
}
if (extracted_input != boost::algorithm::to_lower_copy(remove_manual_delimiters(raw_input_with_cases)))
{
return segmented_input;
}
std::string result;
result.reserve(segmented_input.size());
size_t index = 0;
for (const char ch : segmented_input)
{
if (ch == '\'')
{
result.push_back(ch);
continue;
}
if (index >= raw_input_with_cases.size())
{
return segmented_input;
}
while (index < raw_input_with_cases.size() && raw_input_with_cases[index] == '\'')
{
++index;
}
if (index >= raw_input_with_cases.size())
{
return segmented_input;
}
const char cased = raw_input_with_cases[index];
if (ch == cased || ch == cased + ('a' - 'A'))
{
result.push_back(cased);
}
else
{
result.push_back(ch);
}
++index;
}
return result;
}
std::string get_first_han_char(const std::string &words)
{
return HelpcodeUtils::get_first_han_char(words);
}
std::string get_last_han_char(const std::string &words)
{
return HelpcodeUtils::get_last_han_char(words);
}
std::string::size_type count_utf8_chars(const std::string &text)
{
return ShuangpinUtil::count_utf8_chars(text);
}
std::string::size_type count_han_chars(const std::string &text)
{
return HelpcodeUtils::count_han_chars(text);
}
namespace
{
char to_ascii_lower(char ch)
{
return FanyImeMidSentenceHelpcode::IsAsciiUpper(ch) ? static_cast<char>(ch + ('a' - 'A')) : ch;
}
std::size_t count_segments(const std::string &segmentation)
{
return segmentation.empty()
? 0
: static_cast<std::size_t>(std::count(segmentation.begin(), segmentation.end(), '\'')) + 1;
}
struct MidSentenceBlock
{
std::size_t begin = 0;
std::size_t end = 0;
// 第一码的位置:反引号段、末尾段是标记后一位,大写段就是段首。
std::size_t codes_begin = 0;
FanyImeMidSentenceHelpcode::BlockKind kind = FanyImeMidSentenceHelpcode::BlockKind::Mid;
};
std::vector<MidSentenceBlock> scan_mid_sentence_blocks(const std::string &raw_input, HelpcodeUppercaseTriggers triggers)
{
std::vector<MidSentenceBlock> blocks;
FanyImeMidSentenceHelpcode::ScanBlocks(
raw_input.data(), raw_input.size(), triggers,
[&](std::size_t begin, std::size_t end, std::size_t codes_begin, FanyImeMidSentenceHelpcode::BlockKind kind) {
blocks.push_back({begin, end, codes_begin, kind});
});
return blocks;
}
} // namespace
bool has_mid_sentence_helpcode(const std::string &raw_input, HelpcodeUppercaseTriggers triggers)
{
if (raw_input.find(kMidSentenceHelpcodeMarker) != std::string::npos ||
raw_input.find(kEndHelpcodeMarker) != std::string::npos)
{
return true;
}
return (triggers.mid || triggers.end) && !scan_mid_sentence_blocks(raw_input, triggers).empty();
}
MidSentenceHelpcodeInput parse_mid_sentence_helpcodes(const std::string &raw_input_with_cases,
const ShuangpinProfile &profile,
HelpcodeUppercaseTriggers triggers)
{
MidSentenceHelpcodeInput parsed;
parsed.input.reserve(raw_input_with_cases.size());
parsed.source_index.reserve(raw_input_with_cases.size() + 1);
const auto blocks = scan_mid_sentence_blocks(raw_input_with_cases, triggers);
auto next_block = blocks.begin();
std::size_t index = 0;
while (index < raw_input_with_cases.size())
{
if (next_block == blocks.end() || index != next_block->begin)
{
parsed.input.push_back(raw_input_with_cases[index]);
parsed.source_index.push_back(index);
++index;
continue;
}
const std::size_t end = next_block->end;
const std::size_t codes_begin = next_block->codes_begin;
const bool end_block = next_block->kind == FanyImeMidSentenceHelpcode::BlockKind::End;
++next_block;
const std::size_t syllables =
count_segments(segment_input(boost::algorithm::to_lower_copy(parsed.input), profile));
if (end_block)
{
// 末尾段:在输入串末尾时按词筛(码交给请求),否则只留显示、当分隔符。
if (syllables > 0)
{
parsed.decorations.emplace_back(syllables - 1, raw_input_with_cases.substr(index, end - index));
}
if (end == raw_input_with_cases.size())
{
parsed.end_helpcodes.clear();
for (std::size_t code = codes_begin; code < end; ++code)
{
parsed.end_helpcodes.push_back(to_ascii_lower(raw_input_with_cases[code]));
}
index = end;
continue;
}
}
else if (syllables > 0)
{
const std::size_t syllable = syllables - 1;
parsed.decorations.emplace_back(syllable, raw_input_with_cases.substr(index, end - index));
if (end > codes_begin)
{
SyllableHelpcode helpcode;
helpcode.syllable = syllable;
helpcode.first = to_ascii_lower(raw_input_with_cases[codes_begin]);
helpcode.second = end > codes_begin + 1 ? to_ascii_lower(raw_input_with_cases[codes_begin + 1]) : 0;
// 同一个音节敲了两段,以后一段为准。
auto existing = std::find_if(parsed.helpcodes.begin(), parsed.helpcodes.end(),
[&](const SyllableHelpcode &item) { return item.syllable == syllable; });
if (existing != parsed.helpcodes.end())
*existing = helpcode;
else
parsed.helpcodes.push_back(helpcode);
}
}
// 反引号段同时是一个确定的音节边界,换成手动分隔符;已经有分隔符就不再叠一个。
if (!parsed.input.empty() && parsed.input.back() != '\'')
{
parsed.input.push_back('\'');
parsed.source_index.push_back(index);
}
index = end;
}
parsed.source_index.push_back(raw_input_with_cases.size());
return parsed;
}
std::string decorate_mid_sentence_segmentation(const std::string &segmentation, const std::string &raw_input_with_cases,
const ShuangpinProfile &profile, HelpcodeUppercaseTriggers triggers)
{
if (!has_mid_sentence_helpcode(raw_input_with_cases, triggers) || segmentation.empty())
{
return segmentation;
}
return decorate_segmentation(segmentation,
parse_mid_sentence_helpcodes(raw_input_with_cases, profile, triggers).decorations);
}
std::string decorate_segmentation(const std::string &segmentation,
const std::vector<std::pair<std::size_t, std::string>> &decorations)
{
if (decorations.empty() || segmentation.empty())
{
return segmentation;
}
std::vector<std::string> parts;
std::size_t start = 0;
while (true)
{
const std::size_t separator = segmentation.find('\'', start);
parts.push_back(
segmentation.substr(start, separator == std::string::npos ? std::string::npos : separator - start));
if (separator == std::string::npos)
break;
start = separator + 1;
}
for (const auto &[syllable, text] : decorations)
{
if (syllable < parts.size())
parts[syllable] += text;
}
std::string decorated;
for (std::size_t i = 0; i < parts.size(); ++i)
{
if (i > 0)
decorated.push_back('\'');
decorated += parts[i];
}
return decorated;
}
bool accepts_mid_sentence_helpcode_marker(const std::string &raw_input, HelpcodeUppercaseTriggers triggers)
{
return FanyImeMidSentenceHelpcode::AcceptsMarker(raw_input.data(), raw_input.size(), triggers);
}
bool accepts_mid_sentence_helpcode_marker_at(const std::string &raw_input, std::size_t caret,
HelpcodeUppercaseTriggers triggers)
{
return FanyImeMidSentenceHelpcode::AcceptsMarkerAt(raw_input.data(), raw_input.size(), caret, triggers);
}
bool accepts_mid_sentence_second_code(const std::string &raw_input, char ch, HelpcodeUppercaseTriggers triggers)
{
return FanyImeMidSentenceHelpcode::IsAsciiUpper(ch) &&
FanyImeMidSentenceHelpcode::AcceptsSecondCodeAt(raw_input.data(), raw_input.size(), raw_input.size(),
triggers);
}
} // namespace shuangpin