diff --git a/HISTORY.md b/HISTORY.md index b831dadee8a..d5ad1a589fd 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,11 @@ # Keyman Version History +## 18.0.153 alpha 2024-12-05 + +* feat(developer,common): verify normalization of strings (#12748) +* chore(core): Add link to Keyman Glossary (#12774) +* test(common/web/types): unit tests for file-types (#12716) + ## 18.0.152 alpha 2024-12-04 * refactor(mac): pass kmx data blob to keyman core instead of file path (#12760) diff --git a/VERSION.md b/VERSION.md index 6269a6496fb..153289471d0 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -18.0.153 \ No newline at end of file +18.0.154 \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/values-it-rIT/strings.xml b/android/KMAPro/kMAPro/src/main/res/values-it-rIT/strings.xml index a475ae37255..de474402c06 100644 --- a/android/KMAPro/kMAPro/src/main/res/values-it-rIT/strings.xml +++ b/android/KMAPro/kMAPro/src/main/res/values-it-rIT/strings.xml @@ -68,7 +68,7 @@ Regola altezza tastiera - Adjust longpress delay + Regola ritardo pressione prolungata Didascalia barra spaziatrice @@ -89,11 +89,11 @@ Non mostrare la didascalia sulla barra spaziatrice Vibra durante la digitazione - + Mostra sempre il banner - + Da attuare - + Quando è spento, mostrato solo quando il testo predittivo è abilitato Consenti l\'invio di segnalazioni di crash attraverso la rete @@ -126,13 +126,13 @@ Ripristina impostazioni predefinite - Delay Time: %1$.1f seconds + Tempo ritardo: %1$.1f secondi - Delay time longer + Tempo ritardo maggiore - Delay time shorter + Tempo ritardo minore - Longpress delay time slider + Cursore tempo ritardo pressione prolungata Cerca o digita URL diff --git a/android/KMEA/app/src/main/res/values-it-rIT/strings.xml b/android/KMEA/app/src/main/res/values-it-rIT/strings.xml index da1e3531a2b..7dce8252b27 100644 --- a/android/KMEA/app/src/main/res/values-it-rIT/strings.xml +++ b/android/KMEA/app/src/main/res/values-it-rIT/strings.xml @@ -133,10 +133,15 @@ %1$s tastiera installata Tastiera eliminata - + Abilita correzioni - + Abilita previsioni + + Disabilita suggerimenti + Solo previsioni + Previsioni con correzioni + Previsioni con correzioni automatiche Dizionario diff --git a/common/web/types/tests/util/file-types.tests.ts b/common/web/types/tests/util/file-types.tests.ts new file mode 100644 index 00000000000..3f4d6870918 --- /dev/null +++ b/common/web/types/tests/util/file-types.tests.ts @@ -0,0 +1,234 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Dr Mark C. Sinclair on 2024-11-29 + * + * Test code for file-types.ts + */ + +import 'mocha'; +import { assert } from 'chai'; +import { + ALL, + ALL_SOURCE, + ALL_BINARY, + Binary, + fromFilename, + removeExtension, + sourceOrBinaryTypeFromFilename, + sourceTypeFromFilename, + binaryTypeFromFilename, + filenameIs, + replaceExtension, +} from '../../src/util/file-types.js'; + +describe('Test of File-Types', () => { + describe('Test of fromFilename()', () => { + it('can extract Source file extension', () => { + ALL_SOURCE.forEach((ext) => { + const filename = `file${ext}`; + const actual = fromFilename(filename); + assert.deepEqual(actual, ext); + }); + }); + it('can extract Binary file extension', () => { + ALL_BINARY.forEach((ext) => { + const filename = `file${ext}`; + const actual = fromFilename(filename); + assert.deepEqual(actual, ext); + }); + }); + it('can extract unmatched file extension', () => { + const ext = ".cpp"; + assert.isFalse((Object.values(ALL_SOURCE) as string[]).includes(ext)); + const filename = `file${ext}`; + const actual = fromFilename(filename); + assert.deepEqual(actual, ext); + }); + it('returns empty string for no file extension', () => { + const filename = `file`; + const actual = fromFilename(filename); + assert.deepEqual(actual, ""); + }); + it('can extract upper case file extension', () => { + const ext = ALL_SOURCE[0]; + const upperCaseExt = ext.toUpperCase(); + const filename = `file${upperCaseExt}`; + const actual = fromFilename(filename); + assert.deepEqual(actual, ext); + }); + }); + describe('Test of removeExtension()', () => { + it('can remove Source file extension', () => { + ALL_SOURCE.forEach((ext) => { + const filename = `file${ext}`; + const actual = removeExtension(filename); + assert.deepEqual(actual, "file"); + }); + }); + it('can remove Binary file extension', () => { + ALL_BINARY.forEach((ext) => { + const filename = `file${ext}`; + const actual = removeExtension(filename); + assert.deepEqual(actual, "file"); + }); + }); + it('can handle no file extension', () => { + const filename = removeExtension("file"); + assert.deepEqual(filename, "file"); + }); + }); + describe('Test of sourceOrBinaryTypeFromFilename()', () => { + it('can extract Source file extension', () => { + ALL_SOURCE.forEach((ext) => { + const filename = `file${ext}`; + const actual = sourceOrBinaryTypeFromFilename(filename); + assert.deepEqual(actual, ext); + }); + }); + it('can extract Binary file extension', () => { + ALL_BINARY.forEach((ext) => { + const filename = `file${ext}`; + const actual = sourceOrBinaryTypeFromFilename(filename); + assert.deepEqual(actual, ext); + }); + }); + it('returns null for unmatched file extension', () => { + const ext = ".cpp"; + assert.isFalse((Object.values(ALL) as string[]).includes(ext)); + const filename = `file${ext}`; + const actual = sourceOrBinaryTypeFromFilename(filename); + assert.isNull(actual); + }); + it('can extract upper case file extension', () => { + const ext = ALL[0]; + const upperCaseExt = ext.toUpperCase(); + const filename = `file${upperCaseExt}`; + const actual = sourceOrBinaryTypeFromFilename(filename); + assert.deepEqual(actual, ext); + }); + }); + describe('Test of sourceTypeFromFilename()', () => { + it('can extract Source file extension', () => { + ALL_SOURCE.forEach((ext) => { + const filename = `file${ext}`; + const actual = sourceTypeFromFilename(filename); + assert.deepEqual(actual, ext); + }); + }); + it('returns null for a Binary file extension', () => { + ALL_BINARY.forEach((ext) => { + const filename = `file${ext}`; + const actual = sourceTypeFromFilename(filename); + assert.isNull(actual); + }); + }); + it('returns null for unmatched file extension', () => { + const ext = ".cpp"; + assert.isFalse((Object.values(ALL_SOURCE) as string[]).includes(ext)); + const filename = `file${ext}`; + const actual = sourceTypeFromFilename(filename); + assert.isNull(actual); + }); + it('can extract upper case file extension', () => { + const ext = ALL_SOURCE[0]; + const upperCaseExt = ext.toUpperCase(); + const filename = `file${upperCaseExt}`; + const actual = sourceTypeFromFilename(filename); + assert.deepEqual(actual, ext); + }); + }); + describe('Test of binaryTypeFromFilename()', () => { + it('returns null for a Source file extension', () => { + ALL_SOURCE.forEach((ext) => { + const filename = `file${ext}`; + const actual = binaryTypeFromFilename(filename); + assert.isNull(actual); + }); + }); + it('can extract Binary file extension', () => { + ALL_BINARY.forEach((ext) => { + const filename = `file${ext}`; + const actual = binaryTypeFromFilename(filename); + assert.deepEqual(actual, ext); + }); + }); + it('returns null for unmatched file extension', () => { + const ext = ".cpp"; + assert.isFalse((Object.values(ALL_BINARY) as string[]).includes(ext)); + const filename = `file${ext}`; + const actual = binaryTypeFromFilename(filename); + assert.isNull(actual); + }); + it('can extract upper case file extension', () => { + const ext = ALL_BINARY[0]; + const upperCaseExt = ext.toUpperCase(); + const filename = `file${upperCaseExt}`; + const actual = binaryTypeFromFilename(filename); + assert.deepEqual(actual, ext); + }); + }); + describe('Test of filenameIs()', () => { + it('can identify Source file extension', () => { + ALL_SOURCE.forEach((ext) => { + const filename = `file${ext}`; + const actual = filenameIs(filename, ext); + assert.isTrue(actual); + }); + }); + it('can identify Binary file extension', () => { + ALL_BINARY.forEach((ext) => { + const filename = `file${ext}`; + if (ext == Binary.Model) { // Special case for .model.js + const actual = filenameIs(filename, Binary.WebKeyboard); + assert.isFalse(actual); + } + const actual = filenameIs(filename, ext); + assert.isTrue(actual); + }); + }); + it('can identify upper case file extension', () => { + const ext = ALL[0]; + const upperCaseExt = ext.toUpperCase(); + const filename = `file${upperCaseExt}`; + const actual = filenameIs(filename, ext); + assert.isTrue(actual); + }); + }); + describe('Test of replaceExtension()', () => { + it('can replace an extension', () => { + const oldExt = ".cpp"; + const newExt = ".js"; + const oldFilename = `file${oldExt}`; + const newFilename = `file${newExt}`; + const actual = replaceExtension(oldFilename, oldExt, newExt); + assert.deepEqual(actual, newFilename); + }); + it('should return null for incorrect old extension (too short)', () => { + const oldExt = ".ts"; + const newExt = ".js"; + const oldFilename = `file.c`; + const actual = replaceExtension(oldFilename, oldExt, newExt); + assert.isNull(actual); + }); + it('should return null for incorrect old extension (too long)', () => { + const oldExt = ".ts"; + const newExt = ".js"; + const oldFilename = `file.cpp`; + const actual = replaceExtension(oldFilename, oldExt, newExt); + assert.isNull(actual); + }); + // it('should return null for null old extension', () => { + // const newExt = ".js"; + // const oldFilename = `file.ts`; + // const actual = replaceExtension(oldFilename, null, newExt); + // assert.isNull(actual); + // }); + // it('should return null for null new extension', () => { + // const oldExt = ".ts"; + // const oldFilename = `file.ts`; + // const actual = replaceExtension(oldFilename, oldExt, null); + // assert.isNull(actual); + // }); + }); +}); diff --git a/core/docs/api/background.md b/core/docs/api/background.md index b0d09da10f7..d4c638f0ce7 100644 --- a/core/docs/api/background.md +++ b/core/docs/api/background.md @@ -396,7 +396,7 @@ typedef enum { KM_CORE_FALSE = 0, KM_CORE_TRUE = 1 } km_core_bool; [km_core_keyboard_attrs]: keyboards#km_core_keyboard_attrs "km_core_keyboard_attrs struct" [km_core_keyboard_key]: keyboards#km_core_keyboard_key "km_core_keyboard_key struct" [km_core_keyboard_imx]: keyboards#km_core_keyboard_imx "km_core_keyboard_imx struct" -[km_core_keyboard_load]: keyboards#km_core_keyboard_load "km_core_keyboard_load function" +[km_core_keyboard_load_from_blob]: keyboards#km_core_keyboard_load_from_blob "km_core_keyboard_load_from_blob function" [km_core_keyboard_dispose]: keyboards#km_core_keyboard_dispose "km_core_keyboard_dispose function" [km_core_keyboard_get_attrs]: keyboards#km_core_keyboard_get_attrs "km_core_keyboard_get_attrs function" [km_core_keyboard_get_key_list]: keyboards#km_core_keyboard_get_key_list "km_core_keyboard_get_key_list function" diff --git a/core/docs/api/changes.md b/core/docs/api/changes.md index ab8a2e287a1..e5736494abc 100644 --- a/core/docs/api/changes.md +++ b/core/docs/api/changes.md @@ -43,7 +43,7 @@ title: Changes - Keyman Core API [km_core_keyboard_attrs]: keyboards#km_core_keyboard_attrs "km_core_keyboard_attrs struct" [km_core_keyboard_key]: keyboards#km_core_keyboard_key "km_core_keyboard_key struct" [km_core_keyboard_imx]: keyboards#km_core_keyboard_imx "km_core_keyboard_imx struct" -[km_core_keyboard_load]: keyboards#km_core_keyboard_load "km_core_keyboard_load function" +[km_core_keyboard_load_from_blob]: keyboards#km_core_keyboard_load_from_blob "km_core_keyboard_load_from_blob function" [km_core_keyboard_dispose]: keyboards#km_core_keyboard_dispose "km_core_keyboard_dispose function" [km_core_keyboard_get_attrs]: keyboards#km_core_keyboard_get_attrs "km_core_keyboard_get_attrs function" [km_core_keyboard_get_key_list]: keyboards#km_core_keyboard_get_key_list "km_core_keyboard_get_key_list function" diff --git a/core/docs/api/index.md b/core/docs/api/index.md index c33246f7f44..94bb517654c 100644 --- a/core/docs/api/index.md +++ b/core/docs/api/index.md @@ -104,7 +104,7 @@ Caps Lock. [km_core_keyboard_attrs]: keyboards#km_core_keyboard_attrs "km_core_keyboard_attrs struct" [km_core_keyboard_key]: keyboards#km_core_keyboard_key "km_core_keyboard_key struct" [km_core_keyboard_imx]: keyboards#km_core_keyboard_imx "km_core_keyboard_imx struct" -[km_core_keyboard_load]: keyboards#km_core_keyboard_load "km_core_keyboard_load function" +[km_core_keyboard_load_from_blob]: keyboards#km_core_keyboard_load_from_blob "km_core_keyboard_load_from_blob function" [km_core_keyboard_dispose]: keyboards#km_core_keyboard_dispose "km_core_keyboard_dispose function" [km_core_keyboard_get_attrs]: keyboards#km_core_keyboard_get_attrs "km_core_keyboard_get_attrs function" [km_core_keyboard_get_key_list]: keyboards#km_core_keyboard_get_key_list "km_core_keyboard_get_key_list function" diff --git a/core/docs/api/keyboards.md b/core/docs/api/keyboards.md index b9312a78d94..3439e20722b 100644 --- a/core/docs/api/keyboards.md +++ b/core/docs/api/keyboards.md @@ -102,28 +102,35 @@ typedef struct { ------------------------------------------------------------------------------- -# km_core_keyboard_load() {#km_core_keyboard_load} +# km_core_keyboard_load_from_blob() {#km_core_keyboard_load_from_blob} ## Description -Parse and load keyboard from the supplied path and a pointer to the loaded keyboard -into the out paramter. +Parse and load a keyboard from the supplied blob and return a pointer to the +loaded keyboard in the out parameter. ## Specification ```c KMN_API km_core_status -km_core_keyboard_load(km_core_path_name kb_path, - km_core_keyboard **keyboard); +km_core_keyboard_load_from_blob(const km_core_path_name kb_name, + const void* blob, + const size_t blob_size, + km_core_keyboard** keyboard); ``` ## Parameters -`kb_path` -: On Windows, a UTF-16 string; on other platforms, a C string: - contains a valid path to the keyboard file. +`kb_name` +: a string with the name of the keyboard. + +`blob` +: a byte array containing the content of a KMX/KMX+ file. + +`blob_size` +: a size_t variable with the size of the blob in bytes. `keyboard` : A pointer to result variable: A pointer to the opaque keyboard @@ -154,7 +161,7 @@ km_core_keyboard_load(km_core_path_name kb_path, ## Description Free the allocated memory belonging to an opaque keyboard object previously -returned by [km_core_keyboard_load]. +returned by [km_core_keyboard_load_from_blob]. ## Specification @@ -637,7 +644,7 @@ km_core_state_to_json(km_core_state const *state, [km_core_keyboard_attrs]: keyboards#km_core_keyboard_attrs "km_core_keyboard_attrs struct" [km_core_keyboard_key]: keyboards#km_core_keyboard_key "km_core_keyboard_key struct" [km_core_keyboard_imx]: keyboards#km_core_keyboard_imx "km_core_keyboard_imx struct" -[km_core_keyboard_load]: keyboards#km_core_keyboard_load "km_core_keyboard_load function" +[km_core_keyboard_load_from_blob]: keyboards#km_core_keyboard_load_from_blob "km_core_keyboard_load_from_blob function" [km_core_keyboard_dispose]: keyboards#km_core_keyboard_dispose "km_core_keyboard_dispose function" [km_core_keyboard_get_attrs]: keyboards#km_core_keyboard_get_attrs "km_core_keyboard_get_attrs function" [km_core_keyboard_get_key_list]: keyboards#km_core_keyboard_get_key_list "km_core_keyboard_get_key_list function" diff --git a/core/docs/api/options.md b/core/docs/api/options.md index e5a171d6fea..41d5fe28c9a 100644 --- a/core/docs/api/options.md +++ b/core/docs/api/options.md @@ -263,7 +263,7 @@ km_core_state_options_to_json(km_core_state const *state, [km_core_keyboard_attrs]: keyboards#km_core_keyboard_attrs "km_core_keyboard_attrs struct" [km_core_keyboard_key]: keyboards#km_core_keyboard_key "km_core_keyboard_key struct" [km_core_keyboard_imx]: keyboards#km_core_keyboard_imx "km_core_keyboard_imx struct" -[km_core_keyboard_load]: keyboards#km_core_keyboard_load "km_core_keyboard_load function" +[km_core_keyboard_load_from_blob]: keyboards#km_core_keyboard_load_from_blob "km_core_keyboard_load_from_blob function" [km_core_keyboard_dispose]: keyboards#km_core_keyboard_dispose "km_core_keyboard_dispose function" [km_core_keyboard_get_attrs]: keyboards#km_core_keyboard_get_attrs "km_core_keyboard_get_attrs function" [km_core_keyboard_get_key_list]: keyboards#km_core_keyboard_get_key_list "km_core_keyboard_get_key_list function" diff --git a/core/docs/api/processor.md b/core/docs/api/processor.md index 0cd491ba81a..c72d743bb9c 100644 --- a/core/docs/api/processor.md +++ b/core/docs/api/processor.md @@ -177,7 +177,7 @@ enum km_core_event_code { [km_core_keyboard_attrs]: keyboards#km_core_keyboard_attrs "km_core_keyboard_attrs struct" [km_core_keyboard_key]: keyboards#km_core_keyboard_key "km_core_keyboard_key struct" [km_core_keyboard_imx]: keyboards#km_core_keyboard_imx "km_core_keyboard_imx struct" -[km_core_keyboard_load]: keyboards#km_core_keyboard_load "km_core_keyboard_load function" +[km_core_keyboard_load_from_blob]: keyboards#km_core_keyboard_load_from_blob "km_core_keyboard_load_from_blob function" [km_core_keyboard_dispose]: keyboards#km_core_keyboard_dispose "km_core_keyboard_dispose function" [km_core_keyboard_get_attrs]: keyboards#km_core_keyboard_get_attrs "km_core_keyboard_get_attrs function" [km_core_keyboard_get_key_list]: keyboards#km_core_keyboard_get_key_list "km_core_keyboard_get_key_list function" diff --git a/core/docs/api/state.md b/core/docs/api/state.md index 3c0c57a6aa5..0292d9f8a59 100644 --- a/core/docs/api/state.md +++ b/core/docs/api/state.md @@ -264,7 +264,7 @@ An opaque pointer to a state object. [km_core_keyboard_attrs]: keyboards#km_core_keyboard_attrs "km_core_keyboard_attrs struct" [km_core_keyboard_key]: keyboards#km_core_keyboard_key "km_core_keyboard_key struct" [km_core_keyboard_imx]: keyboards#km_core_keyboard_imx "km_core_keyboard_imx struct" -[km_core_keyboard_load]: keyboards#km_core_keyboard_load "km_core_keyboard_load function" +[km_core_keyboard_load_from_blob]: keyboards#km_core_keyboard_load_from_blob "km_core_keyboard_load_from_blob function" [km_core_keyboard_dispose]: keyboards#km_core_keyboard_dispose "km_core_keyboard_dispose function" [km_core_keyboard_get_attrs]: keyboards#km_core_keyboard_get_attrs "km_core_keyboard_get_attrs function" [km_core_keyboard_get_key_list]: keyboards#km_core_keyboard_get_key_list "km_core_keyboard_get_key_list function" diff --git a/core/include/keyman/keyman_core_api.h b/core/include/keyman/keyman_core_api.h index 3c41182bab2..7863689296d 100644 --- a/core/include/keyman/keyman_core_api.h +++ b/core/include/keyman/keyman_core_api.h @@ -1096,58 +1096,6 @@ typedef struct { ------------------------------------------------------------------------------- -# km_core_keyboard_load() - -## Description - -DEPRECATED: use [km_core_keyboard_load_from_blob] instead. - -Parse and load keyboard from the supplied path and a pointer to the loaded keyboard -into the out parameter. - -## Specification - -```c */ -// TODO-web-core: Deprecate this function (#12497) -// KMN_DEPRECATED_API -KMN_API -km_core_status -km_core_keyboard_load(km_core_path_name kb_path, - km_core_keyboard **keyboard); - -/* -``` - -## Parameters - -`kb_path` -: On Windows, a UTF-16 string; on other platforms, a C string: - contains a valid path to the keyboard file. - -`keyboard` -: A pointer to result variable: A pointer to the opaque keyboard - object returned by the Processor. This memory must be freed with a - call to [km_core_keyboard_dispose]. - -## Returns - -`KM_CORE_STATUS_OK` -: On success. - -`KM_CORE_STATUS_NO_MEM` -: In the event an internal memory allocation fails. - -`KM_CORE_STATUS_IO_ERROR` -: In the event the keyboard file is unparseable for any reason - -`KM_CORE_STATUS_INVALID_ARGUMENT` -: In the event the file doesn't exist or is inaccesible or `keyboard` is null. - -`KM_CORE_STATUS_OS_ERROR` -: Bit 31 (high bit) set, bits 0-30 are an OS-specific error code. - -------------------------------------------------------------------------------- - # km_core_keyboard_load_from_blob() ## Description @@ -1207,7 +1155,7 @@ km_core_status km_core_keyboard_load_from_blob(const km_core_path_name kb_name, ## Description Free the allocated memory belonging to an opaque keyboard object previously -returned by [km_core_keyboard_load]. +returned by [km_core_keyboard_load_from_blob]. ## Specification diff --git a/core/src/km_core_keyboard_api.cpp b/core/src/km_core_keyboard_api.cpp index abaf3bfaa08..70d37fa63b0 100644 --- a/core/src/km_core_keyboard_api.cpp +++ b/core/src/km_core_keyboard_api.cpp @@ -40,16 +40,18 @@ namespace } // namespace km_core_status -keyboard_load_from_blob_internal( +km_core_keyboard_load_from_blob( const km_core_path_name kb_name, - const std::vector & buf, + const void* blob, + const size_t blob_size, km_core_keyboard** keyboard ) { assert(keyboard); - if (!keyboard) { + if (!keyboard || !blob) { return KM_CORE_STATUS_INVALID_ARGUMENT; } + std::vector buf((uint8_t*)blob, (uint8_t*)blob + blob_size); *keyboard = nullptr; try { abstract_processor* kp = processor_factory(kb_name, buf); @@ -65,88 +67,6 @@ keyboard_load_from_blob_internal( return KM_CORE_STATUS_OK; } -km_core_status -km_core_keyboard_load_from_blob( - const km_core_path_name kb_name, - const void* blob, - const size_t blob_size, - km_core_keyboard** keyboard -) { - assert(keyboard); - if (!keyboard || !blob) { - return KM_CORE_STATUS_INVALID_ARGUMENT; - } - - std::vector buf((uint8_t*)blob, (uint8_t*)blob + blob_size); - return keyboard_load_from_blob_internal(kb_name, buf, keyboard); -} - -// TODO-web-core: Remove this code when we remove the deprecated km_core_keyboard_load method -// BEGIN DEPRECATED -#include -std::vector load_kmx_file(path const& kb_path) { - std::vector data; - std::ifstream file(static_cast(kb_path), std::ios::binary | std::ios::ate); - if (!file.good()) { - return std::vector(); - } - const std::streamsize size = file.tellg(); - if (size >= KMX_MAX_ALLOWED_FILE_SIZE) { - return std::vector(); - } - - file.seekg(0, std::ios::beg); - - data.resize((size_t)size); - if (!file.read((char*)data.data(), size)) { - return std::vector(); - } - - file.close(); - return data; -} - -KMN_DEPRECATED_API -km_core_status -km_core_keyboard_load(km_core_path_name kb, km_core_keyboard **keyboard) -{ - assert(keyboard); - if (!keyboard || !kb) { - return KM_CORE_STATUS_INVALID_ARGUMENT; - } - - path const kb_path(kb); - try - { - abstract_processor* kp = nullptr; - km_core_status status = KM_CORE_STATUS_OK; - // Some legacy packages may include upper-case file extensions - if (kb_path.suffix() == ".kmx" || kb_path.suffix() == ".KMX") { - std::vector buf = load_kmx_file(kb_path); - status = keyboard_load_from_blob_internal(kb_path.stem().c_str(), buf, (km_core_keyboard**)&kp); - if (status != KM_CORE_STATUS_OK) { - return status; - } - } else if (kb_path.suffix() == ".mock") { - kp = new mock_processor(kb_path); - } else { - kp = new null_processor(); - } - status = kp->validate(); - if (status != KM_CORE_STATUS_OK) { - delete kp; - return status; - } - *keyboard = static_cast(kp); - } - catch (std::bad_alloc &) - { - return KM_CORE_STATUS_NO_MEM; - } - return KM_CORE_STATUS_OK; -} -// END DEPRECATED - void km_core_keyboard_dispose(km_core_keyboard *keyboard) { diff --git a/core/tests/unit/km_core_keyboard_api.tests.cpp b/core/tests/unit/km_core_keyboard_api.tests.cpp index 184ec130c42..2898f9e7cd7 100644 --- a/core/tests/unit/km_core_keyboard_api.tests.cpp +++ b/core/tests/unit/km_core_keyboard_api.tests.cpp @@ -10,20 +10,6 @@ km::core::path test_dir; -// TODO-web-core: Remove this code when we remove the deprecated km_core_keyboard_load method -// BEGIN DEPRECATED -#if defined(__GNUC__) || defined(__clang__) -#define PRAGMA(X) _Pragma(#X) -#define DISABLE_WARNING_PUSH PRAGMA(GCC diagnostic push) -#define DISABLE_WARNING_POP PRAGMA(GCC diagnostic pop) -#define DISABLE_WARNING(W) PRAGMA(GCC diagnostic ignored #W) -#define DISABLE_WARNING_DEPRECATED_DECLARATIONS DISABLE_WARNING(-Wdeprecated-declarations) -#else -#define DISABLE_WARNING_PUSH -#define DISABLE_WARNING_POP -#define DISABLE_WARNING_DEPRECATED_DECLARATIONS -#endif - class KmCoreKeyboardApiTests : public testing::Test { protected: km_core_keyboard* keyboard = nullptr; @@ -35,22 +21,6 @@ class KmCoreKeyboardApiTests : public testing::Test { } }; -TEST_F(KmCoreKeyboardApiTests, LoadFromFile) { - // Setup - km::core::path kmxfile = km::core::path(test_dir / "kmx/k_020___deadkeys_and_backspace.kmx"); - - // Execute - DISABLE_WARNING_PUSH - DISABLE_WARNING_DEPRECATED_DECLARATIONS - auto status = km_core_keyboard_load(kmxfile.c_str(), &this->keyboard); - DISABLE_WARNING_POP - - // Verify - EXPECT_EQ(status, KM_CORE_STATUS_OK); - EXPECT_TRUE(this->keyboard != nullptr); -} -// END DEPRECATED - TEST_F(KmCoreKeyboardApiTests, LoadFromBlob) { // Setup km::core::path kmxfile = km::core::path(test_dir / "kmx/k_020___deadkeys_and_backspace.kmx"); diff --git a/core/tests/unit/ldml/core_ldml_min.tests.cpp b/core/tests/unit/ldml/core_ldml_min.tests.cpp index fadd62a2a45..9657b104b51 100644 --- a/core/tests/unit/ldml/core_ldml_min.tests.cpp +++ b/core/tests/unit/ldml/core_ldml_min.tests.cpp @@ -26,7 +26,7 @@ int main(int argc, const char *argv[]) { auto blob = km::tests::load_kmx_file(nowhere); status = km_core_keyboard_load_from_blob(nowhere, blob.data(), blob.size(), &test_kb); - std::cerr << "null km_core_keyboard_load = " << status << std::endl; + std::cerr << "null km_core_keyboard_load_from_blob = " << status << std::endl; test_assert(status == KM_CORE_STATUS_INVALID_ARGUMENT); test_assert(test_kb == nullptr); km_core_keyboard_dispose(test_kb); diff --git a/developer/docs/help/reference/kmc/cli/reference.md b/developer/docs/help/reference/kmc/cli/reference.md index 1de0b64c76d..763d5ad71f8 100644 --- a/developer/docs/help/reference/kmc/cli/reference.md +++ b/developer/docs/help/reference/kmc/cli/reference.md @@ -493,9 +493,8 @@ following sources: `author.bcp47.uniq` id pattern, or a keyboard id pattern (where period `.` is not permitted) * A GitHub repository or subfolder within a repository that matches the Keyman - keyboard/model repository layout. The branch name is optional, and will use - the default branch from the repository if omitted. For example, - `github:keyman-keyboards/khmer_angkor:main:/khmer_angkor.kpj` + keyboard/model repository layout. For example, + `https://github.com/keyman-keyboards/khmer_angkor/tree/main/khmer_angkor.kpj` `-o, --out-path ` diff --git a/developer/src/common/web/test-helpers/TestCompilerCallbacks.ts b/developer/src/common/web/test-helpers/TestCompilerCallbacks.ts new file mode 100644 index 00000000000..a0b0464e218 --- /dev/null +++ b/developer/src/common/web/test-helpers/TestCompilerCallbacks.ts @@ -0,0 +1,168 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { CompilerEvent, CompilerCallbacks, CompilerPathCallbacks, CompilerFileSystemCallbacks, + CompilerError, CompilerNetAsyncCallbacks, DefaultCompilerFileSystemAsyncCallbacks, + CompilerFileSystemAsyncCallbacks } from '@keymanapp/developer-utils'; +import { fileURLToPath } from 'url'; + +const { TEST_SAVE_FIXTURES } = process.env; + +/** + * A CompilerCallbacks implementation for testing + */ +export class TestCompilerCallbacks implements CompilerCallbacks { + /* TestCompilerCallbacks */ + + messages: CompilerEvent[] = []; + readonly _net: TestCompilerNetAsyncCallbacks; + readonly _fsAsync: DefaultCompilerFileSystemAsyncCallbacks = new DefaultCompilerFileSystemAsyncCallbacks(this); + + constructor(basePath?: string) { + if(basePath) { + this._net = new TestCompilerNetAsyncCallbacks(basePath); + } + } + + clear() { + this.messages = []; + } + + printMessages() { + if(this.messages.length) { + process.stdout.write(CompilerError.formatEvent(this.messages)); + } + } + + hasMessage(code: number): boolean { + return this.messages.find((item) => item.code == code) === undefined ? false : true; + } + + fileURLToPath(url: string | URL): string { + return fileURLToPath(url); + } + + /** true of at least one error */ + hasError(): boolean { + return CompilerError.hasError(this.messages); + } + + /* CompilerCallbacks */ + + loadFile(filename: string): Uint8Array { + try { + return fs.readFileSync(filename); + } catch(e) { + if (e.code === 'ENOENT') { + return null; + } else { + throw e; + } + } + } + + fileSize(filename: string): number { + return fs.statSync(filename)?.size; + } + + isDirectory(filename: string): boolean { + return fs.statSync(filename)?.isDirectory(); + } + + get path(): CompilerPathCallbacks { + return { + ...path, + isAbsolute: path.win32.isAbsolute + }; + } + + get fs(): CompilerFileSystemCallbacks { + return fs; + } + + get net(): CompilerNetAsyncCallbacks { + return this._net; + } + + get fsAsync(): CompilerFileSystemAsyncCallbacks { + return this._fsAsync; + } + + resolveFilename(baseFilename: string, filename: string): string { + const basePath = + baseFilename.endsWith('/') || baseFilename.endsWith('\\') ? + baseFilename : + path.dirname(baseFilename); + // Transform separators to platform separators -- we are agnostic + // in our use here but path prefers files may use + // either / or \, although older kps files were always \. + if(path.sep == '/') { + filename = filename.replace(/\\/g, '/'); + } else { + filename = filename.replace(/\//g, '\\'); + } + if(!path.isAbsolute(filename)) { + filename = path.resolve(basePath, filename); + } + return filename; + } + + reportMessage(event: CompilerEvent): void { + // console.log(event.message); + this.messages.push(event); + } + + debug(msg: string) { + console.debug(msg); + } +}; + +class TestCompilerNetAsyncCallbacks implements CompilerNetAsyncCallbacks { + constructor(private basePath: string) { + } + + urlToPath(url: string): string { + const p = new URL(url); + return path.join(this.basePath, p.hostname, p.pathname.replaceAll(/[^a-z0-9_.!@#$%() -]/ig, '#')); + } + + async fetchBlob(url: string): Promise { + const p = this.urlToPath(url); + + if(TEST_SAVE_FIXTURES) { + // When TEST_SAVE_FIXTURES env variable is set, we will do the actual + // fetch from the origin so that we can build the fixtures easily + console.log(`Downloading file ${url} --> ${p}`); + let response: Response; + try { + response = await fetch(url); + } catch(e) { + console.error(`failed to download ${url}`); + console.error(e); + throw e; // yes, we want to abort the download + } + + fs.mkdirSync(path.dirname(p), {recursive: true}); + if(!response.ok) { + // We won't save a file, just delete any existing file + if(fs.existsSync(p)) { + fs.rmSync(p); + } + } else { + const data = new Uint8Array(await response.arrayBuffer()); + fs.writeFileSync(p, data); + } + } + + if(!fs.existsSync(p)) { + // missing file, this is okay + return null; + } + const data: Uint8Array = fs.readFileSync(p); + return data; + } + + async fetchJSON(url: string): Promise { + const data = await this.fetchBlob(url); + return data ? JSON.parse(new TextDecoder().decode(data)) : null; + } +} diff --git a/developer/src/common/web/test-helpers/index.ts b/developer/src/common/web/test-helpers/index.ts index ec1fcaddfd5..6970ed3cc80 100644 --- a/developer/src/common/web/test-helpers/index.ts +++ b/developer/src/common/web/test-helpers/index.ts @@ -1,164 +1,2 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { CompilerEvent, CompilerCallbacks, CompilerPathCallbacks, CompilerFileSystemCallbacks, CompilerError, CompilerNetAsyncCallbacks, DefaultCompilerFileSystemAsyncCallbacks, CompilerFileSystemAsyncCallbacks } from '@keymanapp/developer-utils'; -import { fileURLToPath } from 'url'; export { verifyCompilerMessagesObject } from './verifyCompilerMessagesObject.js'; - -const { TEST_SAVE_FIXTURES } = process.env; - -class TestCompilerNetAsyncCallbacks implements CompilerNetAsyncCallbacks { - constructor(private basePath: string) { - } - - urlToPath(url: string): string { - const p = new URL(url); - return path.join(this.basePath, p.hostname, p.pathname.replaceAll(/[^a-z0-9_.!@#$%() -]/ig, '#')); - } - - async fetchBlob(url: string): Promise { - const p = this.urlToPath(url); - - if(TEST_SAVE_FIXTURES) { - // When TEST_SAVE_FIXTURES env variable is set, we will do the actual - // fetch from the origin so that we can build the fixtures easily - console.log(`Downloading file ${url} --> ${p}`); - let response: Response; - try { - response = await fetch(url); - } catch(e) { - console.error(`failed to download ${url}`); - console.error(e); - throw e; // yes, we want to abort the download - } - - fs.mkdirSync(path.dirname(p), {recursive: true}); - if(!response.ok) { - // We won't save a file, just delete any existing file - if(fs.existsSync(p)) { - fs.rmSync(p); - } - } else { - const data = new Uint8Array(await response.arrayBuffer()); - fs.writeFileSync(p, data); - } - } - - if(!fs.existsSync(p)) { - // missing file, this is okay - return null; - } - const data: Uint8Array = fs.readFileSync(p); - return data; - } - - async fetchJSON(url: string): Promise { - const data = await this.fetchBlob(url); - return data ? JSON.parse(new TextDecoder().decode(data)) : null; - } -} - -/** - * A CompilerCallbacks implementation for testing - */ -export class TestCompilerCallbacks implements CompilerCallbacks { - /* TestCompilerCallbacks */ - - messages: CompilerEvent[] = []; - readonly _net: TestCompilerNetAsyncCallbacks; - readonly _fsAsync: DefaultCompilerFileSystemAsyncCallbacks = new DefaultCompilerFileSystemAsyncCallbacks(this); - - constructor(basePath?: string) { - if(basePath) { - this._net = new TestCompilerNetAsyncCallbacks(basePath); - } - } - - clear() { - this.messages = []; - } - - printMessages() { - if(this.messages.length) { - process.stdout.write(CompilerError.formatEvent(this.messages)); - } - } - - hasMessage(code: number): boolean { - return this.messages.find((item) => item.code == code) === undefined ? false : true; - } - - fileURLToPath(url: string | URL): string { - return fileURLToPath(url); - } - - /** true of at least one error */ - hasError(): boolean { - return CompilerError.hasError(this.messages); - } - - /* CompilerCallbacks */ - - loadFile(filename: string): Uint8Array { - try { - return fs.readFileSync(filename); - } catch(e) { - if (e.code === 'ENOENT') { - return null; - } else { - throw e; - } - } - } - - fileSize(filename: string): number { - return fs.statSync(filename)?.size; - } - - isDirectory(filename: string): boolean { - return fs.statSync(filename)?.isDirectory(); - } - - get path(): CompilerPathCallbacks { - return path; - } - - get fs(): CompilerFileSystemCallbacks { - return fs; - } - - get net(): CompilerNetAsyncCallbacks { - return this._net; - } - - get fsAsync(): CompilerFileSystemAsyncCallbacks { - return this._fsAsync; - } - - resolveFilename(baseFilename: string, filename: string): string { - const basePath = - baseFilename.endsWith('/') || baseFilename.endsWith('\\') ? - baseFilename : - path.dirname(baseFilename); - // Transform separators to platform separators -- we are agnostic - // in our use here but path prefers files may use - // either / or \, although older kps files were always \. - if(path.sep == '/') { - filename = filename.replace(/\\/g, '/'); - } else { - filename = filename.replace(/\//g, '\\'); - } - if(!path.isAbsolute(filename)) { - filename = path.resolve(basePath, filename); - } - return filename; - } - - reportMessage(event: CompilerEvent): void { - // console.log(event.message); - this.messages.push(event); - } - - debug(msg: string) { - console.debug(msg); - } -}; +export { TestCompilerCallbacks } from './TestCompilerCallbacks.js'; diff --git a/developer/src/common/web/utils/src/cloud-urls.ts b/developer/src/common/web/utils/src/cloud-urls.ts new file mode 100644 index 00000000000..33f9c669b9b --- /dev/null +++ b/developer/src/common/web/utils/src/cloud-urls.ts @@ -0,0 +1,18 @@ +/** + * Matches a Keyman keyboard resource, based on the permanent home page for the + * keyboard on keyman.com, `https://keyman.com/keyboards/` + */ +export const KEYMANCOM_CLOUD_URI = /^(?:http(?:s)?:\/\/)?keyman\.com\/keyboards\/(?[a-z0-9_.-]+)/i; + +/** + * Matches a `cloud:` URI for a Keyman resource (e.g. keyboard or lexical + * model) + */ +export const CLOUD_URI = /^cloud:(?.+)$/i; + + +export interface CloudUriRegexMatchArray extends RegExpMatchArray { + groups?: { + id?: string; + } +} \ No newline at end of file diff --git a/developer/src/common/web/utils/src/common-messages.ts b/developer/src/common/web/utils/src/common-messages.ts index 724dadf9cd8..6eca08d37c4 100644 --- a/developer/src/common/web/utils/src/common-messages.ts +++ b/developer/src/common/web/utils/src/common-messages.ts @@ -20,17 +20,19 @@ export class CommonTypesMessages { static ERROR_ImportInvalidBase = SevError | 0x0002; static Error_ImportInvalidBase = (o: { base: string, path: string, subtag: string }) => m(this.ERROR_ImportInvalidBase, - `Import element with base ${def(o.base)} is unsupported. Only ${constants.cldr_import_base} is supported.`); + `Import element with base ${def(o.base)} is unsupported. Only ${constants.cldr_import_base} or empty (for local) are supported.`); static ERROR_ImportInvalidPath = SevError | 0x0003; static Error_ImportInvalidPath = (o: { base: string, path: string, subtag: string }) => m(this.ERROR_ImportInvalidPath, - `Import element with invalid path ${def(o.path)}: expected the form '${constants.cldr_version_latest}/*.xml`); + `Import element with invalid path ${def(o.path)}: expected the form '${constants.cldr_version_latest}/*.xml'`); static ERROR_ImportReadFail = SevError | 0x0004; static Error_ImportReadFail = (o: { base: string, path: string, subtag: string }) => m(this.ERROR_ImportReadFail, - `Import could not read data with path ${def(o.path)}: expected the form '${constants.cldr_version_latest}/*.xml'`); + `Import could not read data with path ${def(o.path)}`, + // for CLDR, give guidance on the suggested path + (o.base === constants.cldr_import_base) ? `expected the form '${constants.cldr_version_latest}/*.xml' for ${o.base}` : undefined); static ERROR_ImportWrongRoot = SevError | 0x0005; static Error_ImportWrongRoot = (o: { base: string, path: string, subtag: string }) => diff --git a/developer/src/common/web/utils/src/github-urls.ts b/developer/src/common/web/utils/src/github-urls.ts new file mode 100644 index 00000000000..1a65106bf1f --- /dev/null +++ b/developer/src/common/web/utils/src/github-urls.ts @@ -0,0 +1,35 @@ +/** + * Matches only a GitHub permanent raw URI with a commit hash, without any other + * components; note hash is called branch to match other URI formats + */ +export const GITHUB_STABLE_SOURCE = /^https:\/\/github\.com\/(?[a-zA-Z0-9-]+)\/(?[\w\.-]+)\/raw\/(?[a-f0-9]{40})\/(?.+)$/; + +/** + * Matches any GitHub git resource raw 'user content' URI which can be + * translated to a permanent URI with a commit hash + */ +export const GITHUB_RAW_URI = /^https:\/\/raw\.githubusercontent\.com\/(?[a-zA-Z0-9-]+)\/(?[\w\.-]+)\/(?:refs\/(?:heads|tags)\/)?(?[^/]+)\/(?.+)$/; + +/** + * Matches any GitHub git resource raw URI which can be translated to a + * permanent URI with a commit hash + */ +export const GITHUB_URI = /^https:\/\/github\.com\/(?[a-zA-Z0-9-]+)\/(?[\w\.-]+)\/(?:raw|blob|tree)\/(?:refs\/(?:heads|tags)\/)?(?[^/]+)\/(?.+)$/; + +/** + * Matches any GitHub git resource raw URI which can be translated to a + * permanent URI with a commit hash, with the http[s] protocol optional, for + * matching user-supplied URLs. groups are: `owner`, `repo`, `branch`, and + * `path`. + */ +export const GITHUB_URI_OPTIONAL_PROTOCOL = /^(?:http(?:s)?:\/\/)?github\.com\/(?[a-zA-Z0-9-]+)\/(?[\w\.-]+)(?:\/(?:(?:raw|blob|tree)\/(?:refs\/(?:heads|tags)\/)?(?[^/]+)\/(?.*))?)?$/; + + +export interface GitHubRegexMatchArray extends RegExpMatchArray { + groups?: { + owner?: string; + repo?: string; + branch?: string; + path?: string; + } +} \ No newline at end of file diff --git a/developer/src/common/web/utils/src/index.ts b/developer/src/common/web/utils/src/index.ts index 5b4c9c0e91a..290efd5fc70 100644 --- a/developer/src/common/web/utils/src/index.ts +++ b/developer/src/common/web/utils/src/index.ts @@ -66,3 +66,6 @@ export { UrlSubpathCompilerCallback } from './utils/UrlSubpathCompilerCallback.j export { CommonTypesMessages } from './common-messages.js'; export * as SourceFilenamePatterns from './source-filename-patterns.js'; export { KeymanXMLType, KeymanXMLWriter, KeymanXMLReader } from './xml-utils.js'; + +export * as GitHubUrls from './github-urls.js'; +export * as CloudUrls from './cloud-urls.js'; \ No newline at end of file diff --git a/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts b/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts index 41d4c699f40..928bb94bef7 100644 --- a/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts +++ b/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts @@ -19,7 +19,10 @@ interface NameAndProps { }; export class LDMLKeyboardXMLSourceFileReaderOptions { - importsPath: string; + /** path to the CLDR imports */ + cldrImportsPath: string; + /** ordered list of paths for local imports */ + localImportsPaths: string[]; }; export class LDMLKeyboardXMLSourceFileReader { @@ -31,10 +34,21 @@ export class LDMLKeyboardXMLSourceFileReader { } readImportFile(version: string, subpath: string): Uint8Array { - const importPath = this.callbacks.resolveFilename(this.options.importsPath, `${version}/${subpath}`); + const importPath = this.callbacks.resolveFilename(this.options.cldrImportsPath, `${version}/${subpath}`); return this.callbacks.loadFile(importPath); } + readLocalImportFile(path: string): Uint8Array { + // try each of the local imports paths + for (const localPath of this.options.localImportsPaths) { + const importPath = this.callbacks.path.join(localPath, path); + if(this.callbacks.fs.existsSync(importPath)) { + return this.callbacks.loadFile(importPath); + } + } + return null; // was not able to load from any of the paths + } + /** * xml2js will not place single-entry objects into arrays. * Easiest way to fix this is to box them ourselves as needed @@ -203,16 +217,25 @@ export class LDMLKeyboardXMLSourceFileReader { */ private resolveOneImport(obj: any, subtag: string, asImport: LKImport, implied? : boolean) : boolean { const { base, path } = asImport; - if (base !== constants.cldr_import_base) { + // If base is not an empty string (or null/undefined), then it must be 'cldr' + if (base && base !== constants.cldr_import_base) { this.callbacks.reportMessage(CommonTypesMessages.Error_ImportInvalidBase({base, path, subtag})); return false; } - const paths = path.split('/'); - if (paths[0] == '' || paths[1] == '' || paths.length !== 2) { - this.callbacks.reportMessage(CommonTypesMessages.Error_ImportInvalidPath({base, path, subtag})); - return false; + let importData: Uint8Array; + + if (base === constants.cldr_import_base) { + // CLDR import + const paths = path.split('/'); + if (paths[0] == '' || paths[1] == '' || paths.length !== 2) { + this.callbacks.reportMessage(CommonTypesMessages.Error_ImportInvalidPath({base, path, subtag})); + return false; + } + importData = this.readImportFile(paths[0], paths[1]); + } else { + // local import + importData = this.readLocalImportFile(path); } - const importData: Uint8Array = this.readImportFile(paths[0], paths[1]); if (!importData || !importData.length) { this.callbacks.reportMessage(CommonTypesMessages.Error_ImportReadFail({base, path, subtag})); return false; @@ -241,6 +264,9 @@ export class LDMLKeyboardXMLSourceFileReader { // mark all children as an implied import subsubval.forEach(o => o[ImportStatus.impliedImport] = basePath); } + if (base !== constants.cldr_import_base) { + subsubval.forEach(o => o[ImportStatus.localImport] = path); + } if (!obj[subsubtag]) { obj[subsubtag] = []; // start with empty array diff --git a/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml.ts b/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml.ts index 696194573f8..587b02dd719 100644 --- a/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml.ts +++ b/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml.ts @@ -38,7 +38,7 @@ export interface LKImport { /** * import base, currently `cldr` is supported */ - base: string; + base?: 'cldr' | ''; /** * path to imported resource, of the form `45/*.xml` */ @@ -199,6 +199,8 @@ export class ImportStatus { static impliedImport = Symbol('LDML implied import'); /** item came in via import */ static import = Symbol('LDML import'); + /** item came in via local (not CLDR) import */ + static localImport = Symbol('LDML local import'); /** @returns true if the object was loaded through an implied import */ static isImpliedImport(o : any) : boolean { @@ -208,5 +210,9 @@ export class ImportStatus { static isImport(o : any) : boolean { return o && !!o[ImportStatus.import]; } + /** @returns true if the object was loaded through an explicit import */ + static isLocalImport(o : any) : boolean { + return o && !!o[ImportStatus.localImport]; + } }; diff --git a/developer/src/common/web/utils/test/TestCompilerCallbacks.ts b/developer/src/common/web/utils/test/TestCompilerCallbacks.ts deleted file mode 100644 index 5b36c63fe3e..00000000000 --- a/developer/src/common/web/utils/test/TestCompilerCallbacks.ts +++ /dev/null @@ -1,76 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { loadFile, resolveFilename } from './helpers/index.js'; -import { CompilerCallbacks, CompilerFileSystemAsyncCallbacks, CompilerFileSystemCallbacks, CompilerNetAsyncCallbacks, CompilerPathCallbacks } from '../src/compiler-callbacks.js'; -import { CompilerError, CompilerEvent } from '../src/compiler-interfaces.js'; -import { fileURLToPath } from 'url'; - -// This is related to developer/src/common/web/test-helpers/index.ts but has a slightly different API surface -// as this runs at a lower level than the compiler. -/** - * A CompilerCallbacks implementation for testing - */ -export class TestCompilerCallbacks implements CompilerCallbacks { - clear() { - this.messages = []; - } - debug(msg: string): void { - console.debug(msg); - } - - printMessages() { - process.stdout.write(CompilerError.formatEvent(this.messages)); - } - - messages: CompilerEvent[] = []; - - get path(): CompilerPathCallbacks { - return path; - } - - get fs(): CompilerFileSystemCallbacks { - return fs; - } - - get fsAsync(): CompilerFileSystemAsyncCallbacks { - return null; // Note: not currently used - } - - get net(): CompilerNetAsyncCallbacks { - return null; // Note: not currently used - } - - resolveFilename(baseFilename: string, filename: string): string { - return resolveFilename(baseFilename, filename); - } - - fileURLToPath(url: string | URL): string { - return fileURLToPath(url); - } - - loadFile(filename: string): Uint8Array { - // TODO: error management, does it belong here? - try { - return loadFile(filename); - } catch (e) { - if (e.code === 'ENOENT') { - return null; - } else { - throw e; - } - } - } - - fileSize(filename: string): number { - return fs.statSync(filename).size; - } - - isDirectory(filename: string): boolean { - return fs.statSync(filename)?.isDirectory(); - } - - reportMessage(event: CompilerEvent): void { - // console.log(event.message); - this.messages.push(event); - } -} diff --git a/developer/src/common/web/utils/test/fixtures/ldml-keyboard/import-local.xml b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/import-local.xml new file mode 100644 index 00000000000..b3cc7cabdff --- /dev/null +++ b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/import-local.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-import-local.xml b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-import-local.xml new file mode 100644 index 00000000000..e8ac9dc9640 --- /dev/null +++ b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-import-local.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/developer/src/common/web/utils/test/fixtures/ldml-keyboard/keys-Zyyy-morepunctuation.xml b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/keys-Zyyy-morepunctuation.xml new file mode 100644 index 00000000000..768e81e7cd2 --- /dev/null +++ b/developer/src/common/web/utils/test/fixtures/ldml-keyboard/keys-Zyyy-morepunctuation.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/developer/src/common/web/utils/test/helpers/reader-callback-test.ts b/developer/src/common/web/utils/test/helpers/reader-callback-test.ts index c67d49dfdb4..d865ab2f46a 100644 --- a/developer/src/common/web/utils/test/helpers/reader-callback-test.ts +++ b/developer/src/common/web/utils/test/helpers/reader-callback-test.ts @@ -7,9 +7,11 @@ import { LDMLKeyboardXMLSourceFile } from '../../src/types/ldml-keyboard/ldml-ke import { LDMLKeyboardTestDataXMLSourceFile } from '../../src/types/ldml-keyboard/ldml-keyboard-testdata-xml.js'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { fileURLToPath } from 'url'; +import { dirname } from 'node:path'; const readerOptions: LDMLKeyboardXMLSourceFileReaderOptions = { - importsPath: fileURLToPath(new URL(...LDMLKeyboardXMLSourceFileReader.defaultImportsURL)) + cldrImportsPath: fileURLToPath(new URL(...LDMLKeyboardXMLSourceFileReader.defaultImportsURL)), + localImportsPaths: [], }; export interface CompilationCase { @@ -76,18 +78,29 @@ export interface TestDataCase { export function testReaderCases(cases : CompilationCase[]) { // we need our own callbacks rather than using the global so messages don't get mixed const callbacks = new TestCompilerCallbacks(); - const reader = new LDMLKeyboardXMLSourceFileReader(readerOptions, callbacks); for (const testcase of cases) { const expectFailure = testcase.throws || !!(testcase.errors); // if true, we expect this to fail const testHeading = expectFailure ? `should fail to load: ${testcase.subpath}`: `should load: ${testcase.subpath}`; it(testHeading, function () { callbacks.clear(); - - const data = loadFile(makePathToFixture('ldml-keyboard', testcase.subpath)); + const path = makePathToFixture('ldml-keyboard', testcase.subpath); + // update readerOptions to point to the source dir. + readerOptions.localImportsPaths = [ dirname(path) ]; + const reader = new LDMLKeyboardXMLSourceFileReader(readerOptions, callbacks); + const data = loadFile(path); assert.ok(data, `reading ${testcase.subpath}`); const source = reader.load(data); if (!testcase.loadfail) { + if (!source) { + // print any loading errs here + if (testcase.warnings) { + assert.includeDeepMembers(callbacks.messages, testcase.warnings, 'expected warnings to be included'); + } else if (!expectFailure) { + // no warnings, so expect zero messages + assert.deepEqual(callbacks.messages, [], 'expected zero messages'); + } + } assert.ok(source, `loading ${testcase.subpath}`); } else { assert.notOk(source, `loading ${testcase.subpath} (expected failure)`); diff --git a/developer/src/common/web/utils/test/kmx/ldml-keyboard-xml-reader.tests.ts b/developer/src/common/web/utils/test/kmx/ldml-keyboard-xml-reader.tests.ts index 17c35660245..e3e6fc1e9fb 100644 --- a/developer/src/common/web/utils/test/kmx/ldml-keyboard-xml-reader.tests.ts +++ b/developer/src/common/web/utils/test/kmx/ldml-keyboard-xml-reader.tests.ts @@ -119,11 +119,33 @@ describe('ldml keyboard xml reader tests', function () { // 'hash' is an import but not implied assert.isFalse(ImportStatus.isImpliedImport(k.find(({id}) => id === 'hash'))); assert.isTrue(ImportStatus.isImport(k.find(({id}) => id === 'hash'))); + assert.isFalse(ImportStatus.isLocalImport(k.find(({id}) => id === 'hash'))); // 'zz' is not imported assert.isFalse(ImportStatus.isImpliedImport(k.find(({id}) => id === 'zz'))); assert.isFalse(ImportStatus.isImport(k.find(({id}) => id === 'zz'))); }, }, + { + subpath: 'import-local.xml', + callback: (data, source, subpath, callbacks) => { + assert.ok(source?.keyboard3?.keys); + const k = pluckKeysFromKeybag(source?.keyboard3?.keys.key, ['interrobang','snail']); + assert.sameDeepOrderedMembers(k.map((entry) => { + // Drop the Symbol members from the returned keys; assertions may expect their presence. + return { + id: entry.id, + output: entry.output + }; + }), [ + { id: 'interrobang', output: '‽' }, + { id: 'snail', output: '@' }, + ]); + // all of the keys are implied imports here + assert.isFalse(ImportStatus.isImpliedImport(source?.keyboard3?.keys.key.find(({id}) => id === 'snail'))); + assert.isTrue(ImportStatus.isImport(source?.keyboard3?.keys.key.find(({id}) => id === 'snail'))); + assert.isTrue(ImportStatus.isLocalImport(source?.keyboard3?.keys.key.find(({id}) => id === 'snail'))); + }, + }, { subpath: 'invalid-import-base.xml', loadfail: true, @@ -135,12 +157,23 @@ describe('ldml keyboard xml reader tests', function () { }), ], }, + { + subpath: 'invalid-import-local.xml', + loadfail: true, + errors: [ + CommonTypesMessages.Error_ImportReadFail({ + base: undefined, + path: 'keys-Zyyy-DOESNOTEXIST.xml', + subtag: 'keys' + }), + ], + }, { subpath: 'invalid-import-path.xml', loadfail: true, errors: [ CommonTypesMessages.Error_ImportInvalidPath({ - base: null, + base: 'cldr', path: '45/too/many/slashes/leading/to/nothing-Zxxx-does-not-exist.xml', subtag: null, }), @@ -151,7 +184,7 @@ describe('ldml keyboard xml reader tests', function () { loadfail: true, errors: [ CommonTypesMessages.Error_ImportReadFail({ - base: null, + base: 'cldr', path: '45/none-Zxxx-does-not-exist.xml', subtag: null, }), diff --git a/developer/src/kmc-copy/src/KeymanProjectCopier.ts b/developer/src/kmc-copy/src/KeymanProjectCopier.ts index e463ec1b4c0..40f68fc1fc4 100644 --- a/developer/src/kmc-copy/src/KeymanProjectCopier.ts +++ b/developer/src/kmc-copy/src/KeymanProjectCopier.ts @@ -4,7 +4,7 @@ * Copy a keyboard or lexical model project */ -import { CompilerCallbacks, CompilerLogLevel, KeymanCompiler, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult, KeymanDeveloperProject, KeymanDeveloperProjectOptions, KPJFileReader, KPJFileWriter, KpsFileReader, KpsFileWriter } from "@keymanapp/developer-utils"; +import { CloudUrls, GitHubUrls, CompilerCallbacks, CompilerLogLevel, KeymanCompiler, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult, KeymanDeveloperProject, KeymanDeveloperProjectOptions, KPJFileReader, KPJFileWriter, KpsFileReader, KpsFileWriter } from "@keymanapp/developer-utils"; import { KeymanFileTypes } from "@keymanapp/common-types"; import { CopierMessages } from "./copier-messages.js"; @@ -86,6 +86,9 @@ export class KeymanProjectCopier implements KeymanCompiler { relocateExternalFiles: boolean = false; // TODO-COPY: support public async init(callbacks: CompilerCallbacks, options: CopierOptions): Promise { + if(!callbacks || !options) { + return false; + } this.callbacks = callbacks; this.options = options; this.cloudSource = new KeymanCloudSource(this.callbacks); @@ -97,7 +100,7 @@ export class KeymanProjectCopier implements KeymanCompiler { * artifacts on success. The files are passed in by name, and the compiler * will use callbacks as passed to the {@link KeymanProjectCopier.init} * function to read any input files by disk. - * @param source Source file or folder to copy. Can be a local file or folder, github:repo[:path], or cloud:id + * @param source Source file or folder to copy. Can be a local file or folder, https://github.com/.../repo[/path], or cloud:id * @returns Binary artifacts on success, null on failure. */ public async run(source: string): Promise { @@ -151,10 +154,10 @@ export class KeymanProjectCopier implements KeymanCompiler { * @returns path to .kpj (either local or remote) */ private async getSourceProject(source: string): Promise { - if(source.startsWith('github:')) { - // `github:owner/repo:path/to/kpj`, referencing a .kpj file + if(source.match(GitHubUrls.GITHUB_URI_OPTIONAL_PROTOCOL) || source.match(GitHubUrls.GITHUB_RAW_URI)) { + // `[https://]github.com/owner/repo/[tree|blob|raw]/[refs/...]/branch/path/to/kpj`, referencing a .kpj file return await this.getGitHubSourceProject(source); - } else if(source.startsWith('cloud:')) { + } else if(source.match(CloudUrls.CLOUD_URI) || source.match(CloudUrls.KEYMANCOM_CLOUD_URI)) { // `cloud:id`, referencing a Keyman Cloud keyboard return await this.getCloudSourceProject(source); } else if(this.callbacks.fs.existsSync(source) && source.endsWith(KeymanFileTypes.Source.Project) && !this.callbacks.isDirectory(source)) { @@ -194,64 +197,69 @@ export class KeymanProjectCopier implements KeymanCompiler { /** * Resolve path to GitHub source, which must be in the following format: - * `github:owner/repo[:branch]:path/to/kpj` + * `[https://]github.com/owner/repo/branch/path/to/kpj` * The path must be fully qualified, referencing the .kpj file; it * cannot just be the folder where the .kpj is found * @param source * @returns a promise: GitHub reference to the source for the keyboard, or null on failure */ private async getGitHubSourceProject(source: string): Promise { - const parts = source.split(':'); - if(parts.length < 3 || parts.length > 4 || !parts[1].match(/^[a-z0-9-]+\/[a-z0-9._-]+$/i)) { - // https://stackoverflow.com/questions/59081778/rules-for-special-characters-in-github-repository-name - this.callbacks.reportMessage(CopierMessages.Error_InvalidGitHubSource({source})); - return null; + const parts: GitHubUrls.GitHubRegexMatchArray = + GitHubUrls.GITHUB_URI_OPTIONAL_PROTOCOL.exec(source) ?? + GitHubUrls.GITHUB_RAW_URI.exec(source); + if(!parts) { + throw new Error('Expected GITHUB_URI_OPTIONAL_PROTOCOL or GITHUB_RAW_URI to match'); } - const origin = parts[1].split('/'); - - const ref: GitHubRef = new GitHubRef({ - owner: origin[0], - repo: origin[1], - branch: null, - path: null - }); + const ref: GitHubRef = new GitHubRef(parts); - if(parts.length == 4) { - ref.branch = parts[2]; - ref.path = parts[3]; - } else { + if(!ref.branch) { ref.branch = await this.cloudSource.getDefaultBranchFromGitHub(ref); if(!ref.branch) { this.callbacks.reportMessage(CopierMessages.Error_CouldNotFindDefaultBranchOnGitHub({ref: ref.toString()})); return null; } - ref.path = parts[2]; - + } + if(!ref.path) { + ref.path = '/'; } if(!ref.path.startsWith('/')) { ref.path = '/' + ref.path; } + if(ref.path != '/') { + if(!ref.path.endsWith('.kpj')) { + // Assumption, project filename matches folder name + if(ref.path.endsWith('/')) { + ref.path = ref.path.substring(0, ref.path.length-1); + } + ref.path = ref.path + '/' + this.callbacks.path.basename(ref.path) + '.kpj'; + } + } + return ref; } /** * Resolve path to Keyman Cloud source (which is on GitHub), which must be in * the following format: - * `cloud:keyboard_id|model_id` + * `cloud:keyboard_id`, or + * `cloud:model_id`, or + * `https://keyman.com/keyboards/keyboard_id` * The `keyboard_id` parameter should be a valid id (a-z0-9_), as found at - * https://keyman.com/keyboards; alternativel if it is a model_id, it should + * https://keyman.com/keyboards; alternatively if it is a model_id, it should * have the format author.bcp47.uniq * @param source * @returns a promise: GitHub reference to the source for the keyboard, or null on failure */ private async getCloudSourceProject(source: string): Promise { - const parts = source.split(':'); - const id = parts[1]; + const parts = CloudUrls.CLOUD_URI.exec(source) ?? CloudUrls.KEYMANCOM_CLOUD_URI.exec(source); + if(!parts) { + throw new Error('Expected CLOUD_URI or KEYMANCOM_CLOUD_URI to match'); + } + const id: string = parts.groups.id; const isModel = /^[^.]+\.[^.]+\.[^.]+$/.test(id); - const remote = await this.cloudSource.getSourceFromKeymanCloud(id, isModel); if(!remote) { return null; @@ -687,4 +695,11 @@ export class KeymanProjectCopier implements KeymanCompiler { return true; } /* c8 ignore stop */ + + /** @internal */ + public unitTestEndPoints = { + getGithubSourceProject: this.getGitHubSourceProject.bind(this), + getCloudSourceProject: this.getCloudSourceProject.bind(this) + }; + } diff --git a/developer/src/kmc-copy/src/cloud.ts b/developer/src/kmc-copy/src/cloud.ts index aeb5171895f..7b3ef1ff119 100644 --- a/developer/src/kmc-copy/src/cloud.ts +++ b/developer/src/kmc-copy/src/cloud.ts @@ -3,7 +3,7 @@ * * GitHub and Keyman Cloud interface wrappers */ -import { CompilerCallbacks } from "@keymanapp/developer-utils"; +import { CompilerCallbacks, GitHubUrls } from "@keymanapp/developer-utils"; import { CopierMessages } from "./copier-messages.js"; import { KeymanFileTypes } from "@keymanapp/common-types"; @@ -12,17 +12,24 @@ export class GitHubRef { public repo: string; public branch: string; public path: string; - constructor(owner: string | GitHubRef, repo?: string, branch?: string, path?: string) { + constructor(owner: string | GitHubRef | GitHubUrls.GitHubRegexMatchArray, repo?: string, branch?: string, path?: string) { if(typeof owner == 'string') { this.owner = owner; this.repo = repo; this.branch = branch; this.path = path; - } else { + } else if("groups" in owner) { + this.owner = owner.groups.owner; + this.repo = owner.groups.repo; + this.branch = owner.groups.branch; + this.path = owner.groups.path; + } else if("owner" in owner) { this.owner = owner.owner; this.repo = owner.repo; this.branch = owner.branch; this.path = owner.path; + } else { + throw new Error(`Unrecognized GitHubRef '${owner}'`) } } toString() { diff --git a/developer/src/kmc-copy/src/copier-messages.ts b/developer/src/kmc-copy/src/copier-messages.ts index 02294a90815..78a948ec79b 100644 --- a/developer/src/kmc-copy/src/copier-messages.ts +++ b/developer/src/kmc-copy/src/copier-messages.ts @@ -117,18 +117,7 @@ export class CopierMessages { `Dry run requested. No changes have been saved` ); - static ERROR_InvalidGitHubSource = SevError | 0x0011; - static Error_InvalidGitHubSource = (o:{source: string}) => m( - this.ERROR_InvalidGitHubSource, - `Source project specification '${def(o.source)}' is not a valid GitHub reference`, - `The source project specification for GitHub sources must match the pattern: - github:\\[:\\]:\\ - The path must include the .kpj filename and may optionally begin with a forward slash. - The following are valid examples: - github:keymanapp/keyboards:master:release/k/khmer_angkor/khmer_angkor.kpj - github:keymanapp/keyboards:release/k/khmer_angkor/khmer_angkor.kpj - github:keymanapp/keyboards:/release/k/khmer_angkor/khmer_angkor.kpj` - ); + // 0x0011 unused static ERROR_CannotDownloadFolderFromGitHub = SevError | 0x0012; static Error_CannotDownloadFolderFromGitHub = (o:{ref: string, message?: string, cause?: string}) => m( diff --git a/developer/src/kmc-copy/test/copier.tests.ts b/developer/src/kmc-copy/test/copier.tests.ts index 3cf9e4c0ee6..6d583862aea 100644 --- a/developer/src/kmc-copy/test/copier.tests.ts +++ b/developer/src/kmc-copy/test/copier.tests.ts @@ -12,6 +12,7 @@ import { assert } from 'chai'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { KeymanProjectCopier } from '../src/KeymanProjectCopier.js'; import { makePathToFixture } from './helpers/index.js'; +import { GitHubRef } from './cloud.js'; const { TEST_SAVE_ARTIFACTS, TEST_SAVE_FIXTURES } = env; let outputRoot: string = '/an/imaginary/root/'; @@ -374,7 +375,7 @@ describe('KeymanProjectCopier', function() { // armenian_mnemonic selected because (a) small, and (b) has v2.0 project, so // that exercises the folder retrieval as well - const result = await copier.run('github:keymanapp/keyboards:release/a/armenian_mnemonic/armenian_mnemonic.kpj'); + const result = await copier.run('github.com/keymanapp/keyboards/tree/master/release/a/armenian_mnemonic/armenian_mnemonic.kpj'); // We should have no messages and a successful result assert.isOk(result); @@ -416,6 +417,81 @@ describe('KeymanProjectCopier', function() { }); }); + // Keyman Cloud patterns + + const cloud_khmer_angkor: GitHubRef = { branch: 'master', owner: 'keymanapp', repo: 'keyboards', path: '/release/k/khmer_angkor/khmer_angkor.kpj' }; + const cloud_nrc_en_mtnt: GitHubRef = { branch: 'master', owner: 'keymanapp', repo: 'lexical-models', path: '/release/nrc/nrc.en.mtnt/nrc.en.mtnt.kpj' }; + const cloud_urls: [string,GitHubRef][] = [ + ['cloud:khmer_angkor', cloud_khmer_angkor], + ['https://keyman.com/keyboards/khmer_angkor', cloud_khmer_angkor], + ['https://keyman.com/keyboards/khmer_angkor/', cloud_khmer_angkor], + ['keyman.com/keyboards/khmer_angkor/', cloud_khmer_angkor], + ['http://keyman.com/keyboards/khmer_angkor#abc', cloud_khmer_angkor], + ['cloud:nrc.en.mtnt', cloud_nrc_en_mtnt], + ]; + + cloud_urls.forEach(url => { + it(`should parse URL '${url[0]}' and figure out the .kpj`, async function() { + // url --> + const copier = new KeymanProjectCopier(); + assert.isTrue(await copier.init(callbacks, { + dryRun: false, + outPath: '' + })); + + const ref = await copier.unitTestEndPoints.getCloudSourceProject(url[0]); + assert.isNotNull(ref); + assert.deepEqual(ref, url[1]); + }); + }) + + + // GitHub patterns that should match as inputs for kmc-copy source + + const armenian_mnemonic_urls = [ { + branch: 'master', urls: [ + 'github.com/keymanapp/keyboards/tree/master/release/a/armenian_mnemonic', + 'http://github.com/keymanapp/keyboards/tree/master/release/a/armenian_mnemonic', + 'https://github.com/keymanapp/keyboards/tree/master/release/a/armenian_mnemonic', + 'https://github.com/keymanapp/keyboards/tree/master/release/a/armenian_mnemonic/', + 'https://github.com/keymanapp/keyboards/tree/refs/heads/master/release/a/armenian_mnemonic', + 'https://github.com/keymanapp/keyboards/tree/refs/heads/master/release/a/armenian_mnemonic/', + 'https://github.com/keymanapp/keyboards/raw/refs/heads/master/release/a/armenian_mnemonic/armenian_mnemonic.kpj', + 'https://github.com/keymanapp/keyboards/raw/master/release/a/armenian_mnemonic/armenian_mnemonic.kpj', + 'https://github.com/keymanapp/keyboards/blob/refs/heads/master/release/a/armenian_mnemonic/armenian_mnemonic.kpj', + 'https://github.com/keymanapp/keyboards/blob/master/release/a/armenian_mnemonic/armenian_mnemonic.kpj', + + // And similar patterns for raw.githubusercontent.com + + 'https://raw.githubusercontent.com/keymanapp/keyboards/refs/heads/master/release/a/armenian_mnemonic/armenian_mnemonic.kpj', + 'https://raw.githubusercontent.com/keymanapp/keyboards/master/release/a/armenian_mnemonic/armenian_mnemonic.kpj', + ]}, { + branch: '78b6f98e5db4a249cc4231f8744f5fe4e5fd29f2', urls: [ + 'https://github.com/keymanapp/keyboards/blob/78b6f98e5db4a249cc4231f8744f5fe4e5fd29f2/release/a/armenian_mnemonic/armenian_mnemonic.kpj', + 'https://github.com/keymanapp/keyboards/tree/78b6f98e5db4a249cc4231f8744f5fe4e5fd29f2/release/a/armenian_mnemonic', + 'https://github.com/keymanapp/keyboards/tree/78b6f98e5db4a249cc4231f8744f5fe4e5fd29f2/release/a/armenian_mnemonic/', + 'https://raw.githubusercontent.com/keymanapp/keyboards/78b6f98e5db4a249cc4231f8744f5fe4e5fd29f2/release/a/armenian_mnemonic/armenian_mnemonic.kpj', + ]}]; + + armenian_mnemonic_urls.forEach(({branch,urls}) => urls.forEach(url => { + it(`should parse URL '${url}' and figure out the .kpj`, async function() { + // url --> + const copier = new KeymanProjectCopier(); + assert.isTrue(await copier.init(callbacks, { + dryRun: false, + outPath: '' + })); + + const ref = await copier.unitTestEndPoints.getGithubSourceProject(url); + assert.deepEqual(ref, { + branch, + owner: 'keymanapp', + repo: 'keyboards', + path: '/release/a/armenian_mnemonic/armenian_mnemonic.kpj' + }); + }); + })); + // TODO-COPY: additional tests it.skip('should copy a disorganized project into current structure', async function() {}); it.skip('should copy a standalone .kmn into a new project', async function() {}); diff --git a/developer/src/kmc-copy/test/fixtures/online/api.keyman.com/#keyboard#khmer_angkor b/developer/src/kmc-copy/test/fixtures/online/api.keyman.com/#keyboard#khmer_angkor new file mode 100644 index 00000000000..17b495a24bb --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/online/api.keyman.com/#keyboard#khmer_angkor @@ -0,0 +1,65 @@ +{ + "id": "khmer_angkor", + "name": "Khmer Angkor", + "license": "mit", + "authorName": "Makara Sok", + "authorEmail": "makara_sok@sil.org", + "description": "

Khmer Unicode keyboard layout based on the NiDA keyboard layout.\nAutomatically corrects many common keying errors.

", + "languages": { + "km": { + "examples": [ + { + "keys": "x j m E r", + "note": "Name of language", + "text": "\u1781\u17d2\u1798\u17c2\u179a" + } + ], + "font": { + "family": "Khmer Mondulkiri", + "source": [ + "Mondulkiri-R.ttf" + ] + }, + "oskFont": { + "family": "KbdKhmr", + "source": [ + "KbdKhmr.ttf" + ] + }, + "languageName": "Khmer", + "displayName": "Khmer" + } + }, + "lastModifiedDate": "2024-07-03T15:47:38.000Z", + "packageFilename": "khmer_angkor.kmp", + "packageFileSize": 4259005, + "jsFilename": "khmer_angkor.js", + "jsFileSize": 70494, + "packageIncludes": [ + "visualKeyboard", + "welcome", + "fonts", + "documentation" + ], + "version": "1.5", + "encodings": [ + "unicode" + ], + "platformSupport": { + "windows": "full", + "macos": "full", + "linux": "full", + "desktopWeb": "full", + "ios": "full", + "android": "full", + "mobileWeb": "full" + }, + "minKeymanVersion": "10.0", + "sourcePath": "release/k/khmer_angkor", + "helpLink": "https://help.keyman.com/keyboard/khmer_angkor", + "related": { + "khmer10": { + "deprecates": true + } + } +} \ No newline at end of file diff --git a/developer/src/kmc-copy/test/fixtures/online/api.keyman.com/#model#nrc.en.mtnt b/developer/src/kmc-copy/test/fixtures/online/api.keyman.com/#model#nrc.en.mtnt new file mode 100644 index 00000000000..bee31a4fe4a --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/online/api.keyman.com/#model#nrc.en.mtnt @@ -0,0 +1,23 @@ +{ + "languages": [ + "en", + "en-us", + "en-ca" + ], + "id": "nrc.en.mtnt", + "name": "English language model mined from MTNT", + "license": "mit", + "authorName": "Eddie Antonio Santos", + "authorEmail": "easantos@ualberta.ca", + "description": "

A unigram language model for English derived from the MTNT corpus http://www.cs.cmu.edu/~pmichel1/mtnt/. This corpus itself is gathered from Reddit, so it is unfiltered internet discussion. This is not humanity at its prettiest!

", + "lastModifiedDate": "2024-09-16T01:05:45.000Z", + "packageFilename": "https://keyman.com/go/package/download/model/nrc.en.mtnt?version=0.3.3&update=1", + "packageFileSize": 332955, + "jsFilename": "https://downloads.keyman.com/models/nrc.en.mtnt/0.3.3/nrc.en.mtnt.model.js", + "jsFileSize": 2713050, + "packageIncludes": [], + "version": "0.3.3", + "minKeymanVersion": "12.0", + "helpLink": "https://help.keyman.com/model/nrc.en.mtnt", + "sourcePath": "release/nrc/nrc.en.mtnt" +} \ No newline at end of file diff --git a/developer/src/kmc-ldml/test/fixtures/sections/keys/import-local.xml b/developer/src/kmc-ldml/test/fixtures/sections/keys/import-local.xml new file mode 100644 index 00000000000..d97d711301d --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/keys/import-local.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/keys/keys-Zyyy-morepunctuation.xml b/developer/src/kmc-ldml/test/fixtures/sections/keys/keys-Zyyy-morepunctuation.xml new file mode 100644 index 00000000000..768e81e7cd2 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/keys/keys-Zyyy-morepunctuation.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/developer/src/kmc-ldml/test/helpers/index.ts b/developer/src/kmc-ldml/test/helpers/index.ts index 3bb67278fbe..bd438c4ca11 100644 --- a/developer/src/kmc-ldml/test/helpers/index.ts +++ b/developer/src/kmc-ldml/test/helpers/index.ts @@ -38,7 +38,8 @@ export const compilerTestCallbacks = new TestCompilerCallbacks(); export const compilerTestOptions: LdmlCompilerOptions = { readerOptions: { - importsPath: fileURLToPath(new URL(...LDMLKeyboardXMLSourceFileReader.defaultImportsURL)) + cldrImportsPath: fileURLToPath(new URL(...LDMLKeyboardXMLSourceFileReader.defaultImportsURL)), + localImportsPaths: [], // will be fixed up in loadSectionFixture } }; @@ -59,8 +60,14 @@ export async function loadSectionFixture(compilerClass: SectionCompilerNew, file const data = callbacks.loadFile(inputFilename); assert.isNotNull(data, `Failed to read file ${inputFilename}`); + compilerTestOptions.readerOptions.localImportsPaths = [ path.dirname(inputFilename) ]; + const reader = new LDMLKeyboardXMLSourceFileReader(compilerTestOptions.readerOptions, callbacks); const source = reader.load(data); + if (!source) { + // print any callbacks here + assert.sameDeepMembers(callbacks.messages, [], `Errors loading ${inputFilename}`); + } assert.isNotNull(source, `Failed to load XML from ${inputFilename}`); if (!reader.validate(source)) { diff --git a/developer/src/kmc-ldml/test/keys.tests.ts b/developer/src/kmc-ldml/test/keys.tests.ts index b639c194cc3..cfb37e6baf5 100644 --- a/developer/src/kmc-ldml/test/keys.tests.ts +++ b/developer/src/kmc-ldml/test/keys.tests.ts @@ -199,6 +199,19 @@ describe('keys', function () { assert.equal(flickw.flicks[0].keyId.value, 'dd'); }, }, + { + subpath: 'sections/keys/import-local.xml', + callback: (keys, subpath, callbacks) => { + assert.isNotNull(keys); + assert.equal((keys).keys.length, 2 + KeysCompiler.reserved_count); + const [snail] = (keys).keys.filter(({ id }) => id.value === 'snail'); + assert.ok(snail,`Missing the snail`); + assert.equal(snail.to.value, `@`, `Snail's value`); + const [interrobang] = (keys).keys.filter(({ id }) => id.value === 'interrobang'); + assert.ok(interrobang,`Missing the interrobang`); + assert.equal(interrobang.to.value, `‽`, `Interrobang's value`); + }, + }, ], keysDependencies); }); diff --git a/developer/src/kmc-package/test/fixtures/bcp47/invalid_bcp47_1.kps b/developer/src/kmc-package/test/fixtures/bcp47/invalid_bcp47_1.kps new file mode 100644 index 00000000000..8a656e38baa --- /dev/null +++ b/developer/src/kmc-package/test/fixtures/bcp47/invalid_bcp47_1.kps @@ -0,0 +1,27 @@ + + + + 10.0.700.0 + 7.0 + + + 1.0 + valid_bcp47 + + + + test.kmx + + + + + Invalid BCP 47 + test + 1.0 + + Not a language tag + + + + + diff --git a/developer/src/kmc-package/test/fixtures/bcp47/test.kmx b/developer/src/kmc-package/test/fixtures/bcp47/test.kmx new file mode 100644 index 00000000000..9e4bd15d391 Binary files /dev/null and b/developer/src/kmc-package/test/fixtures/bcp47/test.kmx differ diff --git a/developer/src/kmc-package/test/fixtures/bcp47/valid_bcp47.kps b/developer/src/kmc-package/test/fixtures/bcp47/valid_bcp47.kps new file mode 100644 index 00000000000..72461e04c28 --- /dev/null +++ b/developer/src/kmc-package/test/fixtures/bcp47/valid_bcp47.kps @@ -0,0 +1,39 @@ + + + + 10.0.700.0 + 7.0 + + + + + + + + + + + + 1.0 + valid_bcp47 + + + + test.kmx + + + + + Valid BCP 47 + test + 1.0 + + Central Khmer (Khmer, Cambodia) + SENCOTEN + Unregistered variant of Straits Salish + Private variant of "Old" Khmer + + + + + diff --git a/developer/src/kmc-package/test/package-compiler.tests.ts b/developer/src/kmc-package/test/package-compiler.tests.ts index 7952e2145a2..16c523bda9a 100644 --- a/developer/src/kmc-package/test/package-compiler.tests.ts +++ b/developer/src/kmc-package/test/package-compiler.tests.ts @@ -12,6 +12,7 @@ import { makePathToFixture } from './helpers/index.js'; import { KmpCompiler } from '../src/compiler/kmp-compiler.js'; import { PackageCompilerMessages } from '../src/compiler/package-compiler-messages.js'; +import { PackageValidation } from '../src/compiler/package-validation.js'; const debug = false; @@ -29,6 +30,13 @@ describe('KmpCompiler', function () { assert.isTrue(await kmpCompiler.init(callbacks, null)); }); + this.afterEach(function() { + if(this.currentTest?.isFailed()) { + callbacks.printMessages(); + } + callbacks.clear(); + }); + for (let modelID of MODELS) { const kpsPath = modelID.includes('withfolders') ? makePathToFixture(modelID, 'source', `${modelID}.model.kps`) : makePathToFixture(modelID, `${modelID}.model.kps`); @@ -284,4 +292,20 @@ describe('KmpCompiler', function () { assert.equal(kmpJson.keyboards[0].version, '4.0'); // picks up example.kmx's version }); + it(`should handle a range of valid BCP47 tags`, function () { + const inputFilename = makePathToFixture('bcp47', 'valid_bcp47.kps'); + const kmpJson = kmpCompiler.transformKpsToKmpObject(inputFilename); + assert.isNotNull(kmpJson); + const validation = new PackageValidation(callbacks, {}); + assert.isTrue(validation.validate(inputFilename, kmpJson)); + }); + + it(`should reject an invalid BCP47 tag`, function () { + const inputFilename = makePathToFixture('bcp47', 'invalid_bcp47_1.kps'); + const kmpJson = kmpCompiler.transformKpsToKmpObject(inputFilename); + assert.isNotNull(kmpJson); + const validation = new PackageValidation(callbacks, {}); + assert.isFalse(validation.validate(inputFilename, kmpJson)); + }); + }); diff --git a/developer/src/kmc/src/commands/buildClasses/BuildLdmlKeyboard.ts b/developer/src/kmc/src/commands/buildClasses/BuildLdmlKeyboard.ts index 6ac98c9b92e..6a6300b646b 100644 --- a/developer/src/kmc/src/commands/buildClasses/BuildLdmlKeyboard.ts +++ b/developer/src/kmc/src/commands/buildClasses/BuildLdmlKeyboard.ts @@ -4,6 +4,7 @@ import { CompilerOptions, CompilerCallbacks } from '@keymanapp/developer-utils'; import { LDMLKeyboardXMLSourceFileReader } from '@keymanapp/developer-utils'; import { BuildActivity } from './BuildActivity.js'; import { fileURLToPath } from 'url'; +import { dirname } from 'node:path'; export class BuildLdmlKeyboard extends BuildActivity { public get name(): string { return 'LDML keyboard'; } @@ -13,7 +14,8 @@ export class BuildLdmlKeyboard extends BuildActivity { public async build(infile: string, outfile: string, callbacks: CompilerCallbacks, options: CompilerOptions): Promise { // TODO-LDML: consider hardware vs touch -- touch-only layout will not have a .kvk const ldmlCompilerOptions: kmcLdml.LdmlCompilerOptions = {...options, readerOptions: { - importsPath: fileURLToPath(new URL(...LDMLKeyboardXMLSourceFileReader.defaultImportsURL)) + cldrImportsPath: fileURLToPath(new URL(...LDMLKeyboardXMLSourceFileReader.defaultImportsURL)), + localImportsPaths: [ dirname(infile) ], // local dir }}; const compiler = new kmcLdml.LdmlKeyboardCompiler(); return await super.runCompiler(compiler, infile, outfile, callbacks, ldmlCompilerOptions); diff --git a/developer/src/kmc/src/commands/buildTestData/index.ts b/developer/src/kmc/src/commands/buildTestData/index.ts index c958673c2fa..5343c81702d 100644 --- a/developer/src/kmc/src/commands/buildTestData/index.ts +++ b/developer/src/kmc/src/commands/buildTestData/index.ts @@ -7,6 +7,7 @@ import { fileURLToPath } from 'url'; import { CommandLineBaseOptions } from 'src/util/baseOptions.js'; import { exitProcess } from '../../util/sysexits.js'; import { InfrastructureMessages } from '../../messages/infrastructureMessages.js'; +import { dirname } from 'node:path'; export async function buildTestData(infile: string, _options: any, commander: any): Promise { const options: CommandLineBaseOptions = commander.optsWithGlobals(); @@ -17,7 +18,8 @@ export async function buildTestData(infile: string, _options: any, commander: an saveDebug: false, shouldAddCompilerVersion: false, readerOptions: { - importsPath: fileURLToPath(new URL(...LDMLKeyboardXMLSourceFileReader.defaultImportsURL)) + cldrImportsPath: fileURLToPath(new URL(...LDMLKeyboardXMLSourceFileReader.defaultImportsURL)), + localImportsPaths: [ dirname(infile) ], // local dir } }; diff --git a/developer/src/kmc/src/commands/copy.ts b/developer/src/kmc/src/commands/copy.ts index 9231ba3a5ec..2f22b24a0a4 100644 --- a/developer/src/kmc/src/commands/copy.ts +++ b/developer/src/kmc/src/commands/copy.ts @@ -27,8 +27,9 @@ export function declareCopy(program: Command) { * a .kpj file, e.g. ./keyboards/khmer_angkor/khmer_angkor.kpj * a local folder (with a .kpj file in it), e.g. ./keyboards/khmer_angkor * a cloud keyboard or lexical model, cloud:id, e.g. cloud:khmer_angkor - * a GitHub repository, optional branch, and path, github:owner/repo[:branch]:path - e.g. github:keyman-keyboards/khmer_angkor:main:/khmer_angkor.kpj + * a GitHub repository, branch, and path, [https://]github.com/owner/repo/tree/branch/path + e.g. https://github.com/keyman-keyboards/khmer_angkor/tree/main/khmer_angkor.kpj or + github.com/keymanapp/keyboards/tree/master/release/k/khmer_angkor `); } diff --git a/linux/debian/libkeymancore2.symbols b/linux/debian/libkeymancore2.symbols index 968f155551b..5ecfeb58a28 100644 --- a/linux/debian/libkeymancore2.symbols +++ b/linux/debian/libkeymancore2.symbols @@ -18,7 +18,6 @@ libkeymancore.so.2 libkeymancore2 #MINVER# km_core_keyboard_get_key_list@Base 17.0.195 km_core_keyboard_imx_list_dispose@Base 17.0.195 km_core_keyboard_key_list_dispose@Base 17.0.195 - km_core_keyboard_load@Base 17.0.195 km_core_keyboard_load_from_blob@Base 18.0.101 km_core_options_list_size@Base 17.0.195 km_core_process_event@Base 17.0.195 diff --git a/linux/debian/tests/test-build b/linux/debian/tests/test-build index 9a48325793c..45186784abe 100755 --- a/linux/debian/tests/test-build +++ b/linux/debian/tests/test-build @@ -41,7 +41,7 @@ int main(int argc, char *argv[]) { km_core_option_item opts[] = {KM_CORE_OPTIONS_END}; km_core_keyboard *kb = NULL; km_core_state *state = NULL; - km_core_keyboard_load(NULL, &kb); + km_core_keyboard_load_from_blob(NULL, NULL, 0, &kb); km_core_state_create(kb, opts, &state); km_core_actions const *a = km_core_state_get_actions(state); } @@ -62,7 +62,7 @@ echo "build 3: OK" # km_core_option_item opts[] = {KM_CORE_OPTIONS_END}; # km_core_keyboard *kb = NULL; # km_core_state *state = NULL; -# km_core_keyboard_load(NULL, &kb); +# km_core_keyboard_load_from_blob(NULL, NULL, 0, &kb); # km_core_state_create(kb, opts, &state); # km_core_actions const *a = km_core_state_get_actions(state); # } diff --git a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj index 51cf7185509..483998a16e0 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj +++ b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj @@ -35,6 +35,7 @@ 29BE9D872CA3C21900B67DE7 /* KMModifierMapping.m in Sources */ = {isa = PBXBuildFile; fileRef = 29BE9D862CA3C21900B67DE7 /* KMModifierMapping.m */; }; 29C1CDE22C5B2F8B003C23BB /* KMSettingsRepository.m in Sources */ = {isa = PBXBuildFile; fileRef = D861B03E2C5747F70003675E /* KMSettingsRepository.m */; }; 29C1CDE32C5B2F8B003C23BB /* KMDataRepository.m in Sources */ = {isa = PBXBuildFile; fileRef = 29015ABC2C58D86F00CCBB94 /* KMDataRepository.m */; }; + 29DD5F442CFEF88000683388 /* SILAndikaV1RGB.png in Resources */ = {isa = PBXBuildFile; fileRef = 29DD5F432CFEF88000683388 /* SILAndikaV1RGB.png */; }; 37A245C12565DFA6000BBF92 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 37A245C02565DFA6000BBF92 /* Assets.xcassets */; }; 37AE5C9D239A7B770086CC7C /* qrcode.min.js in Resources */ = {isa = PBXBuildFile; fileRef = 37AE5C9C239A7B770086CC7C /* qrcode.min.js */; }; 37C2B0CB25FF2C350092E16A /* Help in Resources */ = {isa = PBXBuildFile; fileRef = 37C2B0CA25FF2C340092E16A /* Help */; }; @@ -84,7 +85,6 @@ E213601E2142D7C000A043B7 /* keyman-88.png in Resources */ = {isa = PBXBuildFile; fileRef = E213601D2142D7BF00A043B7 /* keyman-88.png */; }; E21799051FC5B7BC00F2D66A /* KMInputMethodEventHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */; }; E21E645820C04EA8000D6274 /* logo.png in Resources */ = {isa = PBXBuildFile; fileRef = E21E645720C04EA7000D6274 /* logo.png */; }; - E23380FB21407AA100B90591 /* SILInBlue76.png in Resources */ = {isa = PBXBuildFile; fileRef = E23380FA21407AA100B90591 /* SILInBlue76.png */; }; E240F599202DED740000067D /* KMPackage.m in Sources */ = {isa = PBXBuildFile; fileRef = E240F598202DED740000067D /* KMPackage.m */; }; E2585A7F20DD6C3C00CBB994 /* KMMethodEventHandlerTests.kmx in Resources */ = {isa = PBXBuildFile; fileRef = E2585A7E20DD6C3C00CBB994 /* KMMethodEventHandlerTests.kmx */; }; E2585A8120DD7CF100CBB994 /* TestAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = E2585A8020DD7CF100CBB994 /* TestAppDelegate.m */; }; @@ -275,6 +275,7 @@ 29D470972C648D5200224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/KMKeyboardHelpWindowController.strings; sourceTree = ""; }; 29D470982C648D5200224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/MainMenu.strings; sourceTree = ""; }; 29D470992C648D7100224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = ""; }; + 29DD5F432CFEF88000683388 /* SILAndikaV1RGB.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = SILAndikaV1RGB.png; sourceTree = ""; }; 29DD8400276C49E20066A16E /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/KMAboutWindowController.strings; sourceTree = ""; }; 29DD8401276C49E20066A16E /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/preferences.strings; sourceTree = ""; }; 29DD8402276C49E30066A16E /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/KMInfoWindowController.strings; sourceTree = ""; }; @@ -379,7 +380,6 @@ E21799031FC5B74D00F2D66A /* KMInputMethodEventHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMInputMethodEventHandler.h; sourceTree = ""; }; E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMInputMethodEventHandler.m; sourceTree = ""; }; E21E645720C04EA7000D6274 /* logo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = logo.png; path = Keyman4MacIM/KMConfiguration/logo.png; sourceTree = SOURCE_ROOT; }; - E23380FA21407AA100B90591 /* SILInBlue76.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = SILInBlue76.png; path = Keyman4MacIM/Images/SILInBlue76.png; sourceTree = SOURCE_ROOT; }; E240F597202DED300000067D /* KMPackage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMPackage.h; sourceTree = ""; }; E240F598202DED740000067D /* KMPackage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMPackage.m; sourceTree = ""; }; E2585A7E20DD6C3C00CBB994 /* KMMethodEventHandlerTests.kmx */ = {isa = PBXFileReference; lastKnownFileType = file; path = KMMethodEventHandlerTests.kmx; sourceTree = ""; }; @@ -639,7 +639,7 @@ 98BF92421BF01CBE0002126A /* KMAboutWindowController.m */, 293EA3E827140D8100545EED /* KMAboutWindowController.xib */, E213601D2142D7BF00A043B7 /* keyman-88.png */, - E23380FA21407AA100B90591 /* SILInBlue76.png */, + 29DD5F432CFEF88000683388 /* SILAndikaV1RGB.png */, 98BF92461BF01D890002126A /* image.jpg */, 98BF92531BF040A50002126A /* title.png */, ); @@ -846,7 +846,6 @@ 989C9C131A7876DE00A20425 /* Images.xcassets in Resources */, 98E2CEA11A92C39C00AE2455 /* InfoPlist.strings in Resources */, E21E645820C04EA8000D6274 /* logo.png in Resources */, - E23380FB21407AA100B90591 /* SILInBlue76.png in Resources */, 9874C3211B536847000BB543 /* info.png in Resources */, 98E672A01B532F5E00DBDE2F /* KMDownloadKBWindowController.xib in Resources */, 37AE5C9D239A7B770086CC7C /* qrcode.min.js in Resources */, @@ -862,6 +861,7 @@ 293EA3E627140D8100545EED /* KMAboutWindowController.xib in Resources */, 37A245C12565DFA6000BBF92 /* Assets.xcassets in Resources */, 293EA3EB27140DEC00545EED /* preferences.xib in Resources */, + 29DD5F442CFEF88000683388 /* SILAndikaV1RGB.png in Resources */, 9800EC5A1C02940300BF0FB5 /* keyman-for-mac-os-license.html in Resources */, 989C9C161A7876DE00A20425 /* MainMenu.xib in Resources */, 29B42A602728343B00EDD5D3 /* KMKeyboardHelpWindowController.xib in Resources */, diff --git a/mac/Keyman4MacIM/Keyman4MacIM/Images/SILInBlue76.png b/mac/Keyman4MacIM/Keyman4MacIM/Images/SILInBlue76.png deleted file mode 100644 index 0275859531c..00000000000 Binary files a/mac/Keyman4MacIM/Keyman4MacIM/Images/SILInBlue76.png and /dev/null differ diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/Base.lproj/KMAboutWindowController.xib b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/Base.lproj/KMAboutWindowController.xib index b6c018d1a56..fa15b43024f 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/Base.lproj/KMAboutWindowController.xib +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/Base.lproj/KMAboutWindowController.xib @@ -1,8 +1,8 @@ - + - + @@ -20,7 +20,7 @@ - + @@ -42,8 +42,12 @@ - - + + + + + + @@ -100,29 +104,29 @@ -