Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 55 additions & 4 deletions src/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ cc_library(
":common_lib",
":config_lib",
":conversion_lib",
":conversion_candidates_lib",
":conversion_chain_lib",
":converter_lib",
":darts_dict_lib",
Expand Down Expand Up @@ -126,15 +127,24 @@ diff_test(
file2 = "opencc_config_schema.inc",
)

# Private header shared between Config.cpp (which builds the converter) and
# conversion_candidates_lib (which unwraps the normalization pre-pass).
cc_library(
name = "config_lib",
srcs = [
"Config.cpp",
"ConfigBasedConverter.hpp",
name = "config_based_converter_lib",
hdrs = ["ConfigBasedConverter.hpp"],
visibility = ["//src:__pkg__"],
deps = [
":converter_lib",
],
)

cc_library(
name = "config_lib",
srcs = ["Config.cpp"],
hdrs = ["Config.hpp"],
deps = [
":common_lib",
":config_based_converter_lib",
":config_schema_inc_lib",
":conversion_chain_lib",
":converter_lib",
Expand Down Expand Up @@ -275,6 +285,47 @@ cc_library(
],
)

# Internal, unstable candidate-enumeration API (GetAllConversions). The header
# is not installed by the CMake build and the function is not part of the
# OPENCC_ABI_VERSION contract; visibility is restricted to this package so no
# other Bazel target can depend on it directly until the API is promoted.
cc_library(
name = "conversion_candidates_lib",
srcs = ["ConversionCandidates.cpp"],
hdrs = ["ConversionCandidates.hpp"],
visibility = ["//src:__pkg__"],
deps = [
":common_lib",
":config_based_converter_lib",
":conversion_lib",
":conversion_chain_lib",
":converter_lib",
":dict_lib",
":dict_entry_lib",
],
)

cc_test(
name = "conversion_candidates_test",
size = "small",
srcs = ["ConversionCandidatesTest.cpp"],
deps = [
":config_based_converter_lib",
":conversion_candidates_lib",
":conversion_lib",
":conversion_chain_lib",
":converter_lib",
":lexicon_lib",
":max_match_segmentation_lib",
":pipeline_converter_lib",
":single_stage_converter_lib",
":test_utils_lib",
":test_utils_utf8_lib",
":text_dict_lib",
"@googletest//:gtest_main",
],
)

cc_library(
name = "single_stage_converter_lib",
srcs = ["SingleStageConverter.cpp"],
Expand Down
3 changes: 3 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ set(
set(
LIBOPENCC_PRIVATE_HEADERS
ConfigBasedConverter.hpp
ConversionCandidates.hpp
DictConverter.hpp
PhraseExtract.hpp
PipelineConverter.hpp
Expand All @@ -59,6 +60,7 @@ set(
LIBOPENCC_SOURCES
Config.cpp
Conversion.cpp
ConversionCandidates.cpp
ConversionChain.cpp
Converter.cpp
Dict.cpp
Expand All @@ -85,6 +87,7 @@ set(

set(UNITTESTS
ConfigTest
ConversionCandidatesTest
ConversionChainTest
ConversionInspectionTest
ConversionTest
Expand Down
8 changes: 8 additions & 0 deletions src/ConfigBasedConverter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ class ConfigBasedConverter : public Converter {
return mainConverter->GetConversionChain();
}

/**
* Returns the normalization pre-pass converter. Used by GetAllConversions()
* to normalize a word before walking the main chain, mirroring Convert().
*/
const ConverterPtr& GetNormalizationConverter() const {
return normConverter;
}

private:
const ConverterPtr normConverter;
const ConverterPtr mainConverter;
Expand Down
100 changes: 100 additions & 0 deletions src/ConversionCandidates.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Open Chinese Convert
*
* Copyright 2010-2026 Carbo Kuo and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#include "ConversionCandidates.hpp"

#include <unordered_set>

#include "Common.hpp"
#include "ConfigBasedConverter.hpp"
#include "Conversion.hpp"
#include "ConversionChain.hpp"
#include "Converter.hpp"
#include "Dict.hpp"
#include "DictEntry.hpp"
#include "Optional.hpp"

namespace opencc {

std::vector<std::string> GetAllConversions(const Converter& converter,
std::string_view word) {
const ConversionChainPtr chain = converter.GetConversionChain();
if (chain == nullptr) {
return {};
}

// A config-based converter runs a normalization pre-pass (e.g. CJK
// Compatibility Ideographs) before its main chain, but GetConversionChain()
// exposes only the main chain. Mirror Convert() by normalizing first, so a
// word whose dictionary key appears only after normalization is still found.
std::string normalized;
if (const auto* configBased =
dynamic_cast<const ConfigBasedConverter*>(&converter)) {
normalized = configBased->GetNormalizationConverter()->Convert(word);
word = normalized;
}

std::vector<std::string> currentWords{std::string(word)};
bool matched = false;
for (const ConversionPtr& conversion : chain->GetConversions()) {
const DictPtr dict = conversion->GetDict();
if (dict == nullptr) {
return {};
}
std::unordered_set<std::string> seen;
std::vector<std::string> nextWords;
for (const std::string& currentWord : currentWords) {
const Optional<const DictEntry*> item = dict->Match(currentWord);
if (item.IsNull()) {
// No exact match: convert greedily by longest prefix so a partially
// convertible word can still flow through the rest of the chain. Even
// when the result is unchanged we keep it, because a later dictionary
// may still convert it. Conversion::Convert also handles ideographic
// description sequences and truncated UTF-8 safely.
std::string buffer = conversion->Convert(currentWord);
if (seen.insert(buffer).second) {
nextWords.push_back(std::move(buffer));
}
continue;
}
matched = true;
if (item.Get()->NumValues() == 0) {
// A key-only entry converts the word to itself, matching Convert(),
// which falls back to the key via DictEntry::GetDefault().
if (seen.insert(currentWord).second) {
nextWords.push_back(currentWord);
}
continue;
}
for (const std::string& value : item.Get()->Values()) {
if (seen.insert(value).second) {
nextWords.push_back(value);
}
}
}
currentWords.swap(nextWords);
}

if (!matched) {
// No dictionary in the chain contains the word.
return {};
}
return currentWords;
}

} // namespace opencc
73 changes: 73 additions & 0 deletions src/ConversionCandidates.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Open Chinese Convert
*
* Copyright 2010-2026 Carbo Kuo and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#pragma once

#include <string>
#include <string_view>
#include <vector>

#include "Export.hpp"

namespace opencc {

class Converter;

/**
* Enumerates every candidate form of a single word produced by running it
* through @p converter's conversion chain.
*
* Unlike Converter::Convert(), which segments the whole text and yields exactly
* one output string, this walks @p word through each dictionary in the chain
* and keeps @b all branch values. For example, @c s2t expands @c 里 to both
* @c 里 and @c 裏, then @c t2tw passes @c 里 through unchanged and converts
* @c 裏 to @c 裡. This mirrors the behaviour input-method engines (e.g.
* librime's @c ConvertWord) need to offer users every plausible conversion of a
* single word.
*
* Matching a word against a dictionary:
* - On an exact match, every value of the entry becomes a candidate; a
* key-only entry (no values) yields the word itself, matching Convert().
* - Otherwise the word is converted with Conversion::Convert (greedy
* longest-prefix, each matched prefix's default value), so a partially
* convertible word still flows through the remaining dictionaries in the
* chain.
*
* A converter loaded from a config with a @c normalization step (e.g.
* @c s2t.json) normalizes @p word first, mirroring Converter::Convert().
*
* @param converter Source of the conversion chain. A converter without a
* single chain (e.g. @c PipelineConverter, whose GetConversionChain()
* returns @c nullptr) yields an empty result.
* @param word UTF-8 text of a single word; need not be null-terminated.
* @return Candidate forms in discovery order with duplicates removed. Empty
* when no dictionary in the chain contains @p word, matching librime's
* convention of reporting "not found".
*
* @note Internal, unstable API with no compatibility guarantee. This header
* is not installed by CMake and the function is not part of the
* OPENCC_ABI_VERSION contract; like other non-installed headers
* (e.g. PhraseExtract.hpp) the symbol is still OPENCC_EXPORT so unit tests
* can link against a shared libopencc on Windows, which means the symbol is
* technically reachable by forward declaration — binding to it outside this
* repository is unsupported and may break without an ABI bump.
*/
OPENCC_EXPORT std::vector<std::string>
GetAllConversions(const Converter& converter, std::string_view word);

} // namespace opencc
Loading
Loading