chore(developer): Merge remote-tracking branch 'origin/master' into chore/developer/non-printing-chars

This commit is contained in:
Darcy Wong 2023-10-27 06:47:43 +07:00
commit 7bacbb2d8d
27 changed files with 1082 additions and 35 deletions

View file

@ -1,5 +1,17 @@
# Keyman Version History
## 17.0.199 alpha 2023-10-26
* fix(developer): handle xml errors in package compiler (#9821)
* fix(developer): server download Keyman link (#9822)
* chore(common): handle invalid XML in kpj-file-reader (#9824)
* fix(developer): reduce confusion in Unicode fields in touch layout editor (#9839)
## 17.0.198 alpha 2023-10-25
* chore(common): Add entries from 16.0 HISTORY.md (#9826)
* feat(core): new actions APIs (#9828)
## 17.0.197 alpha 2023-10-24
* chore(linux): Rename (lib)kmnkbp to (lib)keymancore (#9793)

View file

@ -1 +1 @@
17.0.198
17.0.200

View file

@ -21,7 +21,12 @@ export class KPJFileReader {
emptyTag: ''
});
parser.parseString(file, (e: unknown, r: unknown) => { data = r as KPJFile });
parser.parseString(file, (e: unknown, r: unknown) => {
if(e) {
throw e;
}
data = r as KPJFile;
});
data = this.boxArrays(data);
for(let file of data.KeymanDeveloperProject?.Files?.File) {
// xml2js imports <Details/> as '' so we will just delete the empty string

View file

@ -253,8 +253,8 @@ km_core_context_items_from_utf8(char const *text,
Convert a context item array into a UTF-16 encoded string placing it into
the supplied buffer of specified size, and return the number of code units
actually used in the conversion. If null is passed as the buffer the
number codeunits required is returned. This will strip markers from the
context during the conversion.
number of codeunits required is returned. Any markers in the context will
not be included in the output buffer.
##### Return status:
- `KM_CORE_STATUS_OK`: On success.
- `KM_CORE_STATUS_INVALID_ARGUMENT`: If non-optional parameters are null.
@ -285,8 +285,8 @@ km_core_context_items_to_utf16(km_core_context_item const *item,
Convert a context item array into a UTF-8 encoded string placing it into
the supplied buffer of specified size, and return the number of code units
actually used in the conversion. If null is passed as the buffer the
number codeunits required is returned. This will strip markers from the
context during the conversion.
number of codeunits required is returned. Any markers in the context will
not be included in the output buffer.
##### Return status:
- `KM_CORE_STATUS_OK`: On success.
- `KM_CORE_STATUS_INVALID_ARGUMENT`: If non-optional parameters are null.
@ -310,6 +310,38 @@ km_core_context_items_to_utf8(km_core_context_item const *item,
char *buf,
size_t *buf_size);
/*
```
### `km_core_context_items_to_utf32`
##### Description:
Convert a context item array into a UTF-32 encoded string placing it into
the supplied buffer of specified size, and return the number of codepoints
actually used in the conversion. If null is passed as the buffer the
number of codepoints required is returned. Any markers in the context will
not be included in the output buffer.
##### Return status:
- `KM_CORE_STATUS_OK`: On success.
- `KM_CORE_STATUS_INVALID_ARGUMENT`: If non-optional parameters are null.
- `KM_CORE_STATUS_INSUFFICENT_BUFFER`: If the buffer is not large enough.
`buf_size` will contain the space required. The contents of the buffer are
undefined.
##### Parameters:
- __context_items__: A pointer to the start of an array `km_core_context_item`.
Must be terminated with a type of `KM_CORE_CT_END`.
- __buf__: A pointer to the buffer to place the UTF-32 string into.
May be null to request size calculation.
- __buf_size__: a pointer to the result variable:
The size of the supplied buffer in codepoints if `buf` is given.
On return will be the size required if `buf` is null.
```c
*/
KMN_API
km_core_status
km_core_context_items_to_utf32(km_core_context_item const *item,
km_core_usv *buf,
size_t *buf_size);
/*
```
### `km_core_context_items_dispose`
@ -501,10 +533,10 @@ typedef struct {
uint8_t type;
uint8_t _reserved[sizeof(void*)-sizeof(uint8_t)];
union {
uintptr_t marker; // MARKER type
uint32_t marker; // MARKER type
km_core_option_item const * option; // OPT types
km_core_usv character; // CHAR type
uint8_t capsLock; // CAPSLOCK type, 1 to turn on, 0 to turn off
uint8_t capsLock; // CAPSLOCK type, 1 to turn on, 0 to turn off; re name see #9833
km_core_backspace_item backspace; // BACKSPACE type
};
} km_core_action_item;
@ -526,6 +558,161 @@ enum km_core_action_type {
KM_CORE_IT_MAX_TYPE_ID
};
/*
```
### Actions
This structure provides the results of processing a key event to the Platform layer and
should be processed by the Platform layer to issue commands to the os text
services framework to transform the text store in the Client Application, among
other actions.
This API replaces the Action items APIs, which is now deprecated and will be
removed in the future.
```c
*/
typedef enum { KM_CORE_FALSE = 0, KM_CORE_TRUE = 1 } km_core_bool;
typedef enum { KM_CORE_CAPS_UNCHANGED = -1, KM_CORE_CAPS_OFF = 0, KM_CORE_CAPS_ON = 1 } km_core_caps_state;
typedef struct {
// number of codepoints (not codeunits!) to delete from app context.
unsigned int code_points_to_delete;
// null-term string of characters to insert into document
km_core_usv* output;
// list of options to persist, terminated with KM_CORE_OPTIONS_END
km_core_option_item* persist_options;
// issue a beep, 0 = no, 1 = yes
km_core_bool do_alert;
// emit the (unmodified) input keystroke to the application, 0 = no, 1 = yes
km_core_bool emit_keystroke;
// -1=unchanged, 0=off, 1=on
km_core_caps_state new_caps_lock_state;
} km_core_actions;
/*
```
### `km_core_state_get_actions`
##### Description:
Returns a pointer to an actions object which details all the actions
that the Platform layer must take after a keystroke. The `code_points_to_delete`
action must be performed before the `output` action, but the other
actions may be performed in any order.
##### Return:
A pointer to a `km_core_actions` object, which must be freed with
`km_core_actions_dispose`.
##### Parameters:
- __state__: An opaque pointer to a state object.
```c
*/
KMN_API
km_core_actions*
km_core_state_get_actions(
km_core_state const *state
);
/*
```
### `km_core_actions_dispose`
##### Description:
Free the allocated memory belonging to an actions object previously
returned by `km_core_state_get_actions`.
##### Parameters:
- __actions__: A pointer to the actions object to be disposed of.
```c
*/
KMN_API
km_core_status
km_core_actions_dispose(
km_core_actions* actions
);
/*
```
### `km_core_context_status`
##### Description:
Return values for `km_core_state_context_set_if_needed`.
```c
*/
typedef enum {
KM_CORE_CONTEXT_STATUS_UNCHANGED = 0, // Cached context change was not needed
KM_CORE_CONTEXT_STATUS_UPDATED = 1, // Cached context was set to application context
KM_CORE_CONTEXT_STATUS_CLEARED = 2, // Application context was invalid, context was cleared
KM_CORE_CONTEXT_STATUS_ERROR = 3, // Internal error
KM_CORE_CONTEXT_STATUS_INVALID_ARGUMENT = 4, // Invalid arguments
} km_core_context_status;
/*
```
### `km_core_state_context_set_if_needed`
##### Description:
Sets the internal cached context for the state object, to the passed-in
application context string, if it differs from the codepoints in the
cached context. For the purposes of comparison, (1) cached markers are
ignored, (2) if the cached context is shorter than the application
context, it is considered identical, but (3) if the cached context is
longer, then it is considered different.
If a difference is found, then the cached context will be set to the
application context, and thus any cached markers will be cleared.
`km_core_state_context_set_if_needed` and `km_core_state_context_clear`
will replace most uses of the existing Core context APIs.
##### Parameters:
- __state__: An opaque pointer to a state object.
- __application_context__: A pointer to an null-terminated `km_core_cp`
string representing the current context from the application.
##### Return status:
- `KM_CORE_CONTEXT_STATUS_UNCHANGED`: Cached context change was not needed
- `KM_CORE_CONTEXT_STATUS_UPDATED`: Cached context was set to application
context
- `KM_CORE_CONTEXT_STATUS_CLEARED`: Application context was invalid, perhaps
had unpaired surrogates, and so cached context was cleared instead
- `KM_CORE_CONTEXT_STATUS_ERROR`: Internal error
- `KM_CORE_CONTEXT_STATUS_INVALID_ARGUMENT`: One or more parameters was null
```c
*/
KMN_API
km_core_context_status
km_core_state_context_set_if_needed(
km_core_state *state,
km_core_cp const *application_context
);
/*
```
### `km_core_state_context_clear`
##### Description:
Clears the internal cached context for the state. This is the same as
`km_core_context_clear(km_core_state_context(&state))`.
`km_core_state_context_set_if_needed` and `km_core_state_context_clear`
will replace most uses of the existing Core context APIs.
##### Parameters:
- __state__: An opaque pointer to a state object.
##### Return status:
- `KM_CORE_STATUS_OK`: On success.
- `KM_CORE_STATUS_INVALID_ARGUMENT`: If any parameters are null.
```c
*/
KMN_API
km_core_status
km_core_state_context_clear(
km_core_state *state
);
/*
```

138
core/src/action.cpp Normal file
View file

@ -0,0 +1,138 @@
/*
Copyright: © 2023 SIL International.
Description: Implementation of the action API functions using internal
data structures and functions.
Create Date: 23 Oct 2023
Authors: Marc Durdin (MCD)
History: 23 Oct 2023 - MCD - Initial implementation from #9720
*/
#include <cassert>
#include <algorithm>
#include <sstream>
#include <memory>
#include <keyman/keyman_core_api.h>
#include "action.hpp"
#include "state.hpp"
#include "option.hpp"
km_core_actions * km::core::action_item_list_to_actions_object(
km_core_action_item const *action_items
) {
assert(action_items != nullptr);
if(action_items == nullptr) {
return nullptr;
}
km_core_status status = KM_CORE_STATUS_OK;
std::unique_ptr<km_core_actions> actions(new km_core_actions);
// Set actions default values
std::vector<km_core_context_item> output;
std::vector<km_core_option_item> options;
actions->code_points_to_delete = 0;
actions->do_alert = KM_CORE_FALSE;
actions->emit_keystroke = KM_CORE_FALSE;
actions->new_caps_lock_state = KM_CORE_CAPS_UNCHANGED;
// Clear output pointers, will be set later once we have sizes
actions->output = nullptr;
actions->persist_options = nullptr;
for (; action_items->type != KM_CORE_IT_END; ++action_items) {
assert(action_items->type < KM_CORE_IT_MAX_TYPE_ID);
switch(action_items->type) {
case KM_CORE_IT_ALERT:
actions->do_alert = KM_CORE_TRUE;
break;
case KM_CORE_IT_BACK:
switch(action_items->backspace.expected_type) {
case KM_CORE_BT_UNKNOWN:
// this is equivalent to emit_keystroke, because the only time we
// are allowed to do an unknown bksp is when a bksp is passed in
actions->emit_keystroke = KM_CORE_TRUE;
break;
case KM_CORE_BT_CHAR:
if(output.empty()) {
actions->code_points_to_delete++;
} else {
auto last_context_item = output.back();
output.pop_back();
assert(last_context_item.type == KM_CORE_CT_CHAR);
assert(last_context_item.character == action_items->backspace.expected_value);
}
break;
case KM_CORE_BT_MARKER:
if(output.empty()) {
// deleting a marker has no effect on the application
} else {
auto last_context_item = output.back();
output.pop_back();
assert(last_context_item.type == KM_CORE_CT_MARKER);
assert(last_context_item.marker == action_items->backspace.expected_value);
}
break;
default:
assert(false);
}
break;
case KM_CORE_IT_CAPSLOCK:
actions->new_caps_lock_state = action_items->capsLock ? KM_CORE_CAPS_ON : KM_CORE_CAPS_OFF;
break;
case KM_CORE_IT_CHAR:
output.push_back({KM_CORE_CT_CHAR,{0},{action_items->character}});
break;
case KM_CORE_IT_EMIT_KEYSTROKE:
actions->emit_keystroke = KM_CORE_TRUE;
break;
case KM_CORE_IT_INVALIDATE_CONTEXT:
// no-op
break;
case KM_CORE_IT_MARKER:
output.push_back({KM_CORE_CT_MARKER,{0},{action_items->marker}});
break;
case KM_CORE_IT_PERSIST_OPT:
// TODO: lowpri: replace existing item if already present in options vector?
options.push_back(km::core::option(
static_cast<km_core_option_scope>(action_items->option->scope),
action_items->option->key,
action_items->option->value
));
break;
default:
assert(false);
}
}
// Strip the markers from the output, and convert to an string of UTF-32
output.push_back(KM_CORE_CONTEXT_ITEM_END);
size_t buf_size;
if((status = km_core_context_items_to_utf32(output.data(), nullptr, &buf_size)) != KM_CORE_STATUS_OK) {
return nullptr;
}
std::unique_ptr<km_core_usv[]> output_usv(new km_core_usv[buf_size]);
if((status = km_core_context_items_to_utf32(output.data(), output_usv.get(), &buf_size)) != KM_CORE_STATUS_OK) {
return nullptr;
}
actions->output = output_usv.release();
// Create an array of the persisted options
options.push_back(KM_CORE_OPTIONS_END);
actions->persist_options = new km_core_option_item[options.size()];
std::copy(options.begin(), options.end(), actions->persist_options);
// We now have a complete set of actions
return actions.release();
}

20
core/src/action.hpp Normal file
View file

@ -0,0 +1,20 @@
/*
Copyright: © 2023 SIL International.
Description: Internal actions methods for Keyman Core
Create Date: 23 Oct 2023
Authors: Marc Durdin (MCD)
History: 23 Oct 2023 - MCD - Initial implementation
*/
#pragma once
#include <keyman/keyman_core_api.h>
namespace km {
namespace core
{
km_core_actions* action_item_list_to_actions_object(
km_core_action_item const *action_items
);
} // namespace core
} // namespace km

View file

@ -0,0 +1,64 @@
/*
Copyright: © 2023 SIL International.
Description: Implementation of the action API functions using internal
data structures and functions.
Create Date: 23 Oct 2023
Authors: Marc Durdin (MCD)
History: 23 Oct 2023 - MCD - Initial implementation.
*/
#include <cassert>
#include <algorithm>
#include <sstream>
#include <keyman/keyman_core_api.h>
#include "jsonpp.hpp"
#include "processor.hpp"
#include "state.hpp"
#include "action.hpp"
using namespace km::core;
km_core_actions* km_core_state_get_actions(
km_core_state const *state
) {
assert(state);
if(!state) {
return nullptr;
}
km_core_actions* actions = nullptr;
auto action_items = km_core_state_action_items(state, nullptr);
if(!action_items) {
return nullptr;
}
actions = action_item_list_to_actions_object(action_items);
return actions;
}
km_core_status km_core_actions_dispose(
km_core_actions* actions
) {
if(actions == nullptr) {
return KM_CORE_STATUS_OK;
}
if(actions->output) {
delete[] actions->output;
}
if(actions->persist_options) {
for(auto option = actions->persist_options; option->scope; option++) {
delete[] option->key;
delete[] option->value;
}
delete[] actions->persist_options;
}
delete actions;
return KM_CORE_STATUS_OK;
}

View file

@ -140,6 +140,13 @@ km_core_status km_core_context_items_to_utf16(km_core_context_item const *ci,
sz_ptr);
}
km_core_status km_core_context_items_to_utf32(km_core_context_item const *ci,
km_core_usv *buf, size_t * sz_ptr)
{
return _context_items_to<utf32>(ci,
reinterpret_cast<utf32::codeunit_t *>(buf),
sz_ptr);
}
void km_core_context_items_dispose(km_core_context_item *ci)
{

View file

@ -11,6 +11,7 @@
#include <cassert>
#include <algorithm>
#include <sstream>
#include <memory>
#include <keyman/keyman_core_api.h>
#include "jsonpp.hpp"
@ -259,3 +260,99 @@ void km_core_state_imx_deregister_callback(km_core_state *state)
}
state->imx_deregister_callback();
}
bool is_context_valid(km_core_cp const * context, km_core_cp const * cached_context) {
km_core_cp const* context_p = context;
while(*context_p) {
context_p++;
}
km_core_cp const* cached_context_p = cached_context;
while(*cached_context_p) {
cached_context_p++;
}
// we need to compare from the end of the cached context
for(; context_p >= context && cached_context_p >= cached_context; context_p--, cached_context_p--) {
if(*context_p != *cached_context_p) {
// The cached context doesn't match the application context, so it is
// invalid
return false;
}
}
if(cached_context_p > cached_context) {
// if the cached context is longer than the application context, then we also
// assume that it is invalid
return false;
}
// It's acceptable for the application context to be longer than the cached
// context, so if we match the whole cached context, we can safely return true
return true;
}
km_core_context_status km_core_state_context_set_if_needed(
km_core_state *state,
km_core_cp const *application_context
) {
assert(state != nullptr);
assert(application_context != nullptr);
if(state == nullptr || application_context == nullptr) {
return KM_CORE_CONTEXT_STATUS_INVALID_ARGUMENT;
}
size_t buf_size;
km_core_context_item* context_items = nullptr;
auto context = km_core_state_context(state);
if(km_core_context_get(context, &context_items) != KM_CORE_STATUS_OK) {
return KM_CORE_CONTEXT_STATUS_ERROR;
}
if(km_core_context_items_to_utf16(context_items, nullptr, &buf_size) != KM_CORE_STATUS_OK) {
km_core_context_items_dispose(context_items);
return KM_CORE_CONTEXT_STATUS_ERROR;
}
std::unique_ptr<km_core_cp[]> cached_context(new km_core_cp[buf_size]);
km_core_status status = km_core_context_items_to_utf16(context_items, cached_context.get(), &buf_size);
km_core_context_items_dispose(context_items);
if(status != KM_CORE_STATUS_OK) {
return KM_CORE_CONTEXT_STATUS_ERROR;
}
bool is_valid = is_context_valid(application_context, cached_context.get());
if(is_valid) {
// We keep the context as is
return KM_CORE_CONTEXT_STATUS_UNCHANGED;
}
km_core_context_item* new_context_items = nullptr;
// We replace the cached context with the current application context
status = km_core_context_items_from_utf16(application_context, &new_context_items);
if (status != KM_CORE_STATUS_OK) {
km_core_context_clear(context);
return KM_CORE_CONTEXT_STATUS_CLEARED;
}
km_core_context_set(context, new_context_items);
km_core_context_items_dispose(new_context_items);
return KM_CORE_CONTEXT_STATUS_UPDATED;
}
km_core_status km_core_state_context_clear(
km_core_state *state
) {
assert(state != nullptr);
if(state == nullptr) {
return KM_CORE_STATUS_INVALID_ARGUMENT;
}
km_core_context_clear(km_core_state_context(state));
return KM_CORE_STATUS_OK;
}

View file

@ -42,10 +42,12 @@ endif
kmx_files = files(
'action.cpp',
'option.cpp',
'keyboard.cpp',
'state.cpp',
'debuglog.cpp',
'km_core_action_api.cpp',
'km_core_context_api.cpp',
'km_core_keyboard_api.cpp',
'km_core_options_api.cpp',
@ -74,6 +76,7 @@ kmx_files = files(
)
api_files = files(
'km_core_action_api.cpp',
'km_core_context_api.cpp',
'km_core_keyboard_api.cpp',
'km_core_options_api.cpp',
@ -83,6 +86,7 @@ api_files = files(
)
core_files = files(
'action.cpp',
'option.cpp',
'keyboard.cpp',
'state.cpp',

View file

@ -37,7 +37,7 @@ public:
actions(Args&&... args);
void push_character(km_core_usv usv);
void push_marker(uintptr_t marker);
void push_marker(uint32_t marker);
void push_alert();
void push_backspace(km_core_backspace_type expected_type, uintptr_t expected_value = 0);
void push_persist(option const &);
@ -68,7 +68,7 @@ void actions::push_character(km_core_usv usv) {
inline
void actions::push_marker(uintptr_t marker) {
void actions::push_marker(uint32_t marker) {
assert(empty() || (!empty() && back().type != KM_CORE_IT_END));
emplace_back(km_core_action_item {KM_CORE_IT_MARKER, {0,}, {marker}});
}

View file

@ -0,0 +1,445 @@
/*
Copyright: © 2018 SIL International.
Description: Tests for the context API family of functions.
Create Date: 23 Oct 2023
Authors: Marc Durdin
History: 23 Oct 2023 - MCD - Initial implementation.
*/
#include <string>
#include <keyman/keyman_core_api.h>
#include "path.hpp"
#include "action.hpp"
#include <test_assert.h>
#include "../emscripten_filesystem.h"
const km_core_action_item alert_action_item();
const km_core_action_item bksp_action_item(uint8_t type, uintptr_t value);
const km_core_action_item caps_action_item(uint8_t capsLock);
const km_core_action_item char_action_item(km_core_usv chr);
const km_core_action_item emit_keystroke_action_item();
const km_core_action_item persist_opt_action_item(km_core_option_item const *option);
const km_core_action_item end_action_item();
const km_core_action_item invalidate_context_action_item();
const km_core_action_item marker_action_item(uint32_t marker);
//-------------------------------------------------------------------------------------
void test_two_backspaces() {
const km_core_action_item action_items[] = {
char_action_item('D'),
bksp_action_item(KM_CORE_BT_CHAR, 'D'),
bksp_action_item(KM_CORE_BT_CHAR, 'E'),
end_action_item()
};
km_core_actions *actions = km::core::action_item_list_to_actions_object(action_items);
assert(actions->code_points_to_delete == 1);
assert(std::u32string(actions->output) == U"");
assert(actions->persist_options != nullptr);
assert(actions->persist_options[0].key == nullptr);
assert(actions->persist_options[0].value == nullptr);
assert(actions->persist_options[0].scope == KM_CORE_OPT_UNKNOWN);
assert(actions->do_alert == false);
assert(actions->emit_keystroke == false);
assert(actions->new_caps_lock_state == -1);
try_status(km_core_actions_dispose(actions));
}
//-------------------------------------------------------------------------------------
void test_marker_text_interleaved() {
const km_core_action_item action_items[] = {
char_action_item('A'),
marker_action_item(1),
char_action_item('B'),
marker_action_item(2),
char_action_item('C'),
bksp_action_item(KM_CORE_BT_CHAR, 'C'),
bksp_action_item(KM_CORE_BT_MARKER, 2),
char_action_item('D'),
end_action_item()
};
km_core_actions *actions = km::core::action_item_list_to_actions_object(action_items);
assert(actions->code_points_to_delete == 0);
assert(std::u32string(actions->output) == U"ABD");
assert(actions->persist_options != nullptr);
assert(actions->persist_options[0].key == nullptr);
assert(actions->persist_options[0].value == nullptr);
assert(actions->persist_options[0].scope == KM_CORE_OPT_UNKNOWN);
assert(actions->do_alert == false);
assert(actions->emit_keystroke == false);
assert(actions->new_caps_lock_state == -1);
try_status(km_core_actions_dispose(actions));
}
//-------------------------------------------------------------------------------------
void test_alert() {
const km_core_action_item action_items[] = {
alert_action_item(),
end_action_item()
};
km_core_actions *actions = km::core::action_item_list_to_actions_object(action_items);
assert(actions->code_points_to_delete == 0);
assert(std::u32string(actions->output) == U"");
assert(actions->persist_options != nullptr);
assert(actions->persist_options[0].key == nullptr);
assert(actions->persist_options[0].value == nullptr);
assert(actions->persist_options[0].scope == KM_CORE_OPT_UNKNOWN);
assert(actions->do_alert == KM_CORE_TRUE);
assert(actions->emit_keystroke == KM_CORE_FALSE);
assert(actions->new_caps_lock_state == KM_CORE_CAPS_UNCHANGED);
try_status(km_core_actions_dispose(actions));
}
//-------------------------------------------------------------------------------------
void test_emit_keystroke() {
const km_core_action_item action_items[] = {
emit_keystroke_action_item(),
end_action_item()
};
km_core_actions *actions = km::core::action_item_list_to_actions_object(action_items);
assert(actions->code_points_to_delete == 0);
assert(std::u32string(actions->output) == U"");
assert(actions->persist_options != nullptr);
assert(actions->persist_options[0].key == nullptr);
assert(actions->persist_options[0].value == nullptr);
assert(actions->persist_options[0].scope == KM_CORE_OPT_UNKNOWN);
assert(actions->do_alert == KM_CORE_FALSE);
assert(actions->emit_keystroke == KM_CORE_TRUE);
assert(actions->new_caps_lock_state == KM_CORE_CAPS_UNCHANGED);
try_status(km_core_actions_dispose(actions));
}
//-------------------------------------------------------------------------------------
void test_invalidate_context() {
// note, this generates a no-op
const km_core_action_item action_items[] = {
invalidate_context_action_item(),
end_action_item()
};
km_core_actions *actions = km::core::action_item_list_to_actions_object(action_items);
assert(actions->code_points_to_delete == 0);
assert(std::u32string(actions->output) == U"");
assert(actions->persist_options != nullptr);
assert(actions->persist_options[0].key == nullptr);
assert(actions->persist_options[0].value == nullptr);
assert(actions->persist_options[0].scope == KM_CORE_OPT_UNKNOWN);
assert(actions->do_alert == KM_CORE_FALSE);
assert(actions->emit_keystroke == KM_CORE_FALSE);
assert(actions->new_caps_lock_state == KM_CORE_CAPS_UNCHANGED);
try_status(km_core_actions_dispose(actions));
}
//-------------------------------------------------------------------------------------
void test_persist_opt() {
const km_core_option_item option = {
u"key",
u"value",
KM_CORE_OPT_KEYBOARD
};
const km_core_action_item action_items[] = {
persist_opt_action_item(&option),
end_action_item()
};
km_core_actions *actions = km::core::action_item_list_to_actions_object(action_items);
assert(actions->code_points_to_delete == 0);
assert(std::u32string(actions->output) == U"");
assert(actions->persist_options != nullptr);
assert(std::u16string(actions->persist_options[0].key) == u"key");
assert(std::u16string(actions->persist_options[0].value) == u"value");
assert(actions->persist_options[0].scope == KM_CORE_OPT_KEYBOARD);
// verify that data is copied
assert(actions->persist_options[0].key != option.key);
assert(actions->persist_options[0].value != option.value);
// verify that we have a KM_CORE_OPTIONS_END term
assert(actions->persist_options[1].key == nullptr);
assert(actions->persist_options[1].value == nullptr);
assert(actions->persist_options[1].scope == KM_CORE_OPT_UNKNOWN);
assert(actions->do_alert == KM_CORE_FALSE);
assert(actions->emit_keystroke == KM_CORE_FALSE);
assert(actions->new_caps_lock_state == KM_CORE_CAPS_UNCHANGED);
try_status(km_core_actions_dispose(actions));
}
//-------------------------------------------------------------------------------------
// Context tests
//-------------------------------------------------------------------------------------
km_core_option_item test_env_opts[] =
{
KM_CORE_OPTIONS_END
};
km_core_keyboard * test_kb = nullptr;
km_core_state * test_state = nullptr;
km_core_context_item * citems = nullptr;
std::string arg_path;
void teardown() {
if(citems) {
km_core_context_items_dispose(citems);
citems = nullptr;
}
if(test_state) {
km_core_state_dispose(test_state);
test_state = nullptr;
}
if(test_kb) {
km_core_keyboard_dispose(test_kb);
test_kb = nullptr;
}
}
void setup(const char *keyboard, const km_core_cp* context) {
teardown();
km::core::path path = km::core::path::join(arg_path, keyboard);
try_status(km_core_keyboard_load(path.native().c_str(), &test_kb));
try_status(km_core_state_create(test_kb, test_env_opts, &test_state));
try_status(km_core_context_items_from_utf16(context, &citems));
try_status(km_core_context_set(km_core_state_context(test_state), citems));
}
bool is_identical_context(km_core_cp const *cached_context) {
size_t buf_size;
try_status(km_core_context_get(km_core_state_context(test_state), &citems));
try_status(km_core_context_items_to_utf16(citems, nullptr, &buf_size));
km_core_cp* new_cached_context = new km_core_cp[buf_size];
try_status(km_core_context_items_to_utf16(citems, new_cached_context, &buf_size));
bool result = std::u16string(cached_context) == new_cached_context;
delete[] new_cached_context;
return result;
}
void test_context_set_if_needed_identical_context() {
km_core_cp const *application_context = u"This is a test";
km_core_cp const *cached_context = u"This is a test";
setup("k_000___null_keyboard.kmx", cached_context);
assert(km_core_state_context_set_if_needed(test_state, application_context) == KM_CORE_CONTEXT_STATUS_UNCHANGED);
assert(is_identical_context(cached_context));
teardown();
}
void test_context_set_if_needed_different_context() {
km_core_cp const *application_context = u"This is a test";
km_core_cp const *cached_context = u"This isn't a test";
setup("k_000___null_keyboard.kmx", cached_context);
assert(km_core_state_context_set_if_needed(test_state, application_context) == KM_CORE_CONTEXT_STATUS_UPDATED);
assert(!is_identical_context(cached_context));
assert(is_identical_context(application_context));
teardown();
}
void test_context_set_if_needed_app_context_is_longer() {
km_core_cp const *application_context = u"Longer This is a test";
km_core_cp const *cached_context = u"This is a test";
setup("k_000___null_keyboard.kmx", cached_context);
assert(km_core_state_context_set_if_needed(test_state, application_context) == KM_CORE_CONTEXT_STATUS_UNCHANGED);
// Should be true -- longer, but what exists is identical to cached
assert(is_identical_context(cached_context));
teardown();
}
void test_context_set_if_needed_app_context_is_shorter() {
km_core_cp const *application_context = u"is a test";
km_core_cp const *cached_context = u"This is a test";
setup("k_000___null_keyboard.kmx", cached_context);
assert(km_core_state_context_set_if_needed(test_state, application_context) == KM_CORE_CONTEXT_STATUS_UPDATED);
// Should be false -- app ctxt is shorter, so doesn't matter that what we have
// matches
assert(!is_identical_context(cached_context));
assert(is_identical_context(application_context));
teardown();
}
void test_context_set_if_needed_cached_context_has_markers() {
km_core_cp const *application_context = u"123";
km_core_cp const *cached_context = u"123";
setup("k_000___null_keyboard.kmx", cached_context);
km_core_context_item const citems[] = {
{ KM_CORE_CT_MARKER, {0}, { 5 } },
{ KM_CORE_CT_CHAR, {0}, { '1' } },
{ KM_CORE_CT_MARKER, {0}, { 1 } },
{ KM_CORE_CT_CHAR, {0}, { '2' } },
{ KM_CORE_CT_MARKER, {0}, { 2 } },
{ KM_CORE_CT_CHAR, {0}, { '3' } },
{ KM_CORE_CT_MARKER, {0}, { 3 } },
{ KM_CORE_CT_MARKER, {0}, { 4 } },
KM_CORE_CONTEXT_ITEM_END
};
try_status(km_core_context_set(km_core_state_context(test_state), citems));
assert(km_core_state_context_set_if_needed(test_state, application_context) == KM_CORE_CONTEXT_STATUS_UNCHANGED);
km_core_context_item* citems_new;
try_status(km_core_context_get(km_core_state_context(test_state), &citems_new));
for(int i = 0; citems[i].type || citems_new[i].type; i++) {
assert(citems_new[i].type == citems[i].type);
if(citems[i].type == KM_CORE_CT_CHAR) {
assert(citems_new[i].character == citems[i].character);
} else {
assert(citems_new[i].marker == citems[i].marker);
}
}
teardown();
}
void test_context_set_if_needed() {
test_context_set_if_needed_identical_context();
test_context_set_if_needed_different_context();
test_context_set_if_needed_app_context_is_longer();
test_context_set_if_needed_app_context_is_shorter();
test_context_set_if_needed_cached_context_has_markers();
}
void test_context_clear() {
km_core_cp const *cached_context = u"This is a test";
setup("k_000___null_keyboard.kmx", cached_context);
try_status(km_core_state_context_clear(test_state));
assert(!is_identical_context(cached_context));
assert(is_identical_context(u""));
teardown();
}
//-------------------------------------------------------------------------------------
// Launcher
//-------------------------------------------------------------------------------------
constexpr const auto help_str = "\
action_api [--color] <SOURCE_PATH>\n\
\n\
--color Force color output\n\
SOURCE_PATH Path where debug_api.cpp is found; kmx files are\n\
located relative to this path.\n";
int error_args() {
std::cerr << "debug_api: Invalid arguments." << std::endl;
std::cout << help_str;
return 1;
}
int main(int argc, char *argv []) {
if(argc < 2) {
return error_args();
}
auto arg_color = std::string(argv[1]) == "--color";
if(arg_color && argc < 3) {
return error_args();
}
console_color::enabled = console_color::isaterminal() || arg_color;
#ifdef __EMSCRIPTEN__
arg_path = get_wasm_file_path(argv[arg_color ? 2 : 1]);
#else
arg_path = argv[arg_color ? 2 : 1];
#endif
// actions
test_two_backspaces();
test_marker_text_interleaved();
test_alert();
test_emit_keystroke();
test_invalidate_context();
// context -- todo move to another file
test_context_set_if_needed();
test_context_clear();
}
//-------------------------------------------------------------------------------------
// Helper functions
//-------------------------------------------------------------------------------------
const km_core_action_item alert_action_item() {
km_core_action_item res = {0};
res.type = KM_CORE_IT_ALERT;
return res;
}
const km_core_action_item bksp_action_item(uint8_t type, uintptr_t value) {
km_core_action_item res = {0};
res.type = KM_CORE_IT_BACK;
res.backspace.expected_type = type;
res.backspace.expected_value = value;
return res;
}
const km_core_action_item caps_action_item(uint8_t capsLock) {
km_core_action_item res = {0};
res.type = KM_CORE_IT_CAPSLOCK;
res.capsLock = capsLock;
return res;
}
const km_core_action_item char_action_item(km_core_usv chr) {
km_core_action_item res = {0};
res.type = KM_CORE_IT_CHAR;
res.character = chr;
return res;
}
const km_core_action_item emit_keystroke_action_item() {
km_core_action_item res = {0};
res.type = KM_CORE_IT_EMIT_KEYSTROKE;
return res;
}
const km_core_action_item persist_opt_action_item(km_core_option_item const *option) {
km_core_action_item res = {0};
res.type = KM_CORE_IT_PERSIST_OPT;
res.option = option;
return res;
}
const km_core_action_item end_action_item() {
km_core_action_item res = {0};
res.type = KM_CORE_IT_END;
return res;
}
const km_core_action_item invalidate_context_action_item() {
km_core_action_item res = {0};
res.type = KM_CORE_IT_INVALIDATE_CONTEXT;
return res;
}
const km_core_action_item marker_action_item(uint32_t marker) {
km_core_action_item res = {0};
res.type = KM_CORE_IT_MARKER;
res.character = marker;
return res;
}

View file

@ -402,8 +402,11 @@ void test_save_option() {
km_core_state_debug_item{KM_CORE_DEBUG_END, 0, {}, {u"", nullptr, nullptr, {}, 1}},
}));
km_core_action_item action = {KM_CORE_IT_PERSIST_OPT, {0,}, };
action.option = &opt;
assert(action_items(test_state, {
{KM_CORE_IT_PERSIST_OPT, {0,}, {uintptr_t(&opt)}},
action,
{KM_CORE_IT_END}
}));
}

View file

@ -16,6 +16,7 @@ endif
local_defns = ['-DKM_CORE_LIBRARY_STATIC']
tests = [
['action-api', 'action_api.cpp'],
['context-api', 'context_api.cpp'],
['keyboard-api', 'keyboard_api.cpp'],
['options-api', 'options_api.cpp'],

View file

@ -170,8 +170,10 @@ int main(int argc, char * argv[])
KM_CORE_MODIFIER_SHIFT, 1, KM_CORE_EVENT_FLAG_DEFAULT));
assert(action_items(test_state, {{KM_CORE_IT_CHAR, {0,}, {km_core_usv('L')}}, {KM_CORE_IT_END}}));
try_status(km_core_process_event(test_state, KM_CORE_VKEY_F2, 0, 1, KM_CORE_EVENT_FLAG_DEFAULT));
assert(action_items(test_state, {{KM_CORE_IT_PERSIST_OPT, {0,},
{uintptr_t(&expected_persist_opt)}}, {KM_CORE_IT_END}}));
km_core_action_item action = {KM_CORE_IT_PERSIST_OPT, {0,}, };
action.option = &expected_persist_opt;
assert(action_items(test_state, {action, {KM_CORE_IT_END}}));
// Test debug dump
auto doc1 = get_json_doc(*test_state),

View file

@ -29,6 +29,7 @@ export class KmpCompiler {
public transformKpsToKmpObject(kpsFilename: string): KmpJsonFile.KmpJsonFile {
const kps = this.loadKpsFile(kpsFilename);
if(!kps) {
// errors will already have been reported by loadKpsFile
return null;
}
return this.transformKpsFileToKmpObject(kpsFilename, kps);
@ -48,11 +49,19 @@ export class KmpCompiler {
let parser = new xml2js.Parser({
explicitArray: false
});
// TODO: add unit test for xml errors parsing .kps file
parser.parseString(data, (e: unknown, r: unknown) => { if(e) throw e; a = r as KpsFile.KpsPackage });
try {
parser.parseString(data, (e: unknown, r: unknown) => { if(e) throw e; a = r as KpsFile.KpsPackage });
} catch(e) {
this.callbacks.reportMessage(CompilerMessages.Error_InvalidPackageFile({e}));
}
return a;
})();
if(!kpsPackage) {
return null;
}
const kps: KpsFile.KpsFile = kpsPackage.Package;
return kps;
}

View file

@ -123,5 +123,9 @@ export class CompilerMessages {
static Hint_PackageContainsSourceFile = (o:{filename:string}) => m(this.HINT_PackageContainsSourceFile,
`The source file ${o.filename} should not be included in the package; instead include the compiled result.`);
static HINT_PackageContainsSourceFile = SevHint | 0x001D;
static Error_InvalidPackageFile = (o:{e:any}) => m(this.ERROR_InvalidPackageFile,
`Package source file is invalid: ${(o.e ?? 'unknown error').toString()}`);
static ERROR_InvalidPackageFile = SevError | 0x001E;
}

View file

@ -39,6 +39,10 @@ export class WindowsPackageInstallerCompiler {
public async compile(kpsFilename: string, sources: WindowsPackageInstallerSources): Promise<Uint8Array> {
const kps = this.kmpCompiler.loadKpsFile(kpsFilename);
if(!kps) {
// errors will already have been reported by loadKpsFile
return null;
}
// Check existence of required files
for(const filename of [sources.licenseFilename, sources.msiFilename, sources.setupExeFilename]) {

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<Package>
<System>
<KeymanDeveloperVersion>15.0.266.0</KeymanDeveloperVersion>
<FileVersion>7.0</FileVersion>
</System>
<Info>
<Name URL="">SENĆOŦEN (Saanich Dialect) Keyboard</Name>
<!-- error_invalid_package_file -->
<Copyright URL="">© 2019 National Research Council Canada & this test</Copyright>
<Author URL="mailto:Eddie.Santos@nrc-cnrc.gc.ca">Eddie Antonio Santos</Author>
<Version>1.0</Version>
</Info>
<Files>
<File>
<Name>basic.kmx</Name>
<Description>Keyboard Basic</Description>
<CopyLocation>0</CopyLocation>
<FileType>.kmx</FileType>
</File>
</Files>
<Keyboards>
<Keyboard>
<Name>Basic</Name>
<ID>basic</ID>
<Version>1.0</Version>
<Languages>
<Language ID="KM">Khmer</Language>
</Languages>
</Keyboard>
</Keyboards>
</Package>

View file

@ -226,4 +226,11 @@ describe('CompilerMessages', function () {
CompilerMessages.HINT_PackageContainsSourceFile);
});
// ERROR_InvalidPackageFile
it('should generate ERROR_InvalidPackageFile if package source file contains invalid XML', async function() {
testForMessage(this, ['invalid', 'error_invalid_package_file.kps'],
CompilerMessages.ERROR_InvalidPackageFile);
});
});

View file

@ -44,8 +44,9 @@ function loadDefaultProjectFromFolder(infile: string, callbacks: CompilerCallbac
function loadProjectFromFile(infile: string, callbacks: CompilerCallbacks): KeymanDeveloperProject {
const kpjData = callbacks.loadFile(infile);
const reader = new KPJFileReader(callbacks);
const kpj = reader.read(kpjData);
let kpj = null;
try {
kpj = reader.read(kpjData);
reader.validate(kpj);
} catch(e) {
callbacks.reportMessage(InfrastructureMessages.Error_InvalidProjectFile({message: (e??'').toString()}));

View file

@ -11,12 +11,12 @@ menuDropdown.onclick = (value) => {
menuDropdown.set(''); // we never show an 'active' package
if(value == '#install-keyman') {
let href = '';
switch(keyman.util.device.OS) {
case 'iOS': href = 'https://keyman.com/go/developer/'+versionMajor+'/ios-app'; break;
case 'Android': href = 'https://keyman.com/go/developer/'+versionMajor+'/android-app'; break;
case 'Linux': href = 'https://keyman.com/linux/download'; break;
case 'Windows': href = 'https://keyman.com/go/download/keyman-windows'; break;
case 'MacOSX': href = 'https://keyman.com/go/download/keyman-mac'; break;
switch(keyman.config.hostDevice.OS) { // note: KeymanWeb internal API
case 'ios': href = 'https://keyman.com/go/developer/'+versionMajor+'/ios-app'; break;
case 'android': href = 'https://keyman.com/go/developer/'+versionMajor+'/android-app'; break;
case 'linux': href = 'https://keyman.com/linux/download'; break;
case 'windows': href = 'https://keyman.com/go/download/keyman-windows'; break;
case 'macosx': href = 'https://keyman.com/go/download/keyman-mac'; break;
default: href = 'https://keyman.com/downloads'; break;
}
location.href = href;

View file

@ -216,14 +216,14 @@ window.onload = function() {
if(newOSK) {
document.getElementById('osk-host').removeChild(newOSK.element);
keyman.osk = null;
keyman.osk = null; // Note: undocumented KeymanWeb API
}
// Create a new on screen keyboard view and tell KeymanWeb that
// we are using the targetDevice for context input.
newOSK = new keyman.views.InlinedOSKView(keyman, { device: targetDevice });
keyman.core.contextDevice = targetDevice;
keyman.osk = newOSK;
newOSK = new keyman.views.InlinedOSKView(keyman, { device: targetDevice }); // Note: KeymanWeb internal API
keyman.core.contextDevice = targetDevice; // Note: KeymanWeb internal API
keyman.osk = newOSK; // Note: undocumented KeymanWeb API
if(document.body.offsetWidth < targetDevice.dimensions[0]) {
newOSK.setSize('320px', '200px');
@ -237,8 +237,8 @@ window.onload = function() {
keyman.addEventListener('keyboardchange', function(keyboardProperties) {
if(newOSK) {
keyman.osk = newOSK;
newOSK.activeKeyboard = keyman.contextManager.activeKeyboard; // Private API refs on both sides
keyman.osk = newOSK; // Note: undocumented KeymanWeb API
newOSK.activeKeyboard = keyman.contextManager.activeKeyboard; // Note: undocumented KeymanWeb API refs on both sides
}
keyboardDropdown.set(keyboardProperties.internalName);
window.sessionStorage.setItem('current-keyboard', keyboardProperties.internalName);
@ -284,7 +284,7 @@ function unloadKeyboardsAndModels() {
const lastModel = keyman.core.activeModel;
if(lastModel) {
console.log('Unregistering model '+lastModel.id);
keyman.removeModel(lastModel.id);
keyman.removeModel(lastModel.id); // Note: undocumented KeymanWeb API
}
modelDropdown.removeAll();

View file

@ -192,7 +192,7 @@ type
_reserved: array[0..2] of uint8_t;
{$ENDIF}
case Integer of
0: (marker: uintptr_t);
0: (marker: uint32_t);
1: (option: pkm_core_option_item);
2: (character: km_core_usv);
3: (backspace: km_core_backspace_item);

View file

@ -108,7 +108,7 @@
</div>
<div class='toolbar-item' id='key-cap-unicode-toolbar-item'>
<label for='inpKeyCapUnicode'>Unicode:</label>
<label for='inpKeyCapUnicode'>Text Unicode:</label>
<input id='inpKeyCapUnicode' type='text' size='16' />
</div>
@ -118,7 +118,7 @@
</div>
<div class='toolbar-item' id='key-hint-unicode-toolbar-item'>
<label for='inpKeyHintUnicode'>Unicode:</label>
<label for='inpKeyHintUnicode'>Hint Unicode:</label>
<input id='inpKeyHintUnicode' type='text' size='16' />
</div>
@ -221,7 +221,7 @@
<input id='inpSubKeyCap' type='text' size='8' />
</div>
<div class='toolbar-item' id='sub-key-cap-unicode-toolbar-item'>
<label>Unicode:</label>
<label>Text Unicode:</label>
<input id='inpSubKeyCapUnicode' type='text' size='16' />
</div>
<div class='toolbar-item'>

View file

@ -1,6 +1,7 @@
libkeymancore.so.1 libkeymancore #MINVER#
* Build-Depends-Package: libkeymancore-dev
km_core_actions_dispose@Base 17.0.197
km_core_context_append@Base 17.0.195
km_core_context_clear@Base 17.0.195
km_core_context_get@Base 17.0.195
@ -9,6 +10,7 @@ libkeymancore.so.1 libkeymancore #MINVER#
km_core_context_items_from_utf16@Base 17.0.195
km_core_context_items_from_utf8@Base 17.0.195
km_core_context_items_to_utf16@Base 17.0.195
km_core_context_items_to_utf32@Base 17.0.197
km_core_context_items_to_utf8@Base 17.0.195
km_core_context_length@Base 17.0.195
km_core_context_set@Base 17.0.195
@ -28,11 +30,14 @@ libkeymancore.so.1 libkeymancore #MINVER#
km_core_state_action_items@Base 17.0.195
km_core_state_clone@Base 17.0.195
km_core_state_context@Base 17.0.195
km_core_state_context_clear@Base 17.0.197
km_core_state_context_set_if_needed@Base 17.0.197
km_core_state_create@Base 17.0.195
km_core_state_debug_get@Base 17.0.195
km_core_state_debug_items@Base 17.0.195
km_core_state_debug_set@Base 17.0.195
km_core_state_dispose@Base 17.0.195
km_core_state_get_actions@Base 17.0.197
km_core_state_get_intermediate_context@Base 17.0.195
km_core_state_imx_deregister_callback@Base 17.0.195
km_core_state_imx_register_callback@Base 17.0.195

View file

@ -49,7 +49,7 @@ TEST_F(KMPROCESSACTIONS, processMarkertest) {
WCHAR callbuf[MAXCONTEXT];
AITIP testApp;
WCHAR expectedContext[] = {UC_SENTINEL, CODE_DEADKEY, 2, 0};
uintptr_t marker = 2;
uint32_t marker = 2;
km_core_action_item itemAddMarker = {KM_CORE_IT_MARKER, {0,}, {marker}};
processMarker(&testApp, &itemAddMarker);
@ -67,7 +67,7 @@ TEST_F(KMPROCESSACTIONS, processBackDeadkeytest) {
km_core_action_item itemAddChar = {KM_CORE_IT_CHAR, {0,}, {'A'}};
processUnicodeChar(&testApp, &itemAddChar);
uintptr_t marker = 2;
uint32_t marker = 2;
km_core_action_item itemAddMarker = {KM_CORE_IT_MARKER, {0,}, {marker}};
processMarker(&testApp, &itemAddMarker);
km_core_action_item itemBackSpace = {KM_CORE_IT_BACK};