/* Copyright: © SIL International. Description: This is an implementation of the LDML keyboard spec 3.0. Create Date: 5 Aug 2022 Authors: Marc Durdin (MD) */ #include #include "ldml/ldml_processor.hpp" #include "ldml/ldml_transforms.hpp" #include "ldml/ldml_markers.hpp" #include "state.hpp" #include "kmx_file.h" #include "kmx/kmx_plus.h" #include "kmx/kmx_xstring.h" #include "kmx/kmx_processevent.h" #include "kmx/kmx_processor.hpp" #include "ldml/keyman_core_ldml.h" #include "kmx/kmx_file_validator.hpp" #include "debuglog.h" #include namespace { constexpr km_core_attr const engine_attrs = { 256, KM_CORE_LIB_CURRENT, KM_CORE_LIB_AGE, KM_CORE_LIB_REVISION, KM_CORE_TECH_LDML, "SIL International" }; } // using km::core::kmx::ShouldDebug; // for DebugLog namespace km { namespace core { ldml_processor::ldml_processor(std::u16string const& kb_name, const std::vector& data) : abstract_processor(keyboard_attributes(kb_name, KM_CORE_LMDL_PROCESSOR_VERSION, {})), _valid(false), transforms(), bksp_transforms(), keys(), normalization_disabled(false) { if(data.size() <= sizeof(kmx::COMP_KEYBOARD_EX)) { DebugLog("data.size %zu too small", data.size()); return; } // Locate the structs here, but still retain ptrs to the raw structs. kmx::KMX_FileValidator* comp_keyboard = (kmx::KMX_FileValidator*)data.data(); // Perform the standard validation if(!comp_keyboard->VerifyKeyboard(data.size())) { DebugLog("VerifyKeyboard() returned false"); return; } kmx::kmx_plus kplus(comp_keyboard, data.size()); // slices to a COMP_KEYBOARD if(!kplus.is_valid()) { DebugLog("kmx_plus.is_valid is false"); return; } // Now, if we have keys, use them. if (kplus.key2 != nullptr) { // read all keys into array for (KMX_DWORD i=0; ikmapCount; i++) { std::u16string str; auto kmapEntry = kplus.key2Helper.getKmap(i); assert(kmapEntry != nullptr); // now look up the key auto keyEntry = kplus.key2Helper.getKeys(kmapEntry->key); assert(keyEntry != nullptr); if (keyEntry->flags & LDML_KEYS_KEY_FLAGS_EXTEND) { if (nullptr == kplus.strs) { DebugLog("for keys: kplus.strs == nullptr"); // need a string table to get strings assert(false); return; } str = kplus.strs->get(keyEntry->to); } else { str = keyEntry->get_to_string(); } keys.add((km_core_virtual_key)kmapEntry->vkey, kmapEntry->mod, str); } } else { DebugLog("Error, this keyboard has no 'key2' section on it!"); return; } // load transforms if (kplus.tran != nullptr && kplus.tran->groupCount > 0) { // Note that transforms::load depends on several other sections, // and this loading will fail if they are missing. transforms.reset(km::core::ldml::transforms::load(kplus, kplus.tran, kplus.tranHelper)); if (!transforms) { DebugLog("Failed to load tran transforms"); return; // failed to load } } // else: no transforms // load bksp transforms if (kplus.bksp != nullptr && kplus.bksp->groupCount > 0) { // Note that transforms::load depends on several other sections, // and this loading will fail if they are missing. bksp_transforms.reset(km::core::ldml::transforms::load(kplus, kplus.bksp, kplus.bkspHelper)); if (!bksp_transforms) { DebugLog("Failed to load bksp transforms"); return; // failed to load } } // else: no bksp transforms if (kplus.meta != nullptr) { if (kplus.meta->settings & LDML_META_SETTINGS_NORMALIZATION_DISABLED) { normalization_disabled = true; } } // else: no metadata // Only valid if we reach here DebugLog("_valid = true"); _valid = true; } /** * Returns true if the data is a KMX+ file. * * @param data the keyboard blob * @return true if the processor can handle the keyboard, otherwise false. */ bool ldml_processor::is_handled(const std::vector& data) { // Check if it's a blob from a KMX file if (!kmx_processor::is_handled(data)) { return false; } const kmx::PCOMP_KEYBOARD comp_keyboard = (kmx::PCOMP_KEYBOARD)data.data(); if (comp_keyboard->dwFileVersion < VERSION_160 || (comp_keyboard->dwFlags & KF_KMXPLUS) == 0) { return false; } // The buffer contains KMXPlus data (although more validation is required and will // be done in the constructor) return true; } km_core_status ldml_processor::process_queued_actions( km_core_state *_kmn_unused(state) ) { /* Not implemented. Only used by IMX. */ assert(false); return KM_CORE_STATUS_NOT_IMPLEMENTED; } bool ldml_processor::queue_action( km_core_state * _kmn_unused(state), km_core_action_item const* _kmn_unused(action_item) ) { /* Not implemented. Only used by IMX. */ assert(false); return false; } km_core_status ldml_processor::process_event( km_core_state *state, km_core_virtual_key vk, uint16_t modifier_state, uint8_t is_key_down, uint16_t event_flags ) { assert(state); if (!state) return KM_CORE_STATUS_INVALID_ARGUMENT; // this will hold the parameters, and our response ldml_event_state ldml_state(state, vk, modifier_state, is_key_down, event_flags); ldml_state.clear(); try { if (vk == KM_CORE_VKEY_BKSP) { process_backspace(ldml_state); } else if (is_key_down) { process_key_down(ldml_state); } else { process_key_up(ldml_state); } // all key-up and key-down events end up here. // commit the ldml state into the core state ldml_state.commit(); return KM_CORE_STATUS_OK; } catch (std::bad_alloc &) { // out of memory, clean up and get out ldml_state.clear(); // no actions will be set return KM_CORE_STATUS_NO_MEM; } } void ldml_processor::process_key_up(ldml_event_state &ldml_state) const { // TODO-LDML: Implement caps lock handling // Look up the key bool found = false; const std::u16string key_str = keys.lookup(ldml_state.get_vk(), ldml_state.get_modifier_state(), found); if (!found) { ldml_state.emit_passthrough_keystroke(); } } void ldml_processor::process_backspace(ldml_event_state &ldml_state) const { if (ldml_state.get_modifier_state() & K_MODIFIERFLAG) { // we never process modifier+bksp ldml_state.emit_passthrough_keystroke(); return; } if (!ldml_state.is_key_down()) { if (!ldml_state.get_state()->backspace_handled_internally()) { ldml_state.emit_passthrough_keystroke(); } return; } if (!!bksp_transforms) { // process with an empty string via the bksp transforms auto matchedContext = process_output(ldml_state, std::u32string(), bksp_transforms.get()); if (matchedContext > 0) { return; // The transform took care of the backspacing. } // else, fall through to default processing below. } ldml_state.emit_backspace(); } void ldml_event_state::emit_backspace() { // this is called from user-initiated backspace, not internal backspacing. // Find out what the last actual character was and remove it. // attempt to get the last char // TODO-LDML: emoji backspace while (!state->context().empty()) { auto end = state->context().back(); if (end.type == KM_CORE_CT_CHAR) { actions.code_points_to_delete++; state->context().pop_back(); return; } // else loop again assert(end.type != KM_CORE_CT_END); // inappropriate here. state->context().pop_back(); } /* We couldn't find a character at end of context (context is empty), so we'll pass the backspace keystroke on to the app to process; the app might want to use backspace to move between contexts or delete a text box, etc. Or it might be a legacy app and we've had our caret dumped in somewhere unknown, so we will have to depend on the app to be sensible about backspacing because we know nothing. */ actions.emit_keystroke = KM_CORE_TRUE; } void ldml_processor::process_key_down(ldml_event_state &ldml_state) const { // Look up the key bool found = false; const std::u16string key_str = keys.lookup(ldml_state.get_vk(), ldml_state.get_modifier_state(), found); if (!found) { // no key was found, so pass the keystroke on to the Engine ldml_state.emit_passthrough_keystroke(); } else if (!key_str.empty()) { process_key_string(ldml_state, key_str); } // else no action: It's a gap or gap-like key. } void ldml_processor::process_key_string(ldml_event_state &ldml_state, const std::u16string &key_str) const { // We know that key_str is not empty per the caller. assert(!key_str.empty()); // we convert the keys str to UTF-32 here instead of using the emit_text() overload // so that we don't have to reconvert it inside the transform code. std::u32string key_str32 = kmx::u16string_to_u32string(key_str); (void)process_output(ldml_state, key_str32, transforms.get()); } size_t ldml_processor::process_output(ldml_event_state &ldml_state, const std::u32string &key_str, ldml::transforms *with_transforms) const { // previous context string std::u32string old_ctxt; (void)ldml_state.context_to_string(old_ctxt, true); // new context string (NFD) std::u32string new_ctxt = old_ctxt; // add the newly added key output to new_ctxt new_ctxt.append(key_str); // Note: // The normalize functions will assert() and DebugLog() if there is a problem, // so we do not need to assert their status here unless we're going to do something // different with control flow. if (!normalization_disabled) { (void)ldml::normalize_nfd_markers(old_ctxt); (void)ldml::normalize_nfd_markers(new_ctxt); } /** how many chars of the new_ctxt to replace? This is our return value. */ const size_t new_ctxt_matched = apply_transforms(new_ctxt, with_transforms); if (new_ctxt_matched == 0 && key_str.empty()) { // no transforms matched, and no key string came in // just return. assert(old_ctxt == new_ctxt); } else { // apply difference ldml_state.emit_difference(old_ctxt, new_ctxt); } return new_ctxt_matched; } size_t ldml_processor::apply_transforms(std::u32string &new_ctxt, ldml::transforms *with_transforms) const { if (with_transforms == nullptr) { return 0; // nothing to do } /** transformed output string */ std::u32string transform_output; size_t new_ctxt_matched = with_transforms->apply(new_ctxt, transform_output); // drop last 'matchedContext': new_ctxt.resize(new_ctxt.length() - new_ctxt_matched); // TODO-LDML: should be able to do a normalization-safe append here. Instead, we append and re-normalize. new_ctxt.append(transform_output); if (!normalization_disabled) { (void)ldml::normalize_nfd_markers(new_ctxt); } // new_ctxt is now up-to-date and ready to apply. return new_ctxt_matched; } km_core_attr const & ldml_processor::attributes() const { return engine_attrs; } km_core_keyboard_key * ldml_processor::get_key_list() const { return keys.get_key_list(); } km_core_keyboard_imx * ldml_processor::get_imx_list() const { km_core_keyboard_imx* imx_list = new km_core_keyboard_imx(KM_CORE_KEYBOARD_IMX_END); return imx_list; } km_core_context_item * ldml_processor::get_intermediate_context() { km_core_context_item *citems = new km_core_context_item(KM_CORE_CONTEXT_ITEM_END); return citems; } km_core_status ldml_processor::validate() const { return _valid ? KM_CORE_STATUS_OK : KM_CORE_STATUS_INVALID_KEYBOARD; } void ldml_event_state::emit_difference(const std::u32string &old_ctxt, const std::u32string &new_ctxt) { // find common prefix. // For example, if the context previously had "aaBBBBB" and it is changing to "aaCCC" then we will have: // - old_ctxtstr_changed = "BBBBB" // - new_ctxtstr_changed = "CCC" // So the BBBBB needs to be removed and then CCC added. auto ctxt_prefix = mismatch(old_ctxt.begin(), old_ctxt.end(), new_ctxt.begin(), new_ctxt.end()); // is the 'mismatch' at the end (i.e., no mismatch)? if(ctxt_prefix.first == old_ctxt.end() && ctxt_prefix.second == new_ctxt.end()) { return; // Optimization: We can just exit, there's nothing to do. } // handle a special case where we're simply changing from one marker to another. // Example: // 0. old_ctxtstr_changed ends with … U+FFFF U+0008 | U+0001 … // 1. ctxtstr_cleanedup ends with … U+FFFF U+0008 | U+0002 … // Pipe symbol shows where the difference starts. // As you can see, the different starts in the MIDDLE of a marker sequence. // so, old_ctxtstr_changed will start with U+0001 // and new_ctxtstr_changed will start with U+0002 // remove_text will only be able to delete up to and through the U+0001 // and it will need to emit a push_backspace(KM_CORE_BT_MARKER,…) due to the // marker change. // We can detect this because the unchanged_prefix will end with u+FFFF U+0008 // // k_212* and k_213* hit this case. std::u32string common_prefix(old_ctxt.begin(), ctxt_prefix.first); if (common_prefix.length() >= 2) { auto iter = common_prefix.rbegin(); if (*(iter++) == LDML_MARKER_CODE && *(iter++) == UC_SENTINEL) { // adjust the iterator so that the "U+FFFF U+0008" is not a part of the common prefix. ctxt_prefix.first -= 2; // backup the 'mismatch' point to before the FFFF ctxt_prefix.second -= 2; // backup the 'mismatch' point to before the FFFF // Now, old_ctxtstr_changed and new_ctxtstr_changed will start with U+FFFF U+0008 … } } /** The part of the old string to be removed */ std::u32string old_ctxtstr_changed(ctxt_prefix.first, old_ctxt.end()); /** The new context to be added */ std::u32string new_ctxtstr_changed(ctxt_prefix.second, new_ctxt.end()); // FIRST drop the old suffix. Note: this mutates old_ctxtstr_changed. // see remove_text() docs, this PUSHes actions, POPs context items, and TRIMS the string. remove_text(old_ctxtstr_changed, old_ctxtstr_changed.length()); assert(old_ctxtstr_changed.length() == 0); // old_ctxtstr_changed is now empty because it's been removed. // context is "aa" in the above example. // THEN add the new suffix, "CCC" in the above example emit_text(new_ctxtstr_changed); // context is now "aaCCC" } void ldml_event_state::remove_text(std::u32string &str, size_t length) { // str is the string to remove, so it should be at least as long as length assert(length <= str.length()); while (length > 0 && !state->context().empty()) { #ifndef NDEBUG /** last char of context */ km_core_usv lastCtx = str.back(); #endif auto lastStateCtx = state->context().back(); uint8_t type = lastStateCtx.type; assert(type == KM_CORE_BT_CHAR || type == KM_CORE_BT_MARKER); if (type == KM_CORE_BT_CHAR) { // single char, drop it length--; assert(lastStateCtx.character == lastCtx); str.pop_back(); // Cause prior char to be removed actions.code_points_to_delete++; } else if (type == KM_CORE_BT_MARKER) { assert(length >= 3); // #3 - the marker. assert(lastCtx == lastStateCtx.marker); str.pop_back(); length--; // #2 - the code assert(str.back() == LDML_MARKER_CODE); str.pop_back(); length--; // #1 - the sentinel assert(str.back() == UC_SENTINEL); str.pop_back(); // Nothing in actions length--; } state->context().pop_back(); } assert(length == 0); } void ldml_event_state::emit_text(const std::u16string &str) { const std::u32string str32 = kmx::u16string_to_u32string(str); emit_text(str32); } void ldml_event_state::emit_text(const std::u32string &str) { for (auto it = str.begin(); it < str.end(); it++) { const auto ch = *it; // If we are at the start of a sequence: if (ch == LDML_UC_SENTINEL) { it++; // consume LDML_UC_SENTINEL // TODO-LDML: Might assert if a malformed sequence is included- "should not happen"? assert(it < str.end()); // verify that the next char is LDML_MARKER_CODE assert(*it == LDML_MARKER_CODE); it++; // consume LDML_MARKER_CODE assert(it < str.end()); const auto marker_no = *it; emit_marker(marker_no); } else { emit_text(ch); } } } void ldml_event_state::emit_text( km_core_usv ch) { // assert that no noncharacter gets through (which includes UC_SENTINEL) // markers will go through emit_marker() and not through this function. assert(km::core::kmx::Uni_IsValid(ch)); if(!km::core::kmx::Uni_IsValid(ch)) ch = 0xFFFD; // release build: replace invalid with substitution character state->context().push_character(ch); text.push_back(ch); } void ldml_event_state::emit_marker( KMX_DWORD marker_no) { assert(km::core::kmx::is_valid_marker(marker_no)); // No markers in the action struct! state->context().push_marker(marker_no); } void ldml_event_state::emit_passthrough_keystroke() { // assert we haven't already requested a keystroke assert(actions.emit_keystroke != KM_CORE_TRUE); actions.emit_keystroke = KM_CORE_TRUE; } size_t ldml_event_state::context_to_string(std::u32string &str, bool include_markers) { str.clear(); auto &cp = state->context(); size_t ctxlen = 0; // TODO-LDML: not used by callers? uint8_t last_type = KM_CORE_BT_UNKNOWN; for (auto c = cp.rbegin(); c != cp.rend(); c++, ctxlen++) { last_type = c->type; if (last_type == KM_CORE_BT_CHAR) { str.insert(0, 1, c->character); } else if (last_type == KM_CORE_BT_MARKER) { assert(km::core::kmx::is_valid_marker(c->marker)); if (include_markers) { ldml::prepend_marker(str, c->marker); } } else { break; } } return ctxlen; // consumed the entire context buffer. } static const km_core_option_item NULL_OPTIONS[] = {KM_CORE_OPTIONS_END}; ldml_event_state::ldml_event_state( km_core_state *s, km_core_virtual_key v, uint16_t m, uint8_t i, uint16_t e) { this->state = s; this->_vk = v; this->_modifier_state = m; this->_is_key_down = i; this->_event_flags = e; actions.persist_options = new km_core_option_item[1]; actions.persist_options[0] = NULL_OPTIONS[0]; // initialize actions clear(); } void ldml_event_state::commit() { // thi is only set for the duration of the next call. actions.output = text.c_str(); // current implementation copies actions.output and doesn't retain it state->set_actions(actions); // clean up actions.output = nullptr; // clear struct clear(); // commit. TODO: should this be necessary? #9999 state->actions().commit(); } void ldml_event_state::clear() { // reset text text.clear(); // reset state obj actions.output = nullptr; actions.code_points_to_delete = 0; actions.do_alert = KM_CORE_FALSE; actions.emit_keystroke = KM_CORE_FALSE; actions.new_caps_lock_state = KM_CORE_CAPS_UNCHANGED; } void ldml_event_state::context_clear() { // clear the context state->context().clear(); // clear our own state clear(); } ldml_event_state::~ldml_event_state() { delete [] actions.persist_options; } } // namespace core } // namespace km