mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-05 00:15:32 +00:00
chore: Merge remote-tracking branch 'origin/master' into chore/merge-master-into-web-core
This commit is contained in:
commit
8252a95f61
60 changed files with 1052 additions and 549 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
18.0.153
|
||||
18.0.154
|
||||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
234
common/web/types/tests/util/file-types.tests.ts
Normal file
234
common/web/types/tests/util/file-types.tests.ts
Normal 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);
|
||||
// });
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>`
|
||||
|
||||
|
|
|
|||
168
developer/src/common/web/test-helpers/TestCompilerCallbacks.ts
Normal file
168
developer/src/common/web/test-helpers/TestCompilerCallbacks.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
18
developer/src/common/web/utils/src/cloud-urls.ts
Normal file
18
developer/src/common/web/utils/src/cloud-urls.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }) =>
|
||||
|
|
|
|||
35
developer/src/common/web/utils/src/github-urls.ts
Normal file
35
developer/src/common/web/utils/src/github-urls.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
7
developer/src/common/web/utils/test/fixtures/ldml-keyboard/import-local.xml
vendored
Normal file
7
developer/src/common/web/utils/test/fixtures/ldml-keyboard/import-local.xml
vendored
Normal 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>
|
||||
7
developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-import-local.xml
vendored
Normal file
7
developer/src/common/web/utils/test/fixtures/ldml-keyboard/invalid-import-local.xml
vendored
Normal 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>
|
||||
6
developer/src/common/web/utils/test/fixtures/ldml-keyboard/keys-Zyyy-morepunctuation.xml
vendored
Normal file
6
developer/src/common/web/utils/test/fixtures/ldml-keyboard/keys-Zyyy-morepunctuation.xml
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<keys>
|
||||
<!-- Special Symbols -->
|
||||
<key id="interrobang" output="‽" />
|
||||
<key id="snail" output="@" />
|
||||
</keys>
|
||||
|
|
@ -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)`);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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() {});
|
||||
|
|
|
|||
65
developer/src/kmc-copy/test/fixtures/online/api.keyman.com/#keyboard#khmer_angkor
vendored
Normal file
65
developer/src/kmc-copy/test/fixtures/online/api.keyman.com/#keyboard#khmer_angkor
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
23
developer/src/kmc-copy/test/fixtures/online/api.keyman.com/#model#nrc.en.mtnt
vendored
Normal file
23
developer/src/kmc-copy/test/fixtures/online/api.keyman.com/#model#nrc.en.mtnt
vendored
Normal 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"
|
||||
}
|
||||
16
developer/src/kmc-ldml/test/fixtures/sections/keys/import-local.xml
vendored
Normal file
16
developer/src/kmc-ldml/test/fixtures/sections/keys/import-local.xml
vendored
Normal 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>
|
||||
6
developer/src/kmc-ldml/test/fixtures/sections/keys/keys-Zyyy-morepunctuation.xml
vendored
Normal file
6
developer/src/kmc-ldml/test/fixtures/sections/keys/keys-Zyyy-morepunctuation.xml
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<keys>
|
||||
<!-- Special Symbols -->
|
||||
<key id="interrobang" output="‽" />
|
||||
<key id="snail" output="@" />
|
||||
</keys>
|
||||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
27
developer/src/kmc-package/test/fixtures/bcp47/invalid_bcp47_1.kps
vendored
Normal file
27
developer/src/kmc-package/test/fixtures/bcp47/invalid_bcp47_1.kps
vendored
Normal 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>
|
||||
BIN
developer/src/kmc-package/test/fixtures/bcp47/test.kmx
vendored
Normal file
BIN
developer/src/kmc-package/test/fixtures/bcp47/test.kmx
vendored
Normal file
Binary file not shown.
39
developer/src/kmc-package/test/fixtures/bcp47/valid_bcp47.kps
vendored
Normal file
39
developer/src/kmc-package/test/fixtures/bcp47/valid_bcp47.kps
vendored
Normal 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>
|
||||
|
|
@ -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));
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
# }
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
NSRectFillUsingOperation(topFill, NSCompositingOperationSourceOver);
|
||||
[[NSColor windowBackgroundColor] setFill];
|
||||
NSRectFillUsingOperation(botFill, NSCompositingOperationSourceOver);
|
||||
|
||||
}
|
||||
|
||||
- (BOOL)mouseDownCanMoveWindow {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
BIN
mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/SILAndikaV1RGB.png
Normal file
BIN
mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/SILAndikaV1RGB.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue