chore: Merge remote-tracking branch 'origin/master' into chore/merge-master-into-web-core

This commit is contained in:
Eberhard Beilharz 2024-12-06 09:12:36 +01:00
commit 8252a95f61
No known key found for this signature in database
GPG key ID: E9140597606020D3
60 changed files with 1052 additions and 549 deletions

View file

@ -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)

View file

@ -1 +1 @@
18.0.153
18.0.154

View file

@ -68,7 +68,7 @@
<!-- Context: Keyman Settings menu -->
<string name="adjust_keyboard_height" comment="Menu action to adjust keyboard height">Regola altezza tastiera</string>
<!-- Context: Keyman Settings menu -->
<string name="adjust_longpress_delay" comment="Menu action to adjust longpress delay duration">Adjust longpress delay</string>
<string name="adjust_longpress_delay" comment="Menu action to adjust longpress delay duration">Regola ritardo pressione prolungata</string>
<!-- Context: Keyman Settings menu -->
<string name="spacebar_caption" comment="Menu action to set the spacebar caption">Didascalia barra spaziatrice</string>
<!-- Context: Keyman Settings menu / Spacebar caption -->
@ -89,11 +89,11 @@
<string name="spacebar_caption_hint_blank" comment="Spacebar caption hint - blank">Non mostrare la didascalia sulla barra spaziatrice</string>
<!-- Context: Keyman Settings menu / Haptic feedback -->
<string name="haptic_feedback" comment="haptic feedback on key press">Vibra durante la digitazione</string>
<!-- Context: Keyman Settings menu -->
<!-- Context: Keyman Settings menu. Removed in Keyman 18 -->
<string name="show_banner" comment="text suggestions banner">Mostra sempre il banner</string>
<!-- Context: Keyman Settings menu -->
<!-- Context: Keyman Settings menu. Removed in Keyman 18 -->
<string name="show_banner_on" comment="Description when toggle is on">Da attuare</string>
<!-- Context: Keyman Settings menu -->
<!-- Context: Keyman Settings menu. Removed in Keyman 18 -->
<string name="show_banner_off" comment="Description when toggle is off">Quando è spento, mostrato solo quando il testo predittivo è abilitato</string>
<!-- Context: Keyman Settings menu -->
<string name="show_send_crash_report" comment="permission for sending crash reports">Consenti l\'invio di segnalazioni di crash attraverso la rete</string>
@ -126,13 +126,13 @@
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Ripristina impostazioni predefinite</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="longpress_delay_time" comment="Current longpress delay time (seconds)">Delay Time: %1$.1f seconds</string>
<string name="longpress_delay_time" comment="Current longpress delay time (seconds)">Tempo ritardo: %1$.1f secondi</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_delay_time_up" comment="Make longpress delay longer">Delay time longer</string>
<string name="ic_delay_time_up" comment="Make longpress delay longer">Tempo ritardo maggiore</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_delay_time_down" comment="Make longpress delay shorter">Delay time shorter</string>
<string name="ic_delay_time_down" comment="Make longpress delay shorter">Tempo ritardo minore</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_longpress_delay_slider" comment="Adjust longpress delay time">Longpress delay time slider</string>
<string name="ic_longpress_delay_slider" comment="Adjust longpress delay time">Cursore tempo ritardo pressione prolungata</string>
<!-- Context: Keyman Web Browser -->
<string name="hint_text" comment="Prompt to type in the browser search bar">Cerca o digita URL</string>
<!-- Context: Keyman Web Browser -->

View file

@ -133,10 +133,15 @@
<string name="keyboard_install_toast" comment="Notification when a keyboard is installed">%1$s tastiera installata</string>
<!-- Context: Language Settings Keyboard uninstall strings -->
<string name="keyboard_deleted_toast" comment="Notification when a keyboard is deleted">Tastiera eliminata</string>
<!-- Context: Language Settings menu strings -->
<!-- Context: Language Settings menu strings. Removed in Keyman 18 -->
<string name="enable_corrections" comment="Enable corrections from the suggestion banner">Abilita correzioni</string>
<!-- Context: Language Settings menu strings -->
<!-- Context: Language Settings menu strings. Removed in Keyman 18 -->
<string name="enable_predictions" comment="Enable predictions from the suggestion banner">Abilita previsioni</string>
<!-- Context: Language Settings menu strings - radio buttons -->
<string name="suggestions_radio_0" comment="Predictions and corrections disabled">Disabilita suggerimenti</string>
<string name="suggestions_radio_1" comment="Suggestions Enabled: Predictions with no corrections">Solo previsioni</string>
<string name="suggestions_radio_2" comment="Suggestions Enabled: Predictions with corrections">Previsioni con correzioni</string>
<string name="suggestions_radio_3" comment="Suggestions Enabled: Predictions with auto-corrections">Previsioni con correzioni automatiche</string>
<!-- Context: Language Settings menu strings -->
<string name="model_label">Dizionario</string>
<plurals name="model_count">

View file

@ -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);
// });
});
});

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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<uint8_t> & 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<uint8_t> 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<uint8_t> 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 <fstream>
std::vector<uint8_t> load_kmx_file(path const& kb_path) {
std::vector<uint8_t> data;
std::ifstream file(static_cast<std::string>(kb_path), std::ios::binary | std::ios::ate);
if (!file.good()) {
return std::vector<uint8_t>();
}
const std::streamsize size = file.tellg();
if (size >= KMX_MAX_ALLOWED_FILE_SIZE) {
return std::vector<uint8_t>();
}
file.seekg(0, std::ios::beg);
data.resize((size_t)size);
if (!file.read((char*)data.data(), size)) {
return std::vector<uint8_t>();
}
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<uint8_t> 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<km_core_keyboard *>(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)
{

View file

@ -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 @@ protected:
}
};
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");

View file

@ -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);

View file

@ -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 <filename>`

View file

@ -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<Uint8Array> {
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<any> {
const data = await this.fetchBlob(url);
return data ? JSON.parse(new TextDecoder().decode(data)) : null;
}
}

View file

@ -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<Uint8Array> {
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<any> {
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';

View file

@ -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/<id>`
*/
export const KEYMANCOM_CLOUD_URI = /^(?:http(?:s)?:\/\/)?keyman\.com\/keyboards\/(?<id>[a-z0-9_.-]+)/i;
/**
* Matches a `cloud:<id>` URI for a Keyman resource (e.g. keyboard or lexical
* model)
*/
export const CLOUD_URI = /^cloud:(?<id>.+)$/i;
export interface CloudUriRegexMatchArray extends RegExpMatchArray {
groups?: {
id?: string;
}
}

View file

@ -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 }) =>

View file

@ -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\/(?<owner>[a-zA-Z0-9-]+)\/(?<repo>[\w\.-]+)\/raw\/(?<branch>[a-f0-9]{40})\/(?<path>.+)$/;
/**
* 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\/(?<owner>[a-zA-Z0-9-]+)\/(?<repo>[\w\.-]+)\/(?:refs\/(?:heads|tags)\/)?(?<branch>[^/]+)\/(?<path>.+)$/;
/**
* 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\/(?<owner>[a-zA-Z0-9-]+)\/(?<repo>[\w\.-]+)\/(?:raw|blob|tree)\/(?:refs\/(?:heads|tags)\/)?(?<branch>[^/]+)\/(?<path>.+)$/;
/**
* 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\/(?<owner>[a-zA-Z0-9-]+)\/(?<repo>[\w\.-]+)(?:\/(?:(?:raw|blob|tree)\/(?:refs\/(?:heads|tags)\/)?(?<branch>[^/]+)\/(?<path>.*))?)?$/;
export interface GitHubRegexMatchArray extends RegExpMatchArray {
groups?: {
owner?: string;
repo?: string;
branch?: string;
path?: string;
}
}

View file

@ -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';

View file

@ -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

View file

@ -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];
}
};

View file

@ -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 <Uint8Array><unknown>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);
}
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="und" conformsTo="45">
<info author="srl295" indicator="🙀" layout="qwerty" name="Test Minimal Keyboard"/>
<keys>
<import base="" path="keys-Zyyy-morepunctuation.xml"/>
</keys>
</keyboard3>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="und" conformsTo="45">
<info author="srl295" indicator="🙀" layout="qwerty" name="Test Minimal Keyboard"/>
<keys>
<import base="" path="keys-Zyyy-DOESNOTEXIST.xml"/>
</keys>
</keyboard3>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<keys>
<!-- Special Symbols -->
<key id="interrobang" output="‽" />
<key id="snail" output="@" />
</keys>

View file

@ -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)`);

View file

@ -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,
}),

View file

@ -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<boolean> {
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<CopierResult> {
@ -151,10 +154,10 @@ export class KeymanProjectCopier implements KeymanCompiler {
* @returns path to .kpj (either local or remote)
*/
private async getSourceProject(source: string): Promise<string | GitHubRef> {
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<GitHubRef> {
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(parts);
const ref: GitHubRef = new GitHubRef({
owner: origin[0],
repo: origin[1],
branch: null,
path: null
});
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<GitHubRef> {
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)
};
}

View file

@ -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() {

View file

@ -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:\\<owner/repo>[:\\<branch>]:\\<path>
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(

View file

@ -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() {});

View file

@ -0,0 +1,65 @@
{
"id": "khmer_angkor",
"name": "Khmer Angkor",
"license": "mit",
"authorName": "Makara Sok",
"authorEmail": "makara_sok@sil.org",
"description": "<p>Khmer Unicode keyboard layout based on the NiDA keyboard layout.\nAutomatically corrects many common keying errors.</p>",
"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
}
}
}

View file

@ -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": "<p>A unigram language model for English derived from the MTNT corpus <a href=\"http://www.cs.cmu.edu/~pmichel1/mtnt/\">http://www.cs.cmu.edu/~pmichel1/mtnt/</a>. This corpus itself is gathered from Reddit, so it is unfiltered internet discussion. This is not humanity at its prettiest!</p>",
"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"
}

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="mt" conformsTo="45">
<info name="keys-minimal"/>
<keys>
<import base="" path="keys-Zyyy-morepunctuation.xml"/>
</keys>
<layers formId="us">
<layer id="base">
<row keys="snail interrobang" />
</layer>
</layers>
</keyboard3>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<keys>
<!-- Special Symbols -->
<key id="interrobang" output="‽" />
<key id="snail" output="@" />
</keys>

View file

@ -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)) {

View file

@ -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).keys.length, 2 + KeysCompiler.reserved_count);
const [snail] = (<Keys>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).keys.filter(({ id }) => id.value === 'interrobang');
assert.ok(interrobang,`Missing the interrobang`);
assert.equal(interrobang.to.value, ``, `Interrobang's value`);
},
},
], keysDependencies);
});

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<Package>
<System>
<KeymanDeveloperVersion>10.0.700.0</KeymanDeveloperVersion>
<FileVersion>7.0</FileVersion>
</System>
<Info>
<Version>1.0</Version>
<Name>valid_bcp47</Name>
</Info>
<Files>
<File>
<Name>test.kmx</Name>
</File>
</Files>
<Keyboards>
<Keyboard>
<Name>Invalid BCP 47</Name>
<ID>test</ID>
<Version>1.0</Version>
<Languages>
<Language ID="km-mm-mm">Not a language tag</Language>
</Languages>
</Keyboard>
</Keyboards>
<Strings/>
</Package>

Binary file not shown.

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<Package>
<System>
<KeymanDeveloperVersion>10.0.700.0</KeymanDeveloperVersion>
<FileVersion>7.0</FileVersion>
</System>
<Options>
<ExecuteProgram></ExecuteProgram>
<MSIFileName></MSIFileName>
<MSIOptions></MSIOptions>
</Options>
<StartMenu>
<Folder></Folder>
<Items/>
</StartMenu>
<Info>
<Version URL="">1.0</Version>
<Name URL="">valid_bcp47</Name>
</Info>
<Files>
<File>
<Name>test.kmx</Name>
</File>
</Files>
<Keyboards>
<Keyboard>
<Name>Valid BCP 47</Name>
<ID>test</ID>
<Version>1.0</Version>
<Languages>
<Language ID="km">Central Khmer (Khmer, Cambodia)</Language>
<Language ID="str-x-sencoten">SENCOTEN</Language>
<Language ID="str-sencoten">Unregistered variant of Straits Salish</Language>
<Language ID="km-Khmr-KM-x-boran">Private variant of "Old" Khmer</Language>
</Languages>
</Keyboard>
</Keyboards>
<Strings/>
</Package>

View file

@ -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));
});
});

View file

@ -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<boolean> {
// 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);

View file

@ -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<void> {
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
}
};

View file

@ -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
`);
}

View file

@ -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

View file

@ -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);
# }

View file

@ -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 = "<group>"; };
29D470982C648D5200224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/MainMenu.strings; sourceTree = "<group>"; };
29D470992C648D7100224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = "<group>"; };
29DD5F432CFEF88000683388 /* SILAndikaV1RGB.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = SILAndikaV1RGB.png; sourceTree = "<group>"; };
29DD8400276C49E20066A16E /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/KMAboutWindowController.strings; sourceTree = "<group>"; };
29DD8401276C49E20066A16E /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/preferences.strings; sourceTree = "<group>"; };
29DD8402276C49E30066A16E /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/KMInfoWindowController.strings; sourceTree = "<group>"; };
@ -379,7 +380,6 @@
E21799031FC5B74D00F2D66A /* KMInputMethodEventHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMInputMethodEventHandler.h; sourceTree = "<group>"; };
E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMInputMethodEventHandler.m; sourceTree = "<group>"; };
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 = "<group>"; };
E240F598202DED740000067D /* KMPackage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMPackage.m; sourceTree = "<group>"; };
E2585A7E20DD6C3C00CBB994 /* KMMethodEventHandlerTests.kmx */ = {isa = PBXFileReference; lastKnownFileType = file; path = KMMethodEventHandlerTests.kmx; sourceTree = "<group>"; };
@ -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 */,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="19162" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19162"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22690"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -20,7 +20,7 @@
<windowStyleMask key="styleMask" closable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="450" height="318"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="875"/>
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1055"/>
<view key="contentView" id="se5-gp-TjO" customClass="KMAboutBGView">
<rect key="frame" x="0.0" y="0.0" width="450" height="318"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@ -42,8 +42,12 @@
</constraints>
</customView>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="sgd-KC-LbG" userLabel="SIL Image View">
<rect key="frame" x="403" y="263" width="32" height="36"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="SILInBlue76" id="0zb-i2-bhN"/>
<rect key="frame" x="366" y="259" width="74" height="44"/>
<constraints>
<constraint firstAttribute="width" constant="74" id="hjC-xL-hLw"/>
<constraint firstAttribute="height" constant="44" id="uhI-gi-EpR"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="SILAndikaV1RGB" id="0zb-i2-bhN"/>
</imageView>
<imageView autoresizesSubviews="NO" horizontalHuggingPriority="251" verticalHuggingPriority="251" tag="1" translatesAutoresizingMaskIntoConstraints="NO" id="UOr-Co-IuK" userLabel="Keyman Image View">
<rect key="frame" x="15" y="252" width="400" height="58"/>
@ -100,29 +104,29 @@
<stackView autoresizesSubviews="NO" orientation="vertical" alignment="trailing" spacing="12" horizontalStackHuggingPriority="750" verticalStackHuggingPriority="500" verticalClippingResistancePriority="500" horizontalHuggingPriority="750" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="a6P-E8-rqT" userLabel="Button Stack View">
<rect key="frame" x="306" y="0.0" width="120" height="52"/>
<endViews>
<button horizontalHuggingPriority="750" verticalHuggingPriority="750" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="xqc-bi-pPI">
<rect key="frame" x="10" y="25" width="117" height="32"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="18" id="W0C-Lh-ns5"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="120" id="l3A-K7-gqm"/>
</constraints>
<button horizontalHuggingPriority="750" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="xqc-bi-pPI">
<rect key="frame" x="-7" y="25" width="134" height="32"/>
<buttonCell key="cell" type="push" title="Configuration" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="vkh-b5-vO7">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="18" id="W0C-Lh-ns5"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="120" id="l3A-K7-gqm"/>
</constraints>
<connections>
<action selector="configAction:" target="-2" id="3W6-cm-6P5"/>
</connections>
</button>
<button horizontalHuggingPriority="249" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="XIT-T3-xre">
<rect key="frame" x="-7" y="-7" width="134" height="32"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="18" id="CPF-NR-u5o"/>
</constraints>
<buttonCell key="cell" type="push" title="Close" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Kab-Up-fYH">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="18" id="CPF-NR-u5o"/>
</constraints>
<connections>
<action selector="closeAction:" target="-2" id="frJ-pm-Iyg"/>
</connections>
@ -165,7 +169,7 @@
<constraint firstItem="UOr-Co-IuK" firstAttribute="leading" secondItem="se5-gp-TjO" secondAttribute="leading" constant="15" id="Wq2-xT-mCh"/>
<constraint firstItem="wFc-QI-nGq" firstAttribute="top" secondItem="UOr-Co-IuK" secondAttribute="bottom" id="ZZw-D6-rjh"/>
<constraint firstItem="FYn-ZH-f76" firstAttribute="top" secondItem="wFc-QI-nGq" secondAttribute="bottom" id="kuI-RR-4wa"/>
<constraint firstAttribute="trailing" secondItem="sgd-KC-LbG" secondAttribute="trailing" constant="15" id="xGg-Cj-G34"/>
<constraint firstAttribute="trailing" secondItem="sgd-KC-LbG" secondAttribute="trailing" constant="10" id="xGg-Cj-G34"/>
</constraints>
</view>
<connections>
@ -175,7 +179,7 @@
</window>
</objects>
<resources>
<image name="SILInBlue76" width="31.799999237060547" height="35.400001525878906"/>
<image name="SILAndikaV1RGB" width="17.760000228881836" height="10.560000419616699"/>
<image name="image" width="1125" height="375"/>
<image name="keyman-88" width="400" height="140.80000305175781"/>
</resources>

View file

@ -34,6 +34,7 @@
NSRectFillUsingOperation(topFill, NSCompositingOperationSourceOver);
[[NSColor windowBackgroundColor] setFill];
NSRectFillUsingOperation(botFill, NSCompositingOperationSourceOver);
}
- (BOOL)mouseDownCanMoveWindow {

View file

@ -7,31 +7,39 @@
//
#import "KMBarView.h"
#import "KMLogs.h"
@implementation KMBarView
/**
* draws stripe in three colors as part of Keyman branding
* over half is orange, followed by red and blue segments
*/
- (void)drawRect:(NSRect)rect {
CGContextRef context = (CGContextRef)[[NSGraphicsContext currentContext] CGContext];
NSRect rect1 = NSMakeRect(0, 0, rect.size.width*0.56, rect.size.height);
NSRect rect2 = NSMakeRect(rect1.size.width, 0, rect.size.width*0.23, rect.size.height);
NSRect rect3 = NSMakeRect(rect2.origin.x + rect2.size.width, 0, rect.size.width*0.21, rect.size.height);
NSRect barRect = self.frame;
os_log_debug([KMLogs uiLog], "KMBarView drawRect: %{public}@ frame: %{public}@", NSStringFromRect(rect), NSStringFromRect(barRect));
NSRect orangeRect = NSMakeRect(0, 0, barRect.size.width*0.56, barRect.size.height);
NSRect redRect = NSMakeRect(orangeRect.size.width, 0, barRect.size.width*0.23, barRect.size.height);
NSRect blueRect = NSMakeRect(redRect.origin.x + redRect.size.width, 0, barRect.size.width*0.21, barRect.size.height);
CGContextSetFillColorWithColor(context, [NSColor colorWithSRGBRed:246.0/255.0 green:137.0/255.0 blue:36.0/255.0 alpha:1.0].CGColor);
CGContextBeginPath(context);
CGContextAddRect(context, rect1);
CGContextAddRect(context, orangeRect);
CGContextClosePath(context);
CGContextDrawPath(context, kCGPathFill);
CGContextSetFillColorWithColor(context, [NSColor colorWithSRGBRed:204.0/255.0 green:56.0/255.0 blue:70.0/255.0 alpha:1.0].CGColor);
CGContextBeginPath(context);
CGContextAddRect(context, rect2);
CGContextAddRect(context, redRect);
CGContextClosePath(context);
CGContextDrawPath(context, kCGPathFill);
CGContextSetFillColorWithColor(context, [NSColor colorWithSRGBRed:121.0/255.0 green:195.0/255.0 blue:218.0/255.0 alpha:1.0].CGColor);
CGContextBeginPath(context);
CGContextAddRect(context, rect3);
CGContextAddRect(context, blueRect);
CGContextClosePath(context);
CGContextDrawPath(context, kCGPathFill);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View file

@ -281,13 +281,17 @@ verify_npm_setup() {
# see /docs/build/node.md
_select_node_version_with_nvm() {
local REQUIRED_NODE_VERSION="$("$JQ" -r '.engines.node' "$KEYMAN_ROOT/package.json")"
local CURRENT_NODE_VERSION
if [[ $BUILDER_OS != win ]]; then
# launch nvm in a sub process, see _builder_nvm.sh for details
"$KEYMAN_ROOT/resources/build/_builder_nvm.sh" "$REQUIRED_NODE_VERSION"
else
nvm install "$REQUIRED_NODE_VERSION"
nvm use "$REQUIRED_NODE_VERSION"
CURRENT_NODE_VERSION="$(node --version)"
if [[ "$CURRENT_NODE_VERSION" != "v$REQUIRED_NODE_VERSION" ]]; then
start //wait //b nvm install "$REQUIRED_NODE_VERSION"
start //wait //b nvm use "$REQUIRED_NODE_VERSION"
fi
fi
# Now, check that the node version is correct, on all systems
@ -296,7 +300,7 @@ _select_node_version_with_nvm() {
# https://github.com/coreybutler/nvm-windows/issues/738
# note the 'v' prefix that node emits (and npm doesn't!)
local CURRENT_NODE_VERSION="$(node --version)"
CURRENT_NODE_VERSION="$(node --version)"
if [[ "$CURRENT_NODE_VERSION" != "v$REQUIRED_NODE_VERSION" ]]; then
builder_die "Attempted to select node.js version $REQUIRED_NODE_VERSION but found $CURRENT_NODE_VERSION instead"
fi
@ -306,13 +310,13 @@ check-markdown() {
node "$KEYMAN_ROOT/resources/tools/check-markdown" --root "$1"
}
#
#
# Runs eslint, builds tests, and then runs tests with mocha + c8 (coverage)
#
#
# Usage:
# builder_run_action test builder_do_typescript_tests [coverage_threshold]
# Parameters:
# 1: coverage_threshold optional, minimum coverage for c8 to pass tests,
# 1: coverage_threshold optional, minimum coverage for c8 to pass tests,
# defaults to 90 (percent)
#
# Todo:
@ -342,4 +346,4 @@ builder_do_typescript_tests() {
builder_echo warning "Coverage thresholds are currently $C8_THRESHOLD%, which is lower than ideal."
builder_echo warning "Please increase threshold in build.sh as test coverage improves."
fi
}
}

View file

@ -296,7 +296,7 @@
<!-- Context: Configuration Dialog - Options tab -->
<!-- String Type: FormatString -->
<!-- Introduced: 18.0.95 -->
<string name="koRightModifierHK" comment="General options - Right Ctrl Alt and Shift trigger hotkey">Allow right modifier for hotkeys</string>
<string name="koRightModifierHK" comment="General options - Right Ctrl Alt and Shift trigger hotkey">Consenti tasti modificatori a destra</string>
<!-- Context: Configuration Dialog - Options tab -->
<!-- String Type: FormatString -->
<!-- Introduced: 9.0.480.0 -->
@ -425,11 +425,11 @@
<!-- Context: Configuration Dialog - Support tab -->
<!-- String Type: PlainText -->
<!-- Introduced: 18.0.94.0 -->
<string name="S_Support_CreatedBySILGlobal" comment="Created by {SIL Global} ">Created by %1$s</string>
<string name="S_Support_CreatedBySILGlobal" comment="Created by {SIL Global} ">Creato da %1$s</string>
<!-- Context: Configuration Dialog - Support tab -->
<!-- String Type: PlainText -->
<!-- Introduced: 18.0.94.0 -->
<string name="S_Support_CopyrightSILGlobal" comment="Copyright © {SIL Global}. All Rights Reserved.">Copyright © %1$s. All Rights Reserved.</string>
<string name="S_Support_CopyrightSILGlobal" comment="Copyright © {SIL Global}. All Rights Reserved.">Copyright © %1$s. Tutti i diritti riservati.</string>
<!-- Context: Configuration Dialog - Support tab -->
<!-- String Type: PlainText -->
<!-- Introduced: 7.0.230.0 -->
@ -629,7 +629,7 @@ che usi in Windows. Le tastiere Keyman si adatteranno automaticamente al layout
<!-- Context: Splash Dialog -->
<!-- String Type: PlainText -->
<!-- Introduced: 18.0 -->
<string name="S_Splash_Start_2" comment="Start {Keyman} button">Start %1$s</string>
<string name="S_Splash_Start_2" comment="Start {Keyman} button">Avvia %1$s</string>
<!-- Context: Splash Dialog -->
<!-- String Type: PlainText -->
<!-- Introduced: 8.0.294.0 -->

View file

@ -127,7 +127,7 @@ BOOL LoadlpKeyboard(int i)
free(buffer);
if (err_status != KM_CORE_STATUS_OK) {
SendDebugMessageFormat("km_core_keyboard_load failed for %ls with error status [%d]", keyboardPath, err_status);
SendDebugMessageFormat("km_core_keyboard_load_from_blob failed for %ls with error status [%d]", keyboardPath, err_status);
goto ExitError;
}
delete[] keyboardPath;

View file

@ -16,7 +16,7 @@ TEST(KEYBOARDOPTIONS, SetupCoreEnvironment) {
km_core_path_name dummyPath = L"dummyActions.mock";
EXPECT_EQ(SetupCoreEnvironment(&core_env_opts), TRUE);
EXPECT_EQ(km_core_keyboard_load(dummyPath, &kp->lpCoreKeyboard), KM_CORE_STATUS_OK);
EXPECT_EQ(km_core_keyboard_load_from_blob(dummyPath, (void *)"MOCK", 4, &kp->lpCoreKeyboard), KM_CORE_STATUS_OK);
EXPECT_EQ(km_core_state_create(kp->lpCoreKeyboard, core_env_opts, &kp->lpCoreKeyboardState), KM_CORE_STATUS_OK);
km_core_option_item *expected_items = new km_core_option_item[5];