/* * SPDX-FileCopyrightText: 2023-2023 CSSlayer * * SPDX-License-Identifier: LGPL-2.1-or-later * */ #include "customphrase.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace fcitx { namespace { // localtime_s is relatively evil and we simply want to avoid it. namespace localtime_helper { struct not_available_tag {}; [[maybe_unused]] not_available_tag localtime_r(...) { return {}; } } // namespace localtime_helper struct localtime_impl { std::tm tm_{}; std::time_t time_{}; bool get() { using namespace localtime_helper; return localtime_fallback(localtime_r(&time_, &tm_)); } bool localtime_fallback(std::tm *t) { return t != nullptr; } bool localtime_fallback(localtime_helper::not_available_tag) { // This is a least worse option since win has thread-local for it. auto *t = std::localtime(&time_); if (t) { tm_ = *t; return true; } return false; } }; void normalizeData(std::vector &data) { std::ranges::stable_sort( data, [](const CustomPhrase &lhs, const CustomPhrase &rhs) { return lhs.order() < rhs.order(); }); } std::optional parseInt(std::string_view input) { int out; const std::from_chars_result result = std::from_chars(input.data(), input.data() + input.size(), out); if (result.ec == std::errc::invalid_argument || result.ec == std::errc::result_out_of_range) { return std::nullopt; } return out; } using ParseResult = std::tuple; std::optional parseCustomPhraseLine(std::string_view line) { size_t i = 0; // Consume all letters. for (; i < line.size() && (charutils::islower(line[i]) || charutils::isupper(line[i])); i += 1) { } if (i == 0) { return std::nullopt; } const std::string_view alpha = line.substr(0, i); if (i >= line.size() || line[i] != ',') { return std::nullopt; } i += 1; int sign = 1; if (i < line.size() && line[i] == '-') { sign = -1; i += 1; } const size_t orderStart = i; for (; i < line.size() && charutils::isdigit(line[i]); i += 1) { } if (i == orderStart || i >= line.size() || line[i] != '=') { return std::nullopt; } int order = 0; if (auto result = parseInt(line.substr(orderStart, i - orderStart))) { order = *result; } // Zero is invalid value. if (order == 0) { return std::nullopt; } order *= sign; return std::make_tuple(alpha, order, line.substr(i + 1)); } bool isComment(std::string_view line) { return line.starts_with(';') || line.starts_with('#'); } inline std::tm currentTm() { #if defined(FCITX_CUSTOM_PHRASE_TEST) std::tm timePoint; timePoint.tm_year = 2023 - 1900; timePoint.tm_mon = 6; timePoint.tm_mday = 11; timePoint.tm_wday = 2; timePoint.tm_hour = 23; timePoint.tm_min = 16; timePoint.tm_sec = 6; return timePoint; #else const std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); localtime_impl impl{.time_ = std::chrono::system_clock::to_time_t(now)}; impl.get(); return impl.tm_; #endif } int currentYear() { return currentTm().tm_year + 1900; } int currentMonth() { return currentTm().tm_mon + 1; } int currentDay() { return currentTm().tm_mday; } int currentWeekday() { return currentTm().tm_wday; } int currentHour() { return currentTm().tm_hour; } int currentMinute() { return currentTm().tm_min; } int currentSecond() { return currentTm().tm_sec; } int currentHalfHour() { const int hour = currentHour() % 12; return (hour == 0) ? 12 : hour; } std::string toChineseYear(std::string_view num) { constexpr std::string_view chineseDigit[] = { "〇", "一", "二", "三", "四", "五", "六", "七", "八", "九", }; std::string result; result.reserve(num.size() * 3); for (const char c : num) { assert(charutils::isdigit(c)); result += chineseDigit[c - '0']; } return result; } std::string toChineseWeekDay(int num) { assert(num >= 0 && num < 7); constexpr std::string_view chineseWeekday[] = { "日", "一", "二", "三", "四", "五", "六", }; return std::string(chineseWeekday[num]); } std::string toChineseTwoDigitNumber(int num, bool leadingZero) { assert(num >= 0 && num < 100); constexpr std::string_view chineseDigit[] = { "零", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十", }; if (num == 0) { return std::string(chineseDigit[0]); } const int tens = num / 10; const int ones = num % 10; std::string prefix; if (tens == 0) { if (leadingZero) { prefix = chineseDigit[0]; } } else if (tens == 1) { prefix = chineseDigit[10]; } else { prefix = stringutils::concat(chineseDigit[tens], chineseDigit[10]); } std::string suffix; if (ones != 0) { suffix = chineseDigit[ones]; } return prefix + suffix; } } // namespace bool CustomPhrase::isDynamic() const { return value().starts_with("#"); } std::string CustomPhrase::evaluate( const std::function &evaluator) const { assert(evaluator); if (!isDynamic()) { return value_; } std::string_view content = value_; content = content.substr(1); std::string output; output.reserve(content.size()); size_t variableNameStart = 0; size_t variableNameLength = 0; enum class State { Normal, VariableStart, BracedVariable, Variable, }; auto state = State::Normal; for (size_t i = 0; i < content.size();) { const char c = content[i]; switch (state) { case State::Normal: if (c == '$') { state = State::VariableStart; } else { output += c; } i += 1; break; case State::VariableStart: if (c == '{') { variableNameStart = i + 1; variableNameLength = 0; state = State::BracedVariable; } else if (c == '$') { output += '$'; state = State::Normal; } else if (charutils::islower(c) || charutils::isupper(c) || c == '_') { variableNameStart = i; variableNameLength = 1; state = State::Variable; } else { output += '$'; output += c; state = State::Normal; } i += 1; break; case State::BracedVariable: if (c == '}') { output += evaluator( content.substr(variableNameStart, variableNameLength)); state = State::Normal; } else { variableNameLength += 1; state = State::BracedVariable; } i += 1; break; case State::Variable: if (charutils::islower(c) || charutils::isupper(c) || charutils::isdigit(c) || c == '_') { variableNameLength += 1; state = State::Variable; i += 1; } else { output += evaluator( content.substr(variableNameStart, variableNameLength)); state = State::Normal; } break; } } switch (state) { case State::Normal: break; case State::VariableStart: output += '$'; break; case State::BracedVariable: output += "${"; output += content.substr(variableNameStart, variableNameLength); break; case State::Variable: output += evaluator(content.substr(variableNameStart, variableNameLength)); break; } return output; } std::string CustomPhrase::builtinEvaluator(std::string_view key) { static const std::map, std::less<>> table = { {"year", []() { return std::to_string(currentYear()); }}, {"year_yy", []() { return std::format("{:02d}", currentYear() % 100); }}, {"month", []() { return std::to_string(currentMonth()); }}, {"month_mm", []() { return std::format("{:02d}", currentMonth()); }}, {"day", []() { return std::to_string(currentDay()); }}, {"day_dd", []() { return std::format("{:02d}", currentDay()); }}, {"weekday", []() { return std::to_string(currentWeekday()); }}, {"fullhour", []() { return std::format("{:02d}", currentHour()); }}, {"halfhour", []() { return std::format("{:02d}", currentHalfHour()); }}, {"ampm", []() { return currentHour() < 12 ? "AM" : "PM"; }}, {"minute", []() { return std::format("{:02d}", currentMinute()); }}, {"second", []() { return std::format("{:02d}", currentSecond()); }}, {"year_cn", []() { return toChineseYear(std::to_string(currentYear())); }}, {"year_yy_cn", []() { return toChineseYear( std::format("{:02d}", currentYear() % 100)); }}, {"month_cn", []() { return toChineseTwoDigitNumber(currentMonth(), /*leadingZero=*/false); }}, {"day_cn", []() { return toChineseTwoDigitNumber(currentDay(), /*leadingZero=*/false); }}, {"weekday_cn", []() { return toChineseWeekDay(currentWeekday()); }}, {"fullhour_cn", []() { return toChineseTwoDigitNumber(currentHour(), /*leadingZero=*/false); }}, {"halfhour_cn", []() { return toChineseTwoDigitNumber(currentHalfHour(), /*leadingZero=*/false); }}, {"ampm_cn", []() { return currentHour() < 12 ? "上午" : "下午"; }}, {"minute_cn", []() { return toChineseTwoDigitNumber(currentMinute(), /*leadingZero=*/true); }}, {"second_cn", []() { return toChineseTwoDigitNumber(currentSecond(), /*leadingZero=*/true); }}, }; auto iter = table.find(key); if (iter != table.end()) { return iter->second(); } return ""; } CustomPhraseDict::CustomPhraseDict() = default; void CustomPhraseDict::load(std::istream &in, bool loadDisabled) { clear(); std::string line; // Line looks like // [a-z]+,[-][0-9]+=phrase CustomPhrase *multiline = nullptr; // If loadDisabled is true CustomPhrase dummyPhrase(-1, {}); auto cleanUpMultiline = [&multiline]() { if (!multiline) { return; } if (!multiline->value().empty()) { multiline->mutableValue().pop_back(); } multiline = nullptr; }; while (std::getline(in, line)) { if (!multiline) { if (isComment(line)) { continue; } } auto parseResult = parseCustomPhraseLine(line); if (parseResult) { cleanUpMultiline(); auto [key, order, data] = *parseResult; std::string value{data}; if (value.size() >= 2 && value.starts_with('"') && value.ends_with('"')) { if (auto unescape = stringutils::unescapeForValue(value)) { value = *unescape; } } if (!loadDisabled && order < 0) { if (data.empty()) { multiline = &dummyPhrase; } continue; } auto index = index_.exactMatchSearch(key); if (TrieType::isNoValue(index)) { if (data_.size() >= std::numeric_limits::max()) { break; } index = data_.size(); index_.set(key, index); data_.push_back({}); } data_[index].push_back(CustomPhrase(order, std::move(value))); if (data.empty()) { multiline = &data_[index].back(); } } else if (multiline && multiline != &dummyPhrase) { multiline->mutableValue().append(line); // Always append new line, and we will pop the last new line in // cleanUpMultiline. multiline->mutableValue().append("\n"); } } cleanUpMultiline(); for (auto &data : data_) { normalizeData(data); } } const std::vector * CustomPhraseDict::lookup(std::string_view key) const { auto index = index_.exactMatchSearch(key); if (TrieType::isNoValue(index)) { return nullptr; } return &data_[index]; } std::vector * CustomPhraseDict::getOrCreateEntry(std::string_view key) { auto index = index_.exactMatchSearch(key); if (TrieType::isNoValue(index)) { if (data_.size() >= std::numeric_limits::max()) { return nullptr; } index = data_.size(); index_.set(key, index); data_.push_back({}); } return &data_[index]; } void CustomPhraseDict::addPhrase(std::string_view key, std::string_view value, int order) { if (order == 0) { return; } if (auto *entry = getOrCreateEntry(key)) { entry->push_back(CustomPhrase(order, std::string(value))); } } void CustomPhraseDict::pinPhrase(std::string_view key, std::string_view value) { removePhrase(key, value); if (auto *entry = getOrCreateEntry(key)) { // enabled item is 1 based. entry->insert(entry->begin(), CustomPhrase(1, std::string(value))); normalizeData(*entry); } } void CustomPhraseDict::removePhrase(std::string_view key, std::string_view value) { auto index = index_.exactMatchSearch(key); if (TrieType::isNoValue(index)) { return; } data_[index].erase(std::remove_if(data_[index].begin(), data_[index].end(), [&value](const CustomPhrase &item) { return value == item.value(); }), data_[index].end()); } void CustomPhraseDict::save(std::ostream &out) const { std::string buf; index_.foreach([&out, &buf, this](uint32_t value, size_t _len, libime::DATrie::position_type pos) { index_.suffix(buf, _len, pos); for (const auto &phrase : data_[value]) { auto escaped = fcitx::stringutils::escapeForValue(phrase.value()); out << buf << "," << phrase.order() << "="; if (escaped.size() != phrase.value().size()) { // Always quote escaped value. if (escaped.front() != '"') { out << '"'; } out << escaped; if (escaped.back() != '"') { out << '"'; } } else { out << phrase.value(); } out << '\n'; } return true; }); } void CustomPhraseDict::clear() { index_.clear(); data_.clear(); } } // namespace fcitx