diff --git a/HISTORY.md b/HISTORY.md index d008db3bb7..6cffd0589a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,15 @@ # Keyman Version History +## 18.0.156 alpha 2024-12-09 + +* fix(developer): remove platforms from kmc-generate LM readme (#12803) +* test(developer): add test for ERROR_DescriptionIsMissing to kmc-keyboard-info (#12804) +* chore(developer): verify bundled node version when building installer (#12806) +* fix(developer): support hint property in displaymap (#12807) +* fix(common/web): delete replaceExtension in types/src/util/file-types.ts (#12762) +* chore(developer): add some docs for language examples in kmp.json (#12805) +* fix(core): implement ldml_processor::get_key_list() (#12644) + ## 18.0.155 alpha 2024-12-07 * chore(android,windows): Update crowdin for Czech (#12792) diff --git a/VERSION.md b/VERSION.md index d8dc6af9c7..4f4515d945 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -18.0.156 \ No newline at end of file +18.0.157 \ No newline at end of file diff --git a/core/include/keyman/keyman_core_api_vkeys.h b/core/include/keyman/keyman_core_api_vkeys.h index 7e4a6d181a..7e65d8ad91 100644 --- a/core/include/keyman/keyman_core_api_vkeys.h +++ b/core/include/keyman/keyman_core_api_vkeys.h @@ -16,6 +16,7 @@ #pragma once enum km_core_modifier_state { + KM_CORE_MODIFIER_NONE = 0, KM_CORE_MODIFIER_LCTRL = 1 << 0, KM_CORE_MODIFIER_RCTRL = 1 << 1, KM_CORE_MODIFIER_LALT = 1 << 2, diff --git a/core/src/kmx/kmx_plus.h b/core/src/kmx/kmx_plus.h index a8e5c1f0e4..e3a510b76d 100644 --- a/core/src/kmx/kmx_plus.h +++ b/core/src/kmx/kmx_plus.h @@ -455,7 +455,13 @@ public: bool setLayr(const COMP_KMXPLUS_LAYR *newLayr); bool valid() const; + /** + * @param list index from 0 to layr->listCount + */ const COMP_KMXPLUS_LAYR_LIST *getList(KMX_DWORD list) const; + /** + * @param entry index value: COMP_KMXPLUS_LAYR_LIST.layer but less than COMP_KMXPLUS_LAYR_LIST.layer+COMP_KMXPLUS_LAYR_LIST.count + */ const COMP_KMXPLUS_LAYR_ENTRY *getEntry(KMX_DWORD entry) const; const COMP_KMXPLUS_LAYR_ROW *getRow(KMX_DWORD row) const; const COMP_KMXPLUS_LAYR_KEY *getKey(KMX_DWORD key) const; diff --git a/core/src/ldml/ldml_processor.cpp b/core/src/ldml/ldml_processor.cpp index 548c0d76b0..6f90c1ad48 100644 --- a/core/src/ldml/ldml_processor.cpp +++ b/core/src/ldml/ldml_processor.cpp @@ -328,8 +328,7 @@ km_core_attr const & ldml_processor::attributes() const { } km_core_keyboard_key * ldml_processor::get_key_list() const { - km_core_keyboard_key* key_list = new km_core_keyboard_key(KM_CORE_KEYBOARD_KEY_LIST_END); - return key_list; + return keys.get_key_list(); } km_core_keyboard_imx * ldml_processor::get_imx_list() const { diff --git a/core/src/ldml/ldml_processor.hpp b/core/src/ldml/ldml_processor.hpp index 82f59292c3..6bf8a8fa31 100644 --- a/core/src/ldml/ldml_processor.hpp +++ b/core/src/ldml/ldml_processor.hpp @@ -34,10 +34,6 @@ class ldml_processor : public abstract_processor { const std::vector & data ); - static bool is_kmxplus_file( - const std::vector & data - ); - km_core_status process_event( km_core_state *state, diff --git a/core/src/ldml/ldml_vkeys.cpp b/core/src/ldml/ldml_vkeys.cpp index f79f82141f..dc3f22f18b 100644 --- a/core/src/ldml/ldml_vkeys.cpp +++ b/core/src/ldml/ldml_vkeys.cpp @@ -8,6 +8,8 @@ #include "ldml_vkeys.hpp" #include "kmx_file.h" #include +#include +#include namespace km { namespace core { @@ -22,6 +24,114 @@ vkeys::add(km_core_virtual_key vk, km_core_ldml_modifier_state modifier_state, s const vkey_id id(vk, modifier_state); // assign the string vkey_to_string[id] = output; + // includes all keys - including gaps. + all_vkeys.insert(id); +} + +km_core_keyboard_key * +vkeys::get_key_list() const { + // prescan to find out which modifier flags are used + + std::size_t other_key_count = 0; // number of 'OTHER' keys, which will need to expand to all of the other_state set ( so other_state.size()) + + std::set all_modifiers; + for (const auto &k : all_vkeys) { + const auto mod = k.second; + if (mod == LDML_KEYS_MOD_OTHER) { + other_key_count++; + } + all_modifiers.insert(mod); + } + std::set other_state; + + // Alt + if (all_modifiers.count(LDML_KEYS_MOD_ALT) == 0 && all_modifiers.count(LDML_KEYS_MOD_ALTL) == 0 && all_modifiers.count(LDML_KEYS_MOD_ALTR) == 0) { + // no ALT keys were seen, so OTHER includes ALT + other_state.insert(KM_CORE_MODIFIER_ALT); + } else if(all_modifiers.count(LDML_KEYS_MOD_ALTL) == 0) { + other_state.insert(KM_CORE_MODIFIER_RALT); + } else if(all_modifiers.count(LDML_KEYS_MOD_ALTR) == 0) { + other_state.insert(KM_CORE_MODIFIER_LALT); + } + + // ctrl + if (all_modifiers.count(LDML_KEYS_MOD_CTRL) == 0 && all_modifiers.count(LDML_KEYS_MOD_CTRLL) == 0 && all_modifiers.count(LDML_KEYS_MOD_CTRLR) == 0) { + // no CTRL keys were seen, so OTHER includes CTRL + other_state.insert(KM_CORE_MODIFIER_CTRL); + } else if(all_modifiers.count(LDML_KEYS_MOD_CTRLL) == 0) { + other_state.insert(KM_CORE_MODIFIER_RCTRL); + } else if(all_modifiers.count(LDML_KEYS_MOD_CTRLR) == 0) { + other_state.insert(KM_CORE_MODIFIER_LCTRL); + } + + // shift + if (all_modifiers.count(LDML_KEYS_MOD_SHIFT) == 0) { + other_state.insert(KM_CORE_MODIFIER_SHIFT); + } + + // caps + if (all_modifiers.count(LDML_KEYS_MOD_CAPS) == 0) { + other_state.insert(KM_CORE_MODIFIER_CAPS); + } + + // none- it's possible there is no 'none' layer + if (all_modifiers.count(LDML_KEYS_MOD_NONE) == 0) { + other_state.insert(KM_CORE_MODIFIER_NONE); + } + + // We need ALL combinations of the other_state, except for 'all off'. + // The number of additions will be (2**(other_state.size())-1 + // Also, since the LDML_KEYS_MOD_OTHER modifier is excluded, we + // will need to subtract 1 when calculating new_list_size + const std::size_t other_expanded_count = (1 << other_state.size()) - 1; + + std::vector other_expanded_mods(other_expanded_count); + + // populate the expanded list. + // we start at 1 because 0 is "all bits off" (00000b) + for (std::size_t expansion = 1; expansion <= other_expanded_count; expansion++) { + uint32_t &expanded_mod = other_expanded_mods.at(expansion-1) = KM_CORE_MODIFIER_NONE; + std::size_t bit_mask = 1; + for (const auto mod : other_state) { + // Check if this modifier is on in this iteration of the expansion + // bit_mask will be 2^0 … 2^(other_state().size()-1) + if (bit_mask & expansion) { + // do we include this entry? check if this bit is on + expanded_mod |= mod; + } + // shift the bitmask over + assert(expanded_mod <= KM_CORE_MODIFIER_MASK_CAPS); + bit_mask <<= 1; + } + } + + const std::size_t new_list_size = all_vkeys.size() // original size + + (other_key_count * (other_expanded_count - 1))// number of additional entries needed + + 1; // terminator + km_core_keyboard_key *list = new km_core_keyboard_key[new_list_size]; + std::size_t n = 0; + for (const auto &k : all_vkeys) { + const auto vkey = k.first; + const auto mod = k.second; + + if (mod == LDML_KEYS_MOD_OTHER) { + // expand to all of other_state + for (const auto expanded_mod : other_expanded_mods) { + list[n].key = vkey; + list[n++].modifier_flag = expanded_mod; + assert(n <= new_list_size); + } + } else { + assert(mod <= KM_CORE_MODIFIER_MASK_CAPS); // that no LDMLisms escape + list[n].key = vkey; + list[n++].modifier_flag = mod; + assert(n <= new_list_size); + } + } + // add the list terminator + list[n++] = KM_CORE_KEYBOARD_KEY_LIST_END; + assert(n == new_list_size); + return list; } static const uint16_t BOTH_ALT = LALTFLAG | RALTFLAG; diff --git a/core/src/ldml/ldml_vkeys.hpp b/core/src/ldml/ldml_vkeys.hpp index 6a54d77065..916d805e2c 100644 --- a/core/src/ldml/ldml_vkeys.hpp +++ b/core/src/ldml/ldml_vkeys.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include "keyman_core.h" @@ -37,6 +38,7 @@ typedef std::pair vkey_id; class vkeys { private: std::map vkey_to_string; + std::set all_vkeys; public: vkeys(); @@ -53,6 +55,11 @@ public: std::u16string lookup(km_core_virtual_key vk, uint16_t modifier_state, bool &found) const; + /** + * For implementing ldml_processor::get_key_list() + */ + km_core_keyboard_key* get_key_list() const; + private: /** * Non-recursive internal lookup of a specific ID diff --git a/core/tests/unit/ldml/keyboards/k_004_tinyshift.xml b/core/tests/unit/ldml/keyboards/k_004_tinyshift.xml index 4346fa801b..2d152867a7 100644 --- a/core/tests/unit/ldml/keyboards/k_004_tinyshift.xml +++ b/core/tests/unit/ldml/keyboards/k_004_tinyshift.xml @@ -2,8 +2,9 @@ @@ -23,5 +24,11 @@ + + + + + + diff --git a/core/tests/unit/ldml/ldml.cpp b/core/tests/unit/ldml/ldml.cpp index 78e0aaceba..f0b1eac4fe 100644 --- a/core/tests/unit/ldml/ldml.cpp +++ b/core/tests/unit/ldml/ldml.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include "path.hpp" #include "state.hpp" @@ -268,6 +269,63 @@ verify_context(std::u16string &text_store, km_core_state *&test_state, std::vect delete[] buf; } + +/** + * @param actual the list from get_key_list() + * @param expected optional list with keys to check, can be empty - not exhaistive + * @returns true if passing + */ +bool +verify_key_list(std::set &actual, std::set &expected) { + bool equals = true; + // error if any bad modifier keys + for(const auto &akey : actual) { + if (akey.modifier_state > KM_CORE_MODIFIER_MASK_CAPS) { + equals = false; + std::u16string dump = convert(akey.dump()); // akey.dump() + std::wcout << console_color::fg(console_color::BRIGHT_RED) << "- FAIL - key_map had key with bad modifier " << akey.modifier_state << ": " << dump << console_color::reset() << std::endl; + } + } + // error if any expected keys missing (note expected may be empty) + for(const auto &ekey : expected) { + if (actual.count(ekey) == 0) { + equals = false; + std::u16string dump = convert(ekey.dump()); // akey.dump() + std::wcout << console_color::fg(console_color::BRIGHT_RED) << "- FAIL - key_map had missing key " << dump << console_color::reset() << std::endl; + } + } + if (equals) { + std::wcout << console_color::fg(console_color::GREEN) << " " << actual.size() << " vkeys OK, verified " << expected.size() << console_color::reset() << std::endl; + } + return equals; +} + +/** + * @param actual_list the list from get_key_list() + * @param keylist optional string with keys to check, can be empty + * @param test the LDML test source, for additional data + * @returns true if passing + */ +bool +verify_key_list(const km_core_keyboard_key *actual_list, const std::u16string &expected_list, const km::tests::LdmlTestSource &test) { + std::set actual, expected; + // convert actual list + while (actual_list != nullptr && !(actual_list->key == 0 && actual_list->modifier_flag == 0)) { + km::tests::key_event k(actual_list->key, (uint16_t)actual_list->modifier_flag); + actual.insert(k); + actual_list++; // advance pointer + } + // parse the expected list + std::string keylist = convert(expected_list); + while (!keylist.empty() && keylist[0] == '[') { + const km::tests::key_event k = km::tests::LdmlEmbeddedTestSource::parse_next_key(keylist); + if (!k.empty()) { + expected.emplace(k); + } + } + return verify_key_list(actual, expected); +} + int run_test(const km::core::path &source, const km::core::path &compiled, km::tests::LdmlTestSource& test_source) { km_core_keyboard * test_kb = nullptr; @@ -374,6 +432,17 @@ run_test(const km::core::path &source, const km::core::path &compiled, km::tests errorLine = __LINE__; } } break; + case km::tests::LDML_ACTION_CHECK_KEYLIST: { + std::cout << "- checking keylist" << std::endl; + // get keylist from kbd + const km_core_keyboard_key* actual_list = test_kb->get_key_list(); + if (!verify_key_list(actual_list, action.string, test_source)) { + errorLine = __LINE__; + } else { + std::cout << " .. passes." << std::endl; + } + delete [] actual_list; + } break; case km::tests::LDML_ACTION_FAIL: { // test requested failure std::wcout << console_color::fg(console_color::BRIGHT_RED) << "- FAIL: " << action.string << console_color::reset() @@ -424,7 +493,7 @@ int run_all_tests(const km::core::path &source, const km::core::path &compiled, std::vector failures; // track failures for summary - int embedded_result = embedded_test_source.load_source(source); + int embedded_result = embedded_test_source.load_source(source, compiled); if (!filter.empty()) { // Always skip the embedded test if there's a filter. diff --git a/core/tests/unit/ldml/ldml_test_source.cpp b/core/tests/unit/ldml/ldml_test_source.cpp index 94e10e7ac4..76b7a34a4a 100644 --- a/core/tests/unit/ldml/ldml_test_source.cpp +++ b/core/tests/unit/ldml/ldml_test_source.cpp @@ -127,6 +127,50 @@ bool LdmlTestSource::get_expected_beep() const { return false; } +int LdmlTestSource::load_kmx_plus(const km::core::path &compiled) { + // check and load the KMX (yes, once again) + rawdata = km::tests::load_kmx_file(compiled); + if(!km::core::ldml_processor::is_handled(rawdata)) { + std::cerr << "Reading KMX for test purposes failed: " << compiled << std::endl; + return __LINE__; + } + + auto comp_keyboard = (const km::core::kmx::COMP_KEYBOARD*)rawdata.data(); + // initialize the kmxplus object with our copy + kmxplus.reset(new km::core::kmx::kmx_plus(comp_keyboard, rawdata.size())); + + if (!kmxplus->is_valid()) { + std::cerr << "kmx_plus invalid" << std::endl; + return __LINE__; + } + + if (!kmxplus->key2Helper.valid()) { + std::cerr << "kmx_plus invalid" << std::endl; + return __LINE__; + } + + return 0; // success +} + +bool LdmlTestSource::get_vkey_table(std::set &fillin) const { + if (!kmxplus || !kmxplus->is_valid()) { + return false; // fail + } + + // just dump the kmap table + for (KMX_DWORD kmapIdx = 0; kmapIdx < kmxplus->key2->kmapCount; kmapIdx++) { + const km::core::kmx::COMP_KMXPLUS_KEYS_KMAP *kmap = kmxplus->key2Helper.getKmap(kmapIdx); + if (kmap == nullptr) { + return false; + } + if (kmap->vkey > 0xFF) { + continue; // synthetic key- skip + } + fillin.insert(key_event(kmap->vkey, kmap->mod)); + } + return true; +} + // String trim functions from https://stackoverflow.com/a/217605/1836776 // trim from start (in place) static inline void @@ -264,12 +308,13 @@ LdmlEmbeddedTestSource::is_token(const std::string token, std::string &line) { } int -LdmlEmbeddedTestSource::load_source( const km::core::path &path ) { +LdmlEmbeddedTestSource::load_source( const km::core::path &path, const km::core::path &compiled ) { const std::string s_keys = "@@keys: "; const std::string s_expected = "@@expected: "; const std::string s_context = "@@context: "; const std::string s_capsLock = "@@capsLock: "; const std::string s_expecterror = "@@expect-error: "; + const std::string s_keylist = "@@keylist: "; // Parse out the header statements in file.kmn that tell us (a) environment, (b) key sequence, (c) start context, (d) expected // result @@ -301,6 +346,10 @@ LdmlEmbeddedTestSource::load_source( const km::core::path &path ) { context = parse_source_string(line); } else if (is_token(s_capsLock, line)) { set_caps_lock_on(parse_source_string(line).compare(u"1") == 0); + } else if (is_token(s_keylist, line)) { + set_keylist(line); + } else if (line[0] == '@') { + std::cerr << path << " warning, unknown @-command " << line << std::endl; } } @@ -316,7 +365,13 @@ LdmlEmbeddedTestSource::load_source( const km::core::path &path ) { return __LINE__; } - return 0; + // and load KMX+ for keylist + if (!expected_error) { + // don't attempt to load KMX+ on expected error + return load_kmx_plus(compiled); + } else { + return 0; + } } km_core_status @@ -357,7 +412,7 @@ LdmlTestSource::char_to_event(char ch) { } uint16_t -LdmlTestSource::get_modifier(std::string const m) { +LdmlTestSource::get_modifier(std::string const &m) { for (int i = 0; km::core::kmx::s_modifier_names[i].name; i++) { if (m == km::core::kmx::s_modifier_names[i].name) { return km::core::kmx::s_modifier_names[i].modifier; @@ -366,6 +421,12 @@ LdmlTestSource::get_modifier(std::string const m) { return 0; } +std::string key_event::dump() const { + std::stringstream f; + f << "Key: {" << km::core::kmx::Debug_VirtualKey(vk) << ", " << km::core::kmx::Debug_ModifierName(modifier_state) << "}"; + return f.str(); +} + key_event LdmlEmbeddedTestSource::vkey_to_event(std::string const &vk_event) { // vkey format is MODIFIER MODIFIER K_NAME @@ -401,34 +462,37 @@ LdmlEmbeddedTestSource::vkey_to_event(std::string const &vk_event) { void LdmlEmbeddedTestSource::next_action(ldml_action &fillin) { - if (is_done || keys.empty()) { - // We were already done. return done. - fillin.type = LDML_ACTION_DONE; - return; + if (keys.empty()) { + // #3 we are almost done, let's run the key check + if (check_keylist) { + fillin.type = LDML_ACTION_CHECK_KEYLIST; + fillin.string = expected_keylist; // could be empty + check_keylist = false; + } else { + fillin.type = LDML_ACTION_DONE; + } } else if(keys[0].empty()) { + // #2. Then, when we finish a key set, we check the 'expected' at the end of it. // Got to the end of a key set. time to check fillin.type = LDML_ACTION_CHECK_EXPECTED; fillin.string = expected[0]; // copy expected expected.pop_front(); keys.pop_front(); - if (keys.empty()) { - is_done = true; // so we get DONE next time - } } else { + // #1 First, we process each key fillin.type = LDML_ACTION_KEY_EVENT; fillin.k = next_key(); } } - key_event LdmlEmbeddedTestSource::next_key() { // mutate this->keys - return next_key(keys[0]); + return parse_next_key(keys[0]); } key_event -LdmlEmbeddedTestSource::next_key(std::string &keys) { +LdmlEmbeddedTestSource::parse_next_key(std::string &keys) { // Parse the next element of the string, chop it off, and return it // mutates keys if (keys.length() == 0) @@ -453,10 +517,10 @@ LdmlEmbeddedTestSource::next_key(std::string &keys) { class LdmlJsonTestSource : public LdmlTestSource { public: - LdmlJsonTestSource(const std::string &path, km::core::kmx::kmx_plus *kmxplus); + LdmlJsonTestSource(const std::string &path); virtual ~LdmlJsonTestSource(); virtual const std::u16string &get_context(); - int load(const nlohmann::json &test); + int load(const nlohmann::json &test, const km::core::path &compiled); virtual void next_action(ldml_action &fillin); private: std::string path; @@ -467,14 +531,14 @@ private: * Which action are we on? */ std::size_t action_index = -1; - const km::core::kmx::kmx_plus *kmxplus; /** @return false if not found */ bool set_key_from_id(key_event& k, const std::u16string& id); bool loaded_context = false; + bool check_keys = true; }; -LdmlJsonTestSource::LdmlJsonTestSource(const std::string &path, km::core::kmx::kmx_plus *k) -:path(path), kmxplus(k) { +LdmlJsonTestSource::LdmlJsonTestSource(const std::string &path) +:path(path) { } @@ -519,6 +583,13 @@ bool LdmlJsonTestSource::set_key_from_id(key_event& k, const std::u16string& id) void LdmlJsonTestSource::next_action(ldml_action &fillin) { if ((action_index+1) >= data["/actions"_json_pointer].size()) { + // add check keylist + if (check_keys) { + fillin.type = LDML_ACTION_CHECK_KEYLIST; + fillin.string.clear(); + check_keys = false; + return; + } // at end, done fillin.type = LDML_ACTION_DONE; return; @@ -586,19 +657,21 @@ LdmlJsonTestSource::get_context() { return context; } -int LdmlJsonTestSource::load(const nlohmann::json &data) { +int LdmlJsonTestSource::load(const nlohmann::json &data, const km::core::path &compiled) { this->data = data; - // TODO-LDML: validate here? - return 0; + // TODO-LDML: validate JSON here? + + // load up the kmx_plus + return load_kmx_plus(compiled); } #if defined(HAVE_ICU4C) class LdmlJsonRepertoireTestSource : public LdmlTestSource { public: - LdmlJsonRepertoireTestSource(const std::string &path, km::core::kmx::kmx_plus *kmxplus); + LdmlJsonRepertoireTestSource(const std::string &path ); virtual ~LdmlJsonRepertoireTestSource(); virtual const std::u16string &get_context(); - int load(const nlohmann::json &test); + int load(const nlohmann::json &test, const km::core::path &compiled); virtual void next_action(ldml_action &fillin); private: std::string path; @@ -610,11 +683,10 @@ private: std::unique_ptr uset; std::unique_ptr iterator; bool need_check = false; // set this after each char - const km::core::kmx::kmx_plus *kmxplus; }; -LdmlJsonRepertoireTestSource::LdmlJsonRepertoireTestSource(const std::string &path, km::core::kmx::kmx_plus *k) -:path(path), kmxplus(k){ +LdmlJsonRepertoireTestSource::LdmlJsonRepertoireTestSource(const std::string &path) +:path(path) { } @@ -702,7 +774,7 @@ LdmlJsonRepertoireTestSource::get_context() { return context; // no context needed } -int LdmlJsonRepertoireTestSource::load(const nlohmann::json &data) { +int LdmlJsonRepertoireTestSource::load(const nlohmann::json &data, const km::core::path &compiled) { this->data = data; // TODO-LDML // Need an update to json.hpp to use contains() // if (data.contains("/type"_json_pointer)) { @@ -733,7 +805,7 @@ int LdmlJsonRepertoireTestSource::load(const nlohmann::json &data) { // } // #endif iterator = std::unique_ptr(new icu::UnicodeSetIterator(*uset)); - return 0; + return load_kmx_plus(compiled); } #endif // HAVE_ICU4C @@ -757,26 +829,6 @@ int LdmlJsonTestSourceFactory::load(const km::core::path &compiled, const km::co return __LINE__; // empty } - // check and load the KMX (yes, once again) - rawdata = km::tests::load_kmx_file(compiled); - if (!km::core::ldml_processor::is_handled(rawdata)) { - std::cerr << "Reading KMX for test purposes failed: " << compiled << std::endl; - return __LINE__; - } - - auto comp_keyboard = (const km::core::kmx::COMP_KEYBOARD*)rawdata.data(); - // initialize the kmxplus object with our copy - kmxplus.reset(new km::core::kmx::kmx_plus(comp_keyboard, rawdata.size())); - - if (!kmxplus->is_valid()) { - std::cerr << "kmx_plus invalid" << std::endl; - return __LINE__; - } - - if (!kmxplus->key2Helper.valid()) { - std::cerr << "kmx_plus invalid" << std::endl; - return __LINE__; - } auto conformsTo = data["/keyboardTest3/conformsTo"_json_pointer].get(); assert_or_return(std::string(LDML_CLDR_TEST_VERSION_LATEST) == conformsTo); @@ -799,8 +851,8 @@ int LdmlJsonTestSourceFactory::load(const km::core::path &compiled, const km::co test_path.append(info_name).append("/tests/").append(tests_name).append("/").append(test_name); // std::cout << "JSON: reading " << info_name << "/" << test_path << std::endl; - std::unique_ptr subtest(new LdmlJsonTestSource(test_path, kmxplus.get())); - assert_or_return(subtest->load(test) == 0); + std::unique_ptr subtest(new LdmlJsonTestSource(test_path)); + assert_or_return(subtest->load(test, compiled) == 0); test_map[test_path] = std::unique_ptr(subtest.release()); } } @@ -815,8 +867,8 @@ int LdmlJsonTestSourceFactory::load(const km::core::path &compiled, const km::co std::string test_path; test_path.append(info_name).append("/repertoire/").append(rep_name); - std::unique_ptr reptest(new LdmlJsonRepertoireTestSource(test_path, kmxplus.get())); - assert_or_return(reptest->load(rep) == 0); + std::unique_ptr reptest(new LdmlJsonRepertoireTestSource(test_path)); + assert_or_return(reptest->load(rep, compiled) == 0); test_map[test_path] = std::unique_ptr(reptest.release()); } diff --git a/core/tests/unit/ldml/ldml_test_source.hpp b/core/tests/unit/ldml/ldml_test_source.hpp index 8d2b05159a..4e511d43bf 100644 --- a/core/tests/unit/ldml/ldml_test_source.hpp +++ b/core/tests/unit/ldml/ldml_test_source.hpp @@ -4,6 +4,7 @@ #include "path.hpp" #include +#include #include #include "kmx/kmx_plus.h" @@ -17,6 +18,34 @@ namespace tests { struct key_event { km_core_virtual_key vk; uint16_t modifier_state; + public: + key_event() : vk(0), modifier_state(0) { + } + key_event(km_core_virtual_key k, uint16_t m) :vk(k), modifier_state(m) { + } + std::string dump() const; + int compare(const key_event &other) const { + if (vk < other.vk) return -1; + if (vk > other.vk) return 1; + if (modifier_state < other.modifier_state) return -1; + if (modifier_state > other.modifier_state) return 1; + return 0; + } + + bool operator<(const key_event &other) const { + return compare(other) < 0; + } + bool operator>(const key_event &other) const { + return compare(other) > 0; + } + bool operator==(const key_event &other) const { + return compare(other) == 0; + } + /** true if unset (null) key */ + bool empty() const { + return vk == 0 && modifier_state == 0; + } + }; enum ldml_action_type { @@ -40,6 +69,10 @@ enum ldml_action_type { * expected text */ LDML_ACTION_CHECK_EXPECTED, + /** + * string - keylist to check + */ + LDML_ACTION_CHECK_KEYLIST, // TODO-LDML: gestures, etc? Depends on touch. /** @@ -80,10 +113,17 @@ public: virtual km_core_status get_expected_load_status(); virtual const std::u16string &get_context() = 0; virtual bool get_expected_beep() const; + /** + * fillin a list + * @param fillin key list to be filled in with all vkeys in the hardware map. + * @param modifier modifier to load key list for + * @returns false on load fail, true on OK + */ + bool get_vkey_table(std::set &fillin) const; // helper functions static key_event char_to_event(char ch); - static uint16_t get_modifier(std::string const m); + static uint16_t get_modifier(std::string const &m); static std::u16string parse_source_string(std::string const &s); static std::u16string parse_u8_source_string(std::string const &s); @@ -105,6 +145,12 @@ private: private: bool _caps_lock_on = false; +protected: + /** populate rawdata and kmxplus */ + int load_kmx_plus(const km::core::path &compiled); + // copy of the kbd data, for lookups + std::vector rawdata; + std::unique_ptr kmxplus; }; typedef std::map> JsonTestMap; @@ -123,9 +169,6 @@ class LdmlJsonTestSourceFactory { const JsonTestMap& get_tests() const; private: JsonTestMap test_map; - // copy of the kbd data, for lookups - std::vector rawdata; - std::unique_ptr kmxplus; }; @@ -137,7 +180,7 @@ public: /** * Load the test_source from comments in the .xml source */ - int load_source(const km::core::path &path); + int load_source(const km::core::path &path, const km::core::path &compiled); virtual km_core_status get_expected_load_status(); virtual const std::u16string &get_context(); @@ -146,10 +189,6 @@ public: virtual void next_action(ldml_action &fillin); private: - - bool is_token(const std::string token, std::string &line); - key_event vkey_to_event(std::string const &vk_event); - key_event next_key(std::string &keys); key_event next_key(); std::deque keys; @@ -158,6 +197,24 @@ private: bool expected_beep = false; bool expected_error = false; bool is_done = false; + /** did we check the keylist yet? */ + bool check_keylist = true; + /** set the expected keys in the keylist */ + void set_keylist(std::string const& s) { + expected_keylist = parse_source_string(s); + } + + std::u16string expected_keylist; + + /** returns false on fail and updates the message */ + bool handle_check_keylist(std::string &message) const; + +// utility + static key_event vkey_to_event(std::string const &vk_event); + static bool is_token(const std::string token, std::string &line); + +public: + static key_event parse_next_key(std::string &keys); }; } // namespace tests diff --git a/developer/docs/help/reference/file-types/metadata.md b/developer/docs/help/reference/file-types/metadata.md index 8a3e50deae..1a0d631820 100644 --- a/developer/docs/help/reference/file-types/metadata.md +++ b/developer/docs/help/reference/file-types/metadata.md @@ -243,6 +243,12 @@ The `Keyboard` object describes an individual keyboard in the Keyman package. A The filename of the font for the OSK +`examples` + +: `Array` + + An array of [`Example`](#obj-example) objects linked to the keyboard. + ### The Language object The `Language` object describes the language that can be typed with the keyboard @@ -259,6 +265,51 @@ The `Language` object describes the language that can be typed with the keyboard [BCP 47 language code](../bcp-47) +### The Example object + +The `Example` object describes a keying sequence example text for the keyboard. +This can be the easiest way for a new user to start using a keyboard, and is +particularly helpful when the keyboard makes use of keying sequences that may +not be immediately obvious. + +`id` + +: `string` + + The BCP 47 language code for the example. + +`keys` + +: `string` + + The key sequence to type the example. The key sequence must list each key + combination, separated by space. The actual text for each key is reasonably + arbitrary, to allow you to provide examples for touch keyboards as well as + desktop keyboards. There are three special kinds of key strings: + * Modifier keys may be specified with the `+` character, e.g. `shift+e` or + `right-alt+k`. Use lower case keys. The suggested standard modifiers are: + `shift`, `ctrl`, `alt`, `left-alt`, `right-alt`, `left-ctrl`, `right-ctrl`, + and `option` (mac). + * The space key itself may be specified with `space`. + * To avoid confusion with modifier keys, the + key can be specified + with `plus`. + + For example, the key sequence x, j, m, + Shift+e, r may be specified as + `x j m shift+e r` or `x j m E r`. + +`text` + +: `string` + + The expected output when the `keys` are typed + +`note` + +: `string` + + A brief explanation of the example, e.g. "Name of language" + ### The LexicalModel object The `LexicalModel` object describes an individual model in the Keyman package. A package cannot contain both lexical models and keyboards.