Merge branch 'epic/web-core' into chore/merge-master-into-web-core

This commit is contained in:
Marc Durdin 2024-11-08 05:29:28 +01:00 committed by GitHub
commit a8093460fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
87 changed files with 1580 additions and 489 deletions

View file

@ -1007,7 +1007,11 @@ Provides read-only information about a keyboard.
typedef struct {
km_core_cu const * version_string;
km_core_cu const * id;
// TODO-web-core: Deprecate this field (#12497)
// KMN_DEPRECATED
km_core_path_name folder_path;
km_core_option_item const * default_options;
} km_core_keyboard_attrs;
@ -1022,7 +1026,7 @@ typedef struct {
: Keyman keyboard ID string.
`folder_path`
: Path to the unpacked folder containing the keyboard and associated resources.
: Path to the unpacked folder containing the keyboard and associated resources (deprecated).
`default_options`
: Set of default values for any options included in the keyboard.
@ -1096,12 +1100,16 @@ typedef struct {
## 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 paramter.
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,
@ -1140,6 +1148,60 @@ km_core_keyboard_load(km_core_path_name kb_path,
-------------------------------------------------------------------------------
# km_core_keyboard_load_from_blob()
## Description
Parse and load keyboard from the supplied blob and a pointer to the loaded keyboard
into the out paramter.
## Specification
```c */
KMN_API
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);
/*
```
## Parameters
`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
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 `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_dispose()
## Description

View file

@ -23,7 +23,6 @@
#define _kmn_unused(x) UNUSED_ ## x __attribute__((__unused__))
#else
#define _kmn_unused(x) UNUSED_ ## x
#endif
#if defined _WIN32 || defined __CYGWIN__
@ -36,7 +35,7 @@
#undef _kmn_static_flag
#else // How MSVC sepcifies function level attributes adn deprecation
#define _kmn_and
#define _kmn_tag_fn(a) __declspec(a)
#define _kmn_tag_fn(a) __declspec(a)
#define _kmn_deprecated_flag deprecated
#endif
#define _kmn_export_flag dllexport
@ -48,6 +47,8 @@
#define _KM_CORE_EXT_SEPARATOR ('.')
#endif
#define KMN_DEPRECATED _kmn_tag_fn(_kmn_deprecated_flag)
#if defined KM_CORE_LIBRARY_STATIC
#define KMN_API _kmn_tag_fn(_kmn_static_flag)
#define KMN_DEPRECATED_API _kmn_tag_fn(_kmn_deprecated_flag _kmn_and _kmn_static_flag)

View file

@ -17,18 +17,16 @@ void keyboard_attributes::render()
// Make attributes point to the stored values above.
id = _keyboard_id.c_str();
version_string = _version_string.c_str();
folder_path = _folder_path.c_str();
default_options = _default_opts.data();
}
keyboard_attributes::keyboard_attributes(std::u16string const & kbid,
std::u16string const & version,
path_type const & path,
options_store const &opts)
: _keyboard_id(kbid),
_version_string(version),
_folder_path(path),
_folder_path(""),
_default_opts(opts)
{
// Ensure that the default_options array will be properly terminated.
@ -40,7 +38,7 @@ keyboard_attributes::keyboard_attributes(std::u16string const & kbid,
keyboard_attributes::keyboard_attributes(keyboard_attributes &&rhs)
: _keyboard_id(std::move(rhs._keyboard_id)),
_version_string(std::move(rhs._version_string)),
_folder_path(std::move(rhs._folder_path)),
_folder_path(""),
_default_opts(std::move(rhs._default_opts))
{
rhs.id = rhs.version_string = nullptr;
@ -58,7 +56,6 @@ json & km::core::operator << (json & j, km::core::keyboard_attributes const & kb
{
j << json::object
<< "id" << kb.id
<< "folder" << kb._folder_path
<< "version" << kb.version_string
<< "rules" << json::array << json::close;

View file

@ -26,6 +26,7 @@ namespace core
{
std::u16string _keyboard_id;
std::u16string _version_string;
// unused and deprecated
core::path _folder_path;
std::vector<option> _default_opts;
@ -33,7 +34,6 @@ namespace core
public:
using options_store = decltype(_default_opts);
using path_type = decltype(_folder_path);
keyboard_attributes()
: km_core_keyboard_attrs {nullptr, nullptr, nullptr, nullptr} {}
@ -42,7 +42,6 @@ namespace core
keyboard_attributes(std::u16string const & id,
std::u16string const & version,
path_type const & path,
options_store const &opts);
keyboard_attributes & operator = (keyboard_attributes const &) = delete;
@ -52,8 +51,6 @@ namespace core
options_store const & default_opts_store() const noexcept { return _default_opts; }
options_store & default_opts_store() noexcept { return _default_opts; }
path_type const & path() const noexcept { return _folder_path; }
};
json & operator << (json &, km::core::keyboard_attributes const &);

View file

@ -14,47 +14,125 @@
#include "keyman_core.h"
#include "keyboard.hpp"
#include "processor.hpp"
#include "kmx/kmx_processor.hpp"
#include "ldml/ldml_processor.hpp"
#include "mock/mock_processor.hpp"
#include "processor.hpp"
#include "utfcodec.hpp"
using namespace km::core;
namespace
{
abstract_processor * processor_factory(path const & kb_path) {
// Some legacy packages may include upper-case file extensions
// TODO-LDML: move file io out of core and into engine
if (kb_path.suffix() == ".kmx" || kb_path.suffix() == ".KMX") {
std::vector<uint8_t> buf;
if(ldml_processor::is_kmxplus_file(kb_path, buf)) {
abstract_processor * result = new ldml_processor(kb_path, buf);
return result;
}
return new kmx_processor(kb_path);
abstract_processor* processor_factory(path const & kb_name, const std::vector<uint8_t> & buf) {
if (ldml_processor::is_handled(buf)) {
return new ldml_processor(kb_name, buf);
}
else if (kb_path.suffix() == ".mock") {
return new mock_processor(kb_path);
if (kmx_processor::is_handled(buf)) {
return new kmx_processor(kb_name, buf);
}
else {
return new null_processor();
if (mock_processor::is_handled(buf)) {
return new mock_processor(kb_name);
}
return new null_processor();
}
} // namespace
}
km_core_status
km_core_keyboard_load(km_core_path_name kb_path, km_core_keyboard **keyboard)
keyboard_load_from_blob_internal(
const km_core_path_name kb_name,
const std::vector<uint8_t> & buf,
km_core_keyboard** keyboard
) {
assert(keyboard);
if (!keyboard) {
return KM_CORE_STATUS_INVALID_ARGUMENT;
}
*keyboard = nullptr;
try {
abstract_processor* kp = processor_factory(kb_name, buf);
km_core_status 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;
}
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)
if (!keyboard || !kb) {
return KM_CORE_STATUS_INVALID_ARGUMENT;
}
path const kb_path(kb);
try
{
abstract_processor *kp = processor_factory(kb_path);
km_core_status status = kp->validate();
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;
@ -67,6 +145,7 @@ km_core_keyboard_load(km_core_path_name kb_path, km_core_keyboard **keyboard)
}
return KM_CORE_STATUS_OK;
}
// END DEPRECATED
void
km_core_keyboard_dispose(km_core_keyboard *keyboard)

View file

@ -13,9 +13,9 @@ using namespace kmx;
#include <share.h>
#endif
KMX_BOOL KMX_ProcessEvent::Load(km_core_path_name KeyboardName)
{
if(!LoadKeyboard(KeyboardName, &m_keyboard.Keyboard)) return FALSE; // I5136
KMX_BOOL KMX_ProcessEvent::Load(PKMX_BYTE buf, size_t sz) {
if(!LoadKeyboardFromBlob(buf, sz, &m_keyboard.Keyboard))
return FALSE; // I5136
return TRUE;
}
@ -51,50 +51,22 @@ const int km::core::kmx::CODE__SIZE[] = {
// Ensure that all CODE_### sizes are defined
static_assert(sizeof(CODE__SIZE) / sizeof(CODE__SIZE[0]) == (CODE_LASTCODE + 1), "Size of array CODE__SIZE not correct");
KMX_BOOL KMX_ProcessEvent::LoadKeyboard(km_core_path_name fileName, LPKEYBOARD *lpKeyboard)
{
PKMX_BYTE buf;
FILE *fp;
KMX_BOOL KMX_ProcessEvent::LoadKeyboardFromBlob(
PKMX_BYTE original_buf,
size_t sz,
LPKEYBOARD* lpKeyboard
) {
LPKEYBOARD kbp;
PKMX_BYTE buf;
PKMX_BYTE filebase;
DebugLog("Loading file '%s'", fileName);
if(!fileName || !lpKeyboard)
{
DebugLog("Bad Filename");
if (!lpKeyboard || !original_buf) {
DebugLog("Invalid parameter");
return FALSE;
}
#if defined(_WIN32) || defined(_WIN64)
fp = _wfsopen(fileName, L"rb", _SH_DENYWR);
#else
fp = fopen(fileName, "rb");
#endif
if(fp == NULL)
{
DebugLog("Could not open file");
return FALSE;
}
if (fseek(fp, 0, SEEK_END) != 0) {
fclose(fp);
DebugLog("Could not fseek file");
return FALSE;
}
auto sz = ftell(fp);
if (sz < 0) {
fclose(fp);
return FALSE;
}
if (fseek(fp, 0, SEEK_SET) != 0) {
fclose(fp);
DebugLog("Could not fseek(set) file");
return FALSE;
}
*lpKeyboard = NULL;
#ifdef KMX_64BIT
// allocate enough memory for expanded data structure + original data.
@ -108,57 +80,55 @@ KMX_BOOL KMX_ProcessEvent::LoadKeyboard(km_core_path_name fileName, LPKEYBOARD *
buf = new KMX_BYTE[sz];
#endif
if(!buf)
{
fclose(fp);
if (!buf) {
DebugLog("Not allocmem");
return FALSE;
}
#ifdef KMX_64BIT
filebase = buf + sz*2;
filebase = buf + sz * 2;
#else
filebase = buf;
#endif
memcpy(filebase, original_buf, sz);
if (fread(filebase, 1, sz, fp) < (size_t) sz) {
fclose(fp);
DebugLog("Could not read file");
if (*PKMX_DWORD(filebase) != KMX_DWORD(FILEID_COMPILED)) {
DebugLog("Invalid keyboard - signature is invalid");
delete[] buf;
return FALSE;
}
fclose(fp);
if(*PKMX_DWORD(filebase) != KMX_DWORD(FILEID_COMPILED))
{
delete [] buf;
DebugLog("Invalid file - signature is invalid");
if (!VerifyKeyboard(filebase, sz)) {
DebugLog("Verify keyboard failed");
delete[] buf;
return FALSE;
}
if(!VerifyKeyboard(filebase, sz)) return FALSE;
#ifdef KMX_64BIT
kbp = CopyKeyboard(buf, filebase);
#else
kbp = FixupKeyboard(buf, filebase);
#endif
if(!kbp) return FALSE;
if (!kbp) {
DebugLog("Can't copy/fixup keyboard");
delete[] buf;
return FALSE;
}
if(kbp->dwIdentifier != FILEID_COMPILED) {
delete [] buf;
if (kbp->dwIdentifier != FILEID_COMPILED) {
DebugLog("errNotFileID");
delete[] buf;
return FALSE;
}
*lpKeyboard = kbp;
return TRUE;
}
PKMX_WCHAR KMX_ProcessEvent::StringOffset(PKMX_BYTE base, KMX_DWORD offset)
{
if(offset == 0) return NULL;
if(offset == 0)
return NULL;
return (PKMX_WCHAR)(base + offset);
}

View file

@ -54,7 +54,7 @@ private:
KMX_DWORD m_modifiers = 0;
/* File loading */
KMX_BOOL LoadKeyboard(km_core_path_name fileName, LPKEYBOARD *lpKeyboard);
KMX_BOOL LoadKeyboardFromBlob(PKMX_BYTE buf, size_t sz, LPKEYBOARD* lpKeyboard);
KMX_BOOL VerifyKeyboard(PKMX_BYTE filebase, size_t sz);
KMX_BOOL VerifyChecksum(PKMX_BYTE buf, size_t sz);
#ifdef KMX_64BIT
@ -96,7 +96,7 @@ public:
KMX_ProcessEvent();
~KMX_ProcessEvent();
KMX_BOOL Load(km_core_path_name keyboardName);
KMX_BOOL Load(PKMX_BYTE buf, size_t sz);
KMX_BOOL ProcessEvent(km_core_state *state, KMX_UINT vkey, KMX_DWORD modifiers, KMX_BOOL isKeyDown); // returns FALSE on error or key not matched
KMX_Actions *GetActions();

View file

@ -38,9 +38,8 @@ km_core_status kmx_processor::validate() const {
return _valid ? KM_CORE_STATUS_OK : KM_CORE_STATUS_INVALID_KEYBOARD;
}
kmx_processor::kmx_processor(core::path p) {
p.replace_extension(".kmx");
_valid = bool(_kmx.Load(p.c_str()));
kmx_processor::kmx_processor(std::u16string const& kb_name, const std::vector<uint8_t>& data) {
_valid = bool(_kmx.Load((PKMX_BYTE)data.data(), data.size()));
if (!_valid)
return;
@ -57,8 +56,7 @@ kmx_processor::kmx_processor(core::path p) {
auto v = _kmx.GetKeyboard()->Keyboard->version;
auto vs = std::to_string(v >> 16) + "." + std::to_string(v & 0xffff);
_attributes = keyboard_attributes(static_cast<std::u16string>(p.stem()),
std::u16string(vs.begin(), vs.end()), p.parent(), defaults);
_attributes = keyboard_attributes(kb_name, std::u16string(vs.begin(), vs.end()), defaults);
}
char16_t const *
@ -414,3 +412,24 @@ km_core_keyboard_imx * kmx_processor::get_imx_list() const {
return imx_list;
}
/**
* Returns true the data is a KMX file, i.e. starts with 'KXTS'.
*
* @param data the keyboard blob
* @return true if the processor can handle the keyboard, otherwise false.
*/
bool kmx_processor::is_handled(const std::vector<uint8_t>& data) {
if (data.empty()) {
return false;
}
if (data.size() < sizeof(COMP_KEYBOARD)) { // a KMX file is at least 64 bytes (KMX header)
return false;
}
if (data.size() >= KMX_MAX_ALLOWED_FILE_SIZE) {
return false;
}
return ((kmx::PCOMP_KEYBOARD)data.data())->dwIdentifier == KMX_DWORD(FILEID_COMPILED); // 'KXTS'
}

View file

@ -29,7 +29,7 @@ namespace core
);
public:
kmx_processor(path);
kmx_processor(std::u16string const& kb_name, const std::vector<uint8_t>& data);
km_core_status
process_event(
@ -84,6 +84,8 @@ namespace core
supports_normalization() const override {
return false;
}
static bool is_handled(const std::vector<uint8_t>& buf);
};
} // namespace core

185
core/src/layout.hpp Normal file
View file

@ -0,0 +1,185 @@
/*
* Keyman is copyright (C) SIL International. MIT License.
*
* Keyman Keyboard Processor API - On-Screen Keyboard Layout Interfaces
*/
#pragma once
#include <string>
#include <map>
#include <vector>
#include "keyman_core_api.h"
#if defined(__cplusplus)
extern "C" {
#endif
/**
* Possible directions of a flick
*/
enum keyboard_layout_flick_direction {
/** flick up (north) */
n = 0,
/** flick down (south) */
s = 1,
/** flick right (east) */
e = 2,
/** flick left (west) */
w = 3,
/** flick up-right (north-east) */
ne = 4,
/** flick up-left (north-west) */
nw = 5,
/** flick down-right (south-east) */
se = 6,
/** flick down-left (south-west) */
sw = 7
};
/**
* key type like regular key, framekeys, deadkeys, blank, etc.
*/
enum keyboard_layout_key_type {
/** regular key */
normal = 0,
/** A 'frame' key, such as Shift or Enter */
special = 1,
/** A 'frame' key, such as Shift or Enter, which is is active, such as
* the shift key on a shift layer */
specialActive = 2,
/** **KeymanWeb runtime private use:** a variant of `special` with the
* keyboard font rather than 'KeymanwebOsk' font */
customSpecial = 3,
/** **KeymanWeb runtime private use:** a variant of `specialActive` with the
* keyboard font rather than 'KeymanwebOsk' font. */
customSpecialActive = 4,
/** A deadkey */
deadkey = 8,
/** A key which is rendered as a blank keycap, should block any interaction */
blank = 9,
/** Renders the key only as a gap or spacer, should block any interaction */
spacer = 10
};
/**
* A key on a touch layout/on-screen keyboard
*/
struct keyboard_layout_key {
/** key id */
std::u16string id; // TODO-WEB-CORE: perhaps necessary for special keys, Enter, etc? or can we get that from virtualKey?
/** the virtual key code */
int virtualKey; // TODO-WEB-CORE: do we need this? both id and virtualKey? Or just one of them?
/** text to display on key cap */
std::u16string display;
/** hint e.g. for longpress */
std::u16string hint;
/** the type of key */
keyboard_layout_key_type type;
/**
* the modifier combination (not layer) that should be used in key events,
* for this key, overriding the layer that the key is a part of.
*/
int modifiersOverride;
/** the next layer to switch to after this key is pressed */
std::u16string nextLayerId;
// touch layouts only
/** padding - space to the left of key (in what units?) */
int gap;
/** width of the key (in what units?) */
int width;
/** longpress keys, also known as subkeys */
std::vector<keyboard_layout_key> longpresses;
/** multitaps */
std::vector<keyboard_layout_key> multiTaps;
/** flicks */
std::map<keyboard_layout_flick_direction, keyboard_layout_key> flicks;
};
/**
* a row of keys on a touch layout/on-screen keyboard
*/
struct keyboard_layout_row {
/** row id */
int id; // TODO-WEB-CORE: do we need this? Web has it (`TouchLayoutRow`)
/** keys in this row */
std::vector<keyboard_layout_key> keys;
};
/**
* a layer with rows of keys on a touch layout/on-screen keyboard
*/
struct keyboard_layout_layer {
/** layer id */
std::u16string id;
/** layer modifiers */
// TODO-WEB-CORE: we added this during our discussion, but Web doesn't have it.
// Should be an enum if it's needed.
int modifiers; //? 0 = default, n = shift, etc. -1 = unspecified?
/** rows in this layer */
std::vector<keyboard_layout_row> rows;
};
/**
* layout specification for a specific platform like desktop, phone or tablet
*/
struct keyboard_layout_platform {
/** platform form factor, e.g. 'iso', 'touch', 'ansi', ... (see ldml spec) */
std::u16string form;
/** width of screen for touch layout */
int minWidthMm; // we don't have mobile vs tablet, instead use this
/** layers for this platform */
std::vector<keyboard_layout_layer> layers;
// TODO-WEB-CORE: Do we need these:
// Web additionally has:
// - font (should be in CSS; we have it in `keyboard_layout`)
// - fontsize (should be in CSS; we have it in `keyboard_layout`)
// - displayUnderlying
// - defaultHint ("none"|"dot"|"longpress"|"multitap"|"flick"|"flick-n"|"flick-ne"|
// "flick-e"|"flick-se"|"flick-s"|"flick-sw"|"flick-w"|"flick-nw")
};
/**
* On screen keyboard description consisting of specific layouts for different
* form factors.
*/
struct keyboard_layout {
/** layouts for different form factors */
std::vector<keyboard_layout_platform> platforms;
/** font face name to use for key caps*/
std::string fontFacename;
/** font size to use for key caps */
int fontSizeEm; // TODO-WEB-CORE: em? px? something else?
};
/**
* Get the on-screen keyboard layout for the specified keyboard.
*
* @param keyboard [in] The keyboard to get the layout for.
* @param layout [out] The on-screen keyboard layout.
* @return km_core_status `KM_CORE_STATUS_OK`: On success.
* `KM_CORE_STATUS_INVALID_ARGUMENT`: If `keyboard` is not a valid keyboard or `layout` is null.
*/
km_core_status
keyboard_get_layout(
km_core_keyboard const* keyboard,
keyboard_layout** layout
);
/**
* Dispose the on-screen keyboard layout.
*/
void
keyboard_layout_dispose(keyboard_layout* layout);
#if defined(__cplusplus)
}
#endif

View file

@ -5,7 +5,6 @@
Authors: Marc Durdin (MD)
*/
#include <fstream>
#include <algorithm>
#include "ldml/ldml_processor.hpp"
#include "ldml/ldml_transforms.hpp"
@ -15,6 +14,7 @@
#include "kmx/kmx_plus.h"
#include "kmx/kmx_xstring.h"
#include "kmx/kmx_processevent.h"
#include "kmx/kmx_processor.hpp"
#include "ldml/keyman_core_ldml.h"
#include "kmx/kmx_file_validator.hpp"
#include "debuglog.h"
@ -36,19 +36,15 @@ namespace {
namespace km {
namespace core {
ldml_processor::ldml_processor(path const & kb_path, const std::vector<uint8_t> &data)
: abstract_processor(
keyboard_attributes(kb_path.stem(), KM_CORE_LMDL_PROCESSOR_VERSION, kb_path.parent(), {})
), _valid(false), transforms(), bksp_transforms(), keys(), normalization_disabled(false)
{
ldml_processor::ldml_processor(std::u16string const& kb_name, const std::vector<uint8_t>& data)
: abstract_processor(keyboard_attributes(kb_name, KM_CORE_LMDL_PROCESSOR_VERSION, {})),
_valid(false), transforms(), bksp_transforms(), keys(), normalization_disabled(false) {
if(data.size() <= sizeof(kmx::COMP_KEYBOARD_EX)) {
DebugLog("data.size %zu too small", data.size());
return;
}
// // Locate the structs here, but still retain ptrs to the raw structs.
// Locate the structs here, but still retain ptrs to the raw structs.
kmx::KMX_FileValidator* comp_keyboard = (kmx::KMX_FileValidator*)data.data();
// Perform the standard validation
@ -116,40 +112,24 @@ ldml_processor::ldml_processor(path const & kb_path, const std::vector<uint8_t>
_valid = true;
}
bool ldml_processor::is_kmxplus_file(path const & kb_path, std::vector<uint8_t>& data) {
// TODO-LDML: we should refactor all the core components to delegate file loading
// to the Engine, which requires an API change, but this makes delivery
// of keyboard files more flexible under more WASM.
std::ifstream file(static_cast<std::string>(kb_path), std::ios::binary | std::ios::ate);
if(!file.good()) {
/**
* Returns true if the data is a KMX+ file.
*
* @param data the keyboard blob
* @return true if the processor can handle the keyboard, otherwise false.
*/
bool ldml_processor::is_handled(const std::vector<uint8_t>& data) {
// Check if it's a blob from a KMX file
if (!kmx_processor::is_handled(data)) {
return false;
}
const std::streamsize size = file.tellg();
if(size >= KMX_MAX_ALLOWED_FILE_SIZE) {
return false;
}
file.seekg(0, std::ios::beg);
data.resize((size_t)size);
if(!file.read((char *) data.data(), size)) {
return false;
}
file.close();
const kmx::PCOMP_KEYBOARD comp_keyboard = (kmx::PCOMP_KEYBOARD)data.data();
if(comp_keyboard->dwIdentifier != KMX_DWORD(FILEID_COMPILED)) {
if (comp_keyboard->dwFileVersion < VERSION_160 || (comp_keyboard->dwFlags & KF_KMXPLUS) == 0) {
return false;
}
if(comp_keyboard->dwFileVersion < VERSION_160 || (comp_keyboard->dwFlags & KF_KMXPLUS) == 0) {
return false;
}
// A KMXPlus file is in the buffer (although more validation is required and will
// The buffer contains KMXPlus data (although more validation is required and will
// be done in the constructor)
return true;
}

View file

@ -30,13 +30,12 @@ class ldml_event_state;
class ldml_processor : public abstract_processor {
public:
ldml_processor(
path const & kb_path,
std::u16string const& kb_name,
const std::vector<uint8_t> & data
);
static bool is_kmxplus_file(
path const & kb_path,
std::vector<uint8_t>& data
const std::vector<uint8_t> & data
);
km_core_status
@ -86,9 +85,11 @@ class ldml_processor : public abstract_processor {
return !normalization_disabled;
}
static bool is_handled(const std::vector<uint8_t> & buf);
private:
/** process a key-up */
void process_key_up(ldml_event_state &ldml_state) const;
void process_key_up(ldml_event_state& ldml_state) const;
/** process a key-down (if it wasn't handled exceptionally) */
void process_key_down(ldml_event_state &ldml_state) const;

View file

@ -118,6 +118,7 @@ api_files = files(
'km_core_state_api.cpp',
'km_core_debug_api.cpp',
'km_core_processevent_api.cpp',
'wasm.cpp',
)
core_files = files(
@ -133,6 +134,16 @@ mock_files = files(
'mock/mock_processor.cpp',
)
if cpp_compiler.get_id() == 'emscripten'
host_links = ['--whole-archive', '-sALLOW_MEMORY_GROWTH=1', '-sMODULARIZE=1',
'-sEXPORT_ES6', '-sENVIRONMENT=web,webview',
'--emit-tsd', 'km-core-interface.d.ts', '-sERROR_ON_UNDEFINED_SYMBOLS=0']
links += ['-sEXPORTED_RUNTIME_METHODS=[\'UTF8ToString\',\'stringToNewUTF8\',\'wasmExports\']',
# Forcing inclusion of debug symbols
'-g', '-Wlimited-postlink-optimizations', '--bind']
endif
lib = library('keymancore',
api_files,
core_files,
@ -160,3 +171,24 @@ pkg.generate(
description: 'Keyman processor for KMN keyboards.',
subdirs: headerdirs,
libraries: lib)
if cpp_compiler.get_id() == 'emscripten'
# Build an executable
host = executable('km-core',
cpp_args: defns,
include_directories: inc,
link_args: links + host_links,
objects: lib.extract_all_objects(recursive: false))
if get_option('buildtype') == 'release'
# TODO: #12888
# Split debug symbols into separate wasm file for release builds only
# as the release symbols will be uploaded to sentry
# custom_target('core.wasm',
# depends: host,
# input: host,
# output: 'core.wasm',
# command: ['wasm-split', '@OUTDIR@/core.wasm', '-o', '@OUTPUT@', '--strip', '--debug-out=@OUTDIR@/core.debug.wasm'],
# build_by_default: true)
endif
endif

View file

@ -3,7 +3,7 @@
Description: This is a test implementation of the keyboard processor API to
enable testing API clients against a basic keyboard and give
them something to link against and load.
TODO: Add a mecahnism to trigger output of PERSIST_OPT &
TODO: Add a mechanism to trigger output of PERSIST_OPT &
RESET_OPT actions items, options support and context matching.
Create Date: 17 Oct 2018
Authors: Tim Eves (TSE)
@ -74,7 +74,7 @@ namespace km {
{
mock_processor::mock_processor(core::path const & path)
: abstract_processor(
keyboard_attributes(path.stem(), u"3.145", path.parent(), {
keyboard_attributes(path.stem(), u"3.145", {
option{KM_CORE_OPT_KEYBOARD, u"__test_point", u"not tiggered"},
})),
_options({
@ -228,5 +228,23 @@ namespace km {
km_core_status mock_processor::validate() const { return KM_CORE_STATUS_OK; }
km_core_status null_processor::validate() const { return KM_CORE_STATUS_INVALID_ARGUMENT; }
} // namespace core
/**
* Returns true if the data starts with 'MOCK'.
*
* @param data the keyboard blob
* @return true if the processor can handle the keyboard, otherwise false.
*/
bool mock_processor::is_handled(const std::vector<uint8_t>& data) {
if (data.empty()) {
return false;
}
if (data.size() < 4) { // a MOCK file is at least 4 bytes (MOCK)
return false;
}
return ((char*)data.data())[0] == 'M' && ((char*)data.data())[1] == 'O' && ((char*)data.data())[2] == 'C' && ((char*)data.data())[3] == 'K';
}
} // namespace core
} // namespace km

View file

@ -69,13 +69,21 @@ namespace core
supports_normalization() const override {
return true;
}
/**
* Returns true if the data starts with 'MOCK'
*
* @param buf the keyboard blob
* @return true if the processor can handle the keyboard, otherwise false.
*/
static bool is_handled(const std::vector<uint8_t>& data);
};
class null_processor : public mock_processor {
public:
null_processor(): mock_processor(path())
{
_attributes = keyboard_attributes(u"null", u"0.0", path(), {});
_attributes = keyboard_attributes(u"null", u"0.0", {});
}
km_core_status validate() const override;

View file

@ -17,7 +17,6 @@
#include "core_icu.h"
#include <fstream>
#include <iostream>
#include <string>
#include <vector>

44
core/src/wasm.cpp Normal file
View file

@ -0,0 +1,44 @@
#ifdef __EMSCRIPTEN__
#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h>
#include <emscripten/bind.h>
#else
#define EMSCRIPTEN_KEEPALIVE
#endif
#ifdef __cplusplus
#define EXTERN extern "C" EMSCRIPTEN_KEEPALIVE
#else
#define EXTERN EMSCRIPTEN_KEEPALIVE
#endif
#include <keyman_core.h>
constexpr km_core_attr const engine_attrs = {
256,
KM_CORE_LIB_CURRENT,
KM_CORE_LIB_AGE,
KM_CORE_LIB_REVISION,
KM_CORE_TECH_KMX,
"SIL International"
};
EMSCRIPTEN_KEEPALIVE km_core_attr const & tmp_wasm_attributes() {
return engine_attrs;
}
EMSCRIPTEN_BINDINGS(core_interface) {
emscripten::value_object<km_core_attr>("km_core_attr")
.field("max_context", &km_core_attr::max_context)
.field("current", &km_core_attr::current)
.field("revision", &km_core_attr::revision)
.field("age", &km_core_attr::age)
.field("technology", &km_core_attr::technology)
//.field("vendor", &km_core_attr::vendor, emscripten::allow_raw_pointers())
;
emscripten::function("tmp_wasm_attributes", &tmp_wasm_attributes);
}
#endif

View file

@ -2,3 +2,4 @@
/*.zip
/*.tgz
/packagecache
/googletest*/

View file

@ -0,0 +1,16 @@
[wrap-file]
directory = googletest-1.14.0
source_url = https://github.com/google/googletest/archive/refs/tags/v1.14.0.tar.gz
source_filename = gtest-1.14.0.tar.gz
source_hash = 8ad598c73ad796e0d8280b082cebd82a630d73e73cd3c70057938a6501bba5d7
patch_filename = gtest_1.14.0-2_patch.zip
patch_url = https://github.com/mesonbuild/wrapdb/releases/download/gtest_1.14.0-2/gtest_1.14.0-2_patch.zip
patch_hash = 4ec7f767364386a99f7b2d61678287a73ad6ba0f9998be43b51794c464a63732
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/gtest_1.14.0-2/gtest-1.14.0.tar.gz
wrapdb_version = 1.14.0-2
[provide]
gtest = gtest_dep
gtest_main = gtest_main_dep
gmock = gmock_dep
gmock_main = gmock_main_dep

View file

@ -10,10 +10,7 @@
cmpfiles = ['-c', 'import sys; a = open(sys.argv[1], \'r\').read(); b = open(sys.argv[2], \'r\').read(); exit(not (a==b))']
stnds = join_paths(meson.current_source_dir(), 'standards')
libsrc = include_directories(
'../src',
'../../common/include'
)
libsrc = include_directories('../src')
# kmx_test_source is required for linux builds, so always enable it even when we
# disable all other tests

View file

@ -11,7 +11,7 @@ else
endif
e = executable('jsontest', 'jsontest.cpp',
include_directories: [libsrc],
include_directories: [inc, libsrc],
link_args: links + tests_flags,
objects: lib.extract_objects('jsonpp.cpp'))
test('jsontest', e, args: 'jsontest.json')

View file

@ -0,0 +1,128 @@
// Copyright (c) 2024 SIL International
// This software is licensed under the MIT license (http://opensource.org/licenses/MIT)
#include <fstream>
#include <gtest/gtest.h>
#include <keyman/keyman_core_api.h>
#include "emscripten_filesystem.h"
#include "load_kmx_file.hpp"
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;
void TearDown() override {
if (this->keyboard) {
km_core_keyboard_dispose(this->keyboard);
this->keyboard = nullptr;
}
}
};
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");
std::vector<uint8_t> data = km::tests::load_kmx_file(kmxfile.native());
ASSERT_GT(data.size(), (size_t)0);
// Execute
auto status = km_core_keyboard_load_from_blob(kmxfile.stem().c_str(), data.data(), data.size(), &this->keyboard);
// Verify
EXPECT_EQ(status, KM_CORE_STATUS_OK);
EXPECT_TRUE(this->keyboard != nullptr);
}
TEST_F(KmCoreKeyboardApiTests, LoadFromBlobMock) {
// Setup
km::core::path kmxfile = "mock_keyboard.mock";
std::string blob_string = "MOCK";
std::vector<uint8_t> data = std::vector<uint8_t>(blob_string.begin(), blob_string.end());
ASSERT_GT(data.size(), (size_t)0);
// Execute
auto status = km_core_keyboard_load_from_blob(kmxfile.stem().c_str(), data.data(), data.size(), &this->keyboard);
// Verify
EXPECT_EQ(status, KM_CORE_STATUS_OK);
EXPECT_TRUE(this->keyboard != nullptr);
}
TEST_F(KmCoreKeyboardApiTests, LoadFromBlobNull) {
// Setup
km::core::path kmxfile = "";
std::unique_ptr<uint8_t> data(new uint8_t[0]);
// Execute
auto status = km_core_keyboard_load_from_blob(kmxfile.stem().c_str(), data.get(), 0, &this->keyboard);
// Verify
EXPECT_EQ(status, KM_CORE_STATUS_INVALID_ARGUMENT);
EXPECT_TRUE(this->keyboard == nullptr);
}
TEST_F(KmCoreKeyboardApiTests, LoadFromBlobInvalidKeyboard) {
// Setup
km::core::path kmxfile = "invalid_keyboard.kmx";
std::string blob_string = "KXTS";
std::vector<uint8_t> data = std::vector<uint8_t>(blob_string.begin(), blob_string.end());
for (auto i = data.size(); i < 64; i++) {
data.push_back(0);
}
ASSERT_GT(data.size(), (size_t)0);
// Execute
auto status = km_core_keyboard_load_from_blob(kmxfile.stem().c_str(), data.data(), data.size(), &this->keyboard);
// Verify
EXPECT_EQ(status, KM_CORE_STATUS_INVALID_KEYBOARD);
EXPECT_TRUE(this->keyboard == nullptr);
}
// provide our own `main` so that we can get the path of the exe so that
// we have a well-defined location to find our test keyboards
int main(int argc, char **argv) {
#ifdef __EMSCRIPTEN__
test_dir = get_wasm_file_path(km::core::path(argv[0]).parent());
#else
test_dir = km::core::path(argv[0]).parent();
#endif
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View file

@ -14,6 +14,7 @@
#include <test_assert.h>
#include "../emscripten_filesystem.h"
#include "../load_kmx_file.hpp"
const km_core_action_item alert_action_item();
const km_core_action_item bksp_action_item(uint8_t type, uintptr_t value);
@ -57,7 +58,9 @@ void setup(const char *keyboard, const km_core_cu* 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));
auto blob = km::tests::load_kmx_file(path.native().c_str());
try_status(km_core_keyboard_load_from_blob(path.stem().c_str(), blob.data(), blob.size(), &test_kb));
try_status(km_core_state_create(test_kb, test_env_opts, &test_state));
try_status(context_items_from_utf16(context, &citems));
try_status(km_core_context_set(km_core_state_context(test_state), citems));

View file

@ -22,6 +22,7 @@
#include <test_assert.h>
#include "../emscripten_filesystem.h"
#include "../load_kmx_file.hpp"
using namespace km::core::kmx;
@ -54,8 +55,8 @@ void setup(const char *keyboard) {
teardown();
km::core::path path = km::core::path::join(arg_path, keyboard);
try_status(km_core_keyboard_load(path.native().c_str(), &test_kb));
auto blob = km::tests::load_kmx_file(path.native().c_str());
try_status(km_core_keyboard_load_from_blob(path.stem().c_str(), blob.data(), blob.size(), &test_kb));
try_status(km_core_state_create(test_kb, test_env_opts, &test_state));
try_status(context_items_from_utf16(u"Hello 😁", &citems));

View file

@ -8,8 +8,7 @@
#include "keyman_core.h"
#include "path.hpp"
//#include "keyboard.hpp"
#include "mock/mock_processor.hpp"
namespace
{
@ -26,14 +25,11 @@ int main(int, char *[])
km_core_keyboard_key * kb_key_list = nullptr;
km_core_keyboard_imx * kb_imx_list = nullptr;
try_status(km_core_keyboard_load(test_kb_path.c_str(), &test_kb));
test_kb = (km_core_keyboard *)new km::core::mock_processor(test_kb_path);
try_status(km_core_keyboard_get_attrs(test_kb, &kb_attrs));
try_status(km_core_keyboard_get_key_list(test_kb,&kb_key_list));
try_status(km_core_keyboard_get_imx_list(test_kb,&kb_imx_list));
if (kb_attrs->folder_path != test_kb_path.parent())
return __LINE__;
km_core_keyboard_dispose(test_kb);
km_core_keyboard_key_list_dispose(kb_key_list);
km_core_keyboard_imx_list_dispose(kb_imx_list);

View file

@ -14,6 +14,7 @@
#include "path.hpp"
#include "state.hpp"
#include "action_items.hpp"
#include "mock/mock_processor.hpp"
#include <test_assert.h>
@ -52,7 +53,6 @@ constexpr char const *doc1_expected = u8"\
\"$schema\" : \"keyman/core/docs/introspection.schema\",\n\
\"keyboard\" : {\n\
\"id\" : \"dummy\",\n\
\"folder\" : \"\",\n\
\"version\" : \"3.145\",\n\
\"rules\" : []\n\
},\n\
@ -78,7 +78,6 @@ constexpr char const *doc2_expected = u8"\
\"$schema\" : \"keyman/core/docs/introspection.schema\",\n\
\"keyboard\" : {\n\
\"id\" : \"dummy\",\n\
\"folder\" : \"\",\n\
\"version\" : \"3.145\",\n\
\"rules\" : []\n\
},\n\
@ -112,7 +111,7 @@ int main(int argc, char * argv[])
km_core_keyboard * test_kb = nullptr;
km_core_state * test_state = nullptr,
* test_clone = nullptr;
try_status(km_core_keyboard_load(km::core::path("dummy.mock").c_str(), &test_kb));
test_kb = (km_core_keyboard *)new km::core::mock_processor(km::core::path("dummy.mock"));
// Simple sanity tests.
try_status(km_core_state_create(test_kb, test_env_opts, &test_state));

View file

@ -5,8 +5,10 @@
#include "context.hpp"
#include "path.hpp"
#include "mock/mock_processor.hpp"
#include "../emscripten_filesystem.h"
#include "../load_kmx_file.hpp"
#include <test_assert.h>
//-------------------------------------------------------------------------------------
@ -41,7 +43,14 @@ setup(const char *keyboard, const km_core_cu *context, bool setup_app_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));
auto blob = km::tests::load_kmx_file(path.native().c_str());
const auto mock_extension = ".mock";
if (strlen(keyboard) > strlen(mock_extension) && strcmp(keyboard + strlen(keyboard) - strlen(mock_extension), mock_extension) == 0) {
km::core::abstract_processor* kp = new km::core::mock_processor(keyboard);
test_kb = static_cast<km_core_keyboard*>(kp);
} else {
try_status(km_core_keyboard_load_from_blob(path.stem().c_str(), blob.data(), blob.size(), &test_kb));
}
try_status(km_core_state_create(test_kb, test_env_opts, &test_state));
try_status(context_items_from_utf16(context, &citems));
try_status(km_core_context_set(km_core_state_context(test_state), citems));

View file

@ -15,6 +15,7 @@
#include <test_assert.h>
#include "../emscripten_filesystem.h"
#include "../load_kmx_file.hpp"
km_core_option_item test_env_opts[] =
{
@ -48,7 +49,8 @@ void setup(const km_core_cu *app_context, const km_core_cu *cached_context, int
teardown();
km::core::path path = km::core::path::join(arg_path, "..", "ldml", "keyboards", "k_001_tiny.kmx");
try_status(km_core_keyboard_load(path.native().c_str(), &test_kb));
auto blob = km::tests::load_kmx_file(path.native().c_str());
try_status(km_core_keyboard_load_from_blob(path.stem().c_str(), blob.data(), blob.size(), &test_kb));
try_status(km_core_state_create(test_kb, test_env_opts, &test_state));
try_status(set_context_from_string(km_core_state_context(test_state), cached_context));

View file

@ -15,6 +15,7 @@
#include <test_assert.h>
#include "../emscripten_filesystem.h"
#include "../load_kmx_file.hpp"
void compare_context(km_core_context *app_context, const km_core_cu* expected_final_app_context);
@ -46,7 +47,8 @@ void setup(const km_core_cu *app_context, const km_core_cu *cached_context_strin
teardown();
km::core::path path = km::core::path::join(arg_path, "..", "ldml", "keyboards", "k_001_tiny.kmx");
try_status(km_core_keyboard_load(path.native().c_str(), &test_kb));
auto blob = km::tests::load_kmx_file(path.native().c_str());
try_status(km_core_keyboard_load_from_blob(path.stem().c_str(), blob.data(), blob.size(), &test_kb));
try_status(km_core_state_create(test_kb, test_env_opts, &test_state));
if(cached_context_string) {

View file

@ -29,6 +29,7 @@
#include <test_assert.h>
#include <test_color.h>
#include "../emscripten_filesystem.h"
#include "../load_kmx_file.hpp"
#include "kmx_test_source.hpp"
@ -191,7 +192,8 @@ run_test(const km::core::path &source, const km::core::path &compiled) {
km_core_keyboard * test_kb = nullptr;
km_core_state * test_state = nullptr;
try_status(km_core_keyboard_load(compiled.c_str(), &test_kb));
auto blob = km::tests::load_kmx_file(compiled.native().c_str());
try_status(km_core_keyboard_load_from_blob(compiled.stem().c_str(), blob.data(), blob.size(), &test_kb));
// Setup state, environment
try_status(km_core_state_create(test_kb, test_env_opts, &test_state));

View file

@ -14,6 +14,7 @@
#include <test_assert.h>
#include <test_color.h>
#include "../emscripten_filesystem.h"
#include "../load_kmx_file.hpp"
#include <map>
#include <iostream>
@ -43,7 +44,8 @@ void test_external_event(const km::core::path &source_file){
km::core::path full_path = source_file;
try_status(km_core_keyboard_load(full_path.native().c_str(), &test_kb));
auto blob = km::tests::load_kmx_file(full_path.native().c_str());
try_status(km_core_keyboard_load_from_blob(full_path.stem().c_str(), blob.data(), blob.size(), &test_kb));
// Setup state, environment
try_status(km_core_state_create(test_kb, test_env_opts, &test_state));

View file

@ -14,6 +14,7 @@
#include <test_assert.h>
#include <test_color.h>
#include "../emscripten_filesystem.h"
#include "../load_kmx_file.hpp"
#include "utfcodec.hpp"
#include <map>
@ -164,7 +165,8 @@ void test_imx_list(const km::core::path &source_file){
km::core::path full_path = source_file;
try_status(km_core_keyboard_load(full_path.native().c_str(), &test_kb));
auto blob = km::tests::load_kmx_file(full_path.native().c_str());
try_status(km_core_keyboard_load_from_blob(full_path.stem().c_str(), blob.data(), blob.size(), &test_kb));
// Setup state, environment
try_status(km_core_state_create(test_kb, test_env_opts, &test_state));
@ -211,7 +213,8 @@ void test_queue_actions (const km::core::path &source_keyboard) {
km::core::path full_path = source_keyboard;
try_status(km_core_keyboard_load(full_path.native().c_str(), &test_kb));
auto blob = km::tests::load_kmx_file(full_path.native().c_str());
try_status(km_core_keyboard_load_from_blob(full_path.stem().c_str(), blob.data(), blob.size(), &test_kb));
// Setup state, environment
try_status(km_core_state_create(test_kb, test_env_opts, &test_state));

View file

@ -15,6 +15,7 @@
#include <test_assert.h>
#include <test_color.h>
#include "../emscripten_filesystem.h"
#include "../load_kmx_file.hpp"
using namespace km::core::kmx;
@ -56,7 +57,8 @@ void test_key_list(const km::core::path &source_file){
km::core::path full_path = source_file;
try_status(km_core_keyboard_load(full_path.native().c_str(), &test_kb));
auto blob = km::tests::load_kmx_file(full_path.native().c_str());
try_status(km_core_keyboard_load_from_blob(full_path.stem().c_str(), blob.data(), blob.size(), &test_kb));
// Setup state, environment
try_status(km_core_state_create(test_kb, test_env_opts, &test_state));

View file

@ -14,6 +14,7 @@
#include <assert.h>
#include "keyman_core.h"
#include "../load_kmx_file.hpp"
int main(int argc, const char *argv[]) {
@ -22,7 +23,8 @@ int main(int argc, const char *argv[]) {
km_core_status status;
km_core_path_name nowhere = {0}; // this is a narrow or wide char string
status = km_core_keyboard_load(nowhere, &test_kb);
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;
assert(status == KM_CORE_STATUS_INVALID_ARGUMENT);

View file

@ -35,6 +35,7 @@
#include "ldml/ldml_markers.hpp"
#include "processor.hpp"
#include "../load_kmx_file.hpp"
namespace {
@ -273,7 +274,8 @@ run_test(const km::core::path &source, const km::core::path &compiled, km::tests
km_core_state * test_state = nullptr;
const km_core_status expect_load_status = test_source.get_expected_load_status();
assert_equal(km_core_keyboard_load(compiled.c_str(), &test_kb), expect_load_status);
auto blob = km::tests::load_kmx_file(compiled.native().c_str());
assert_equal(km_core_keyboard_load_from_blob(compiled.stem().c_str(), blob.data(), blob.size(), &test_kb), expect_load_status);
if (expect_load_status != KM_CORE_STATUS_OK) {
std::cout << "Keyboard was expected to be invalid, so exiting " << std::endl;

View file

@ -40,6 +40,7 @@
#include "unicode/uniset.h"
#include "unicode/usetiter.h"
#include "../load_kmx_file.hpp"
#include <test_color.h>
#define assert_or_return(expr) if(!(expr)) { \
@ -56,8 +57,6 @@ namespace km {
namespace tests {
/** string munging */
static void append_to_str(std::u16string &str, const char *buf) {
const PKMX_WCHAR p = km::core::kmx::strtowstr((char *)buf); /** cast away const, unused*/
@ -757,7 +756,8 @@ int LdmlJsonTestSourceFactory::load(const km::core::path &compiled, const km::co
}
// check and load the KMX (yes, once again)
if(!km::core::ldml_processor::is_kmxplus_file(compiled, rawdata)) {
rawdata = km::tests::load_kmx_file(compiled);
if (!km::core::ldml_processor::is_handled(rawdata)) {
std::cerr << "Reading KMX for test purposes failed: " << compiled << std::endl;
return __LINE__;
}

View file

@ -83,7 +83,10 @@ ldml = executable('ldml',
)
core_ldml_min = executable('core_ldml_min',
['core_ldml_min.cpp'],
[
'core_ldml_min.cpp',
meson.current_source_dir() / '../load_kmx_file.cpp',
],
cpp_args: defns + warns,
include_directories: [inc, libsrc],
link_args: links,

View file

@ -13,6 +13,7 @@
#include <test_assert.h>
#include "../emscripten_filesystem.h"
#include "../load_kmx_file.hpp"
//-------------------------------------------------------------------------------------
// Context normalization tests
@ -42,7 +43,8 @@ void setup(const char *keyboard) {
teardown();
km::core::path path = km::core::path::join(arg_path, "keyboards", keyboard);
try_status(km_core_keyboard_load(path.native().c_str(), &test_kb));
auto blob = km::tests::load_kmx_file(path.native().c_str());
try_status(km_core_keyboard_load_from_blob(path.stem().c_str(), blob.data(), blob.size(), &test_kb));
try_status(km_core_state_create(test_kb, test_env_opts, &test_state));
}

View file

@ -0,0 +1,34 @@
#include <fstream>
#include <iostream>
#include <list>
#include <string>
#include "kmx_file.h"
#include "path.hpp"
#include "utfcodec.hpp"
namespace km::tests {
std::vector<uint8_t> load_kmx_file(km::core::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.fail()) {
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;
}
}

View file

@ -0,0 +1,14 @@
#ifndef __LOAD_KMX_FILE_HPP__
#define __LOAD_KMX_FILE_HPP__
#include "path.hpp"
namespace km {
namespace tests {
std::vector<uint8_t> load_kmx_file(km::core::path const& kb_path);
}
}
#endif // __LOAD_KMX_FILE_HPP__

View file

@ -1,13 +1,41 @@
node = find_program('node', required: true)
common_test_files = [
gtest = subproject('gtest')
gtest_dep = gtest.get_variable('gtest_dep')
gmock_dep = gtest.get_variable('gmock_dep')
test_util_files = [
meson.current_source_dir() / 'emscripten_filesystem.cpp',
meson.current_source_dir() / 'load_kmx_file.cpp',
]
common_test_files = [
test_util_files,
meson.global_source_root() / '../common/include/test_color.cpp'
]
hextobin_root = meson.global_source_root() / '../common/tools/hextobin/build/hextobin.js'
hextobin_cmd = [node, hextobin_root]
if cpp_compiler.get_id() == 'emscripten'
extra_link_args = [ '-lnodefs.js' ]
else
extra_link_args = []
endif
kmcorekeyboardapitests = executable('km_core_keyboard_api.tests',
[
'km_core_keyboard_api.tests.cpp',
test_util_files,
],
include_directories: [inc, libsrc],
link_args: [ links, extra_link_args ],
dependencies: [icu_uc, icu_i18n, gtest_dep, gmock_dep],
objects: lib.extract_all_objects(recursive: false),
)
test('km-core-keyboard-api-tests', kmcorekeyboardapitests)
subdir('json')
subdir('utftest')
subdir('kmnkbd')

View file

@ -6,5 +6,5 @@
e = executable('utftest', 'utftest.cpp',
objects: lib.extract_objects('../../common/cpp/utfcodec.cpp'),
include_directories: [libsrc])
include_directories: [inc, libsrc])
test('utftest', e)

View file

@ -61,7 +61,7 @@ https://help.keyman.com/developer/engine/android/latest-version/
| KEYMAN_MIN_TARGET_VERSION_WINDOWS | 10 |
| KEYMAN_MIN_VERSION_ANDROID_SDK | 21 |
| KEYMAN_MIN_VERSION_CPP | 17 |
| KEYMAN_MIN_VERSION_EMSCRIPTEN | 3.1.58 |
| KEYMAN_MIN_VERSION_EMSCRIPTEN | 3.1.64 |
| KEYMAN_MIN_VERSION_MESON | 1.0.0 |
| KEYMAN_MIN_VERSION_NODE_MAJOR | 20 |
| KEYMAN_MIN_VERSION_NPM | 10.5.1 |

View file

@ -19,6 +19,7 @@ libkeymancore.so.2 libkeymancore2 #MINVER#
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.123
km_core_options_list_size@Base 17.0.195
km_core_process_event@Base 17.0.195
km_core_process_queued_actions@Base 17.0.195

2
package-lock.json generated
View file

@ -8761,6 +8761,7 @@
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz",
"integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@ -15647,6 +15648,7 @@
"@sentry/cli": "^2.31.0",
"@zip.js/zip.js": "^2.7.32",
"c8": "^7.12.0",
"express": "^4.19.2",
"jsdom": "^23.0.1",
"mocha": "^10.0.0",
"tsx": "^4.19.0"

View file

@ -27,7 +27,7 @@ KEYMAN_MIN_TARGET_VERSION_WEB_SAFARI=13.0 # iOS 13.0, macOS 10.13.6+
# Dependency versions
KEYMAN_MIN_VERSION_NODE_MAJOR=20 # node version source of truth is /package.json:/engines/node; use KEYMAN_USE_NVM to automatically update
KEYMAN_MIN_VERSION_NPM=10.5.1 # 10.5.0 has bug, discussed in #10350
KEYMAN_MIN_VERSION_EMSCRIPTEN=3.1.58 # Use KEYMAN_USE_EMSDK to automatically update to this version
KEYMAN_MIN_VERSION_EMSCRIPTEN=3.1.64 # Use KEYMAN_USE_EMSDK to automatically update to this version
KEYMAN_MIN_VERSION_VISUAL_STUDIO=2019
KEYMAN_MIN_VERSION_MESON=1.0.0

View file

@ -29,8 +29,9 @@ src/test/auto A Node-driven test suite for automated testing of Key
## Usage
Open **index.html** or **samples/index.html** in your browser. Be sure to
compile Keyman Engine for Web before viewing the pages.
Start the test server by running `./build.sh start`, then open
your browser to http://localhost:3000. Be sure to compile Keyman Engine
for Web before viewing the pages.
Refer to the samples for usage details.

View file

@ -20,12 +20,14 @@ builder_describe "Builds engine modules for Keyman Engine for Web (KMW)." \
"clean" \
"configure" \
"build" \
"start Starts the test server" \
"test" \
"coverage Create an HTML page with code coverage" \
":app/browser The form of Keyman Engine for Web for use on websites" \
":app/webview A puppetable version of KMW designed for use in a host app's WebView" \
":app/ui Builds KMW's desktop form-factor keyboard-selection UI modules" \
":engine/attachment Subset used for detecting valid page contexts for use in text editing " \
":engine/core-processor Keyman Core WASM integration" \
":engine/dom-utils A common subset of function used for DOM calculations, layout, etc" \
":engine/events Specialized classes utilized to support KMW API events" \
":engine/element-wrappers Subset used to integrate with website elements" \
@ -60,6 +62,7 @@ builder_describe_outputs \
build:app/webview "/web/build/app/webview/${config}/keymanweb-webview.js" \
build:app/ui "/web/build/app/ui/${config}/kmwuitoggle.js" \
build:engine/attachment "/web/build/engine/attachment/lib/index.mjs" \
build:engine/core-processor "/web/build/engine/core-processor/lib/index.mjs" \
build:engine/dom-utils "/web/build/engine/dom-utils/obj/index.js" \
build:engine/events "/web/build/engine/events/lib/index.mjs" \
build:engine/element-wrappers "/web/build/engine/element-wrappers/lib/index.mjs" \
@ -166,6 +169,8 @@ builder_run_child_actions build:engine/attachment
# Uses engine/interfaces (due to resource-path config interface)
builder_run_child_actions build:engine/keyboard-storage
builder_run_child_actions build:engine/core-processor
# Uses engine/interfaces, engine/keyboard-storage, & engine/osk
builder_run_child_actions build:engine/main
@ -201,3 +206,6 @@ builder_run_action test:help do_test_help
# Create coverage report
builder_run_action coverage:_all coverage_action
# Start the test server
builder_run_action start node src/tools/testing/test-server/index.cjs

View file

@ -12,10 +12,10 @@
"types": "./build/engine/attachment/obj/index.d.ts",
"import": "./build/engine/attachment/obj/index.js"
},
"./engine/interfaces": {
"es6-bundling": "./src/engine/interfaces/src/index.ts",
"types": "./build/engine/interfaces/obj/index.d.ts",
"import": "./build/engine/interfaces/obj/index.js"
"./engine/core-processor": {
"es6-bundling": "./src/engine/core-processor/src/index.ts",
"types": "./build/engine/core-processor/obj/index.d.ts",
"import": "./build/engine/core-processor/obj/index.js"
},
"./engine/dom-utils": {
"es6-bundling": "./src/engine/dom-utils/src/index.ts",
@ -32,6 +32,11 @@
"types": "./build/engine/events/obj/index.d.ts",
"import": "./build/engine/events/obj/index.js"
},
"./engine/interfaces": {
"es6-bundling": "./src/engine/interfaces/src/index.ts",
"types": "./build/engine/interfaces/obj/index.d.ts",
"import": "./build/engine/interfaces/obj/index.js"
},
"./engine/js-processor": {
"es6-bundling": "./src/engine/js-processor/src/index.ts",
"types": "./build/engine/js-processor/obj/index.d.ts",
@ -81,6 +86,10 @@
"./engine/osk/internals": {
"types": "./build/engine/osk/obj/test-index.d.ts",
"import": "./build/engine/osk/obj/test-index.js"
},
"./tools/testing/test-utils": {
"types": "./build/tools/testing/test-utils/obj/index.d.ts",
"import": "./build/tools/testing/test-utils/obj/index.js"
}
},
"imports": {
@ -108,6 +117,7 @@
"@sentry/cli": "^2.31.0",
"@zip.js/zip.js": "^2.7.32",
"c8": "^7.12.0",
"express": "^4.19.2",
"jsdom": "^23.0.1",
"mocha": "^10.0.0",
"tsx": "^4.19.0"

View file

@ -72,6 +72,10 @@ compile_and_copy() {
mkdir -p "$KEYMAN_ROOT/web/build/app/resources/osk"
cp -R "$KEYMAN_ROOT/web/src/resources/osk/." "$KEYMAN_ROOT/web/build/app/resources/osk/"
# Copy Keyman Core build artifacts for local reference
cp "${KEYMAN_ROOT}/web/build/engine/core-processor/obj/import/core/"km-core.{js,wasm} "${KEYMAN_ROOT}/web/build/app/browser/debug/"
cp "${KEYMAN_ROOT}/web/build/engine/core-processor/obj/import/core/"km-core.{js,wasm} "${KEYMAN_ROOT}/web/build/app/browser/release/"
# Update the build/publish copy of our build artifacts
prepare

View file

@ -60,8 +60,16 @@ compile_and_copy() {
mkdir -p "$KEYMAN_ROOT/web/build/app/resources/osk"
cp -R "$KEYMAN_ROOT/web/src/resources/osk/." "$KEYMAN_ROOT/web/build/app/resources/osk/"
# Copy Keyman Core build artifacts for local reference
cp "${KEYMAN_ROOT}/web/build/engine/core-processor/obj/import/core/"km-core.{js,wasm} "${KEYMAN_ROOT}/web/build/app/webview/debug/"
cp "${KEYMAN_ROOT}/web/build/engine/core-processor/obj/import/core/"km-core.{js,wasm} "${KEYMAN_ROOT}/web/build/app/webview/release/"
# Clean the sourcemaps of .. and . components
for script in "$KEYMAN_ROOT/web/build/$SUBPROJECT_NAME/debug/"*.js; do
if [[ "${script}" == *"/km-core.js" ]]; then
continue
fi
sourcemap="$script.map"
node "$KEYMAN_ROOT/web/build/tools/building/sourcemap-root/index.js" \
"$script" "$sourcemap" --clean --inline
@ -70,6 +78,9 @@ compile_and_copy() {
# Do NOT inline sourcemaps for release builds - we don't want them to affect
# load time.
for script in "$KEYMAN_ROOT/web/build/$SUBPROJECT_NAME/release/"*.js; do
if [[ "${script}" == *"/km-core.js" ]]; then
continue
fi
sourcemap="$script.map"
node "$KEYMAN_ROOT/web/build/tools/building/sourcemap-root/index.js" \
"$script" "$sourcemap" --clean

View file

@ -0,0 +1 @@
src/import/

View file

@ -0,0 +1,64 @@
#!/usr/bin/env bash
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../../../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
SUBPROJECT_NAME=engine/core-processor
. "${KEYMAN_ROOT}/web/common.inc.sh"
. "${KEYMAN_ROOT}/resources/shellHelperFunctions.sh"
# ################################ Main script ################################
builder_describe "Keyman Core WASM integration" \
"@/core:wasm" \
"@/web/src/engine/common/web-utils" \
"clean" \
"configure" \
"build" \
"test" \
"--ci+ Set to utilize CI-based test configurations & reporting."
builder_describe_outputs \
configure "/web/src/engine/core-processor/src/import/core/km-core-interface.d.ts" \
build "/web/build/${SUBPROJECT_NAME}/lib/index.mjs"
builder_parse "$@"
#### Build action definitions ####
do_clean() {
rm -rf "${KEYMAN_ROOT}/web/build/${SUBPROJECT_NAME}"
rm -rf "src/import/"
}
do_configure() {
verify_npm_setup
mkdir -p "src/import/core/"
# we don't need this file for release builds, but it's nice to have
# for reference and auto-completion
cp "${KEYMAN_ROOT}/core/build/wasm/${BUILDER_CONFIGURATION}/src/km-core-interface.d.ts" "src/import/core/"
}
copy_deps() {
mkdir -p "${KEYMAN_ROOT}/web/build/${SUBPROJECT_NAME}/obj/import/core/"
cp "${KEYMAN_ROOT}/core/build/wasm/${BUILDER_CONFIGURATION}/src/"km-core-interface.d.ts "${KEYMAN_ROOT}/web/build/${SUBPROJECT_NAME}/obj/import/core/"
cp "${KEYMAN_ROOT}/core/build/wasm/${BUILDER_CONFIGURATION}/src/"km-core{.js,.wasm} "${KEYMAN_ROOT}/web/build/${SUBPROJECT_NAME}/obj/import/core/"
}
do_build () {
copy_deps
compile "${SUBPROJECT_NAME}"
${BUNDLE_CMD} "${KEYMAN_ROOT}/web/build/${SUBPROJECT_NAME}/obj/index.js" \
--out "${KEYMAN_ROOT}/web/build/${SUBPROJECT_NAME}/lib/index.mjs" \
--format esm
}
builder_run_action clean do_clean
builder_run_action configure do_configure
builder_run_action build do_build

View file

@ -0,0 +1,30 @@
type km_core_attr = import('./import/core/km-core-interface.js').km_core_attr;
export class CoreProcessor {
private instance: any;
/**
* Initialize Core Processor
* @param baseurl - The url where km-core.js is located
*/
public async init(baseurl: string): Promise<boolean> {
if (!this.instance) {
try {
const module = await import(baseurl + '/km-core.js');
this.instance = await module.default({
locateFile: function (path: string, scriptDirectory: string) {
return baseurl + '/' + path;
}
});
} catch (e: any) {
return false;
}
}
return !!this.instance;
};
public tmp_wasm_attributes(): km_core_attr {
return this.instance.tmp_wasm_attributes();
}
}

View file

@ -0,0 +1 @@
export * from './core-processor.js';

View file

@ -0,0 +1,13 @@
{
// While the actual references themselves are headless, it compiles against the DOM-reliant OSK module.
"extends": "../../tsconfig.dom.json",
"compilerOptions": {
"baseUrl": "./",
"outDir": "../../../build/engine/core-processor/obj/",
"tsBuildInfoFile": "../../../build/engine/core-processor/obj/tsconfig.tsbuildinfo",
"rootDir": "./src"
},
"include": [ "**/*.ts", "src/import/core/km-core.js" ],
}

View file

@ -106,6 +106,10 @@ export default class PathConfiguration implements OSKResourcePathConfiguration {
return this._root;
}
get basePath(): string {
return this.sourcePath;
}
get resources(): string {
return this._resources;
}

View file

@ -36,7 +36,12 @@ do_build () {
--format esm
}
do_test() {
test-headless "${SUBPROJECT_NAME}" ""
test-headless-typescript "${SUBPROJECT_NAME}"
}
builder_run_action configure verify_npm_setup
builder_run_action clean rm -rf "${KEYMAN_ROOT}/web/build/${SUBPROJECT_NAME}"
builder_run_action build do_build
builder_run_action test test-headless "${SUBPROJECT_NAME}" ""
builder_run_action test do_test

View file

@ -44,7 +44,7 @@ function do_configure() {
BUILD_DIR="${KEYMAN_ROOT}/web/build/${SUBPROJECT_NAME}"
function do_build() {
do_build() {
tsc --build "${THIS_SCRIPT_PATH}/tsconfig.all.json"
# Base product - the main keyboard processor
@ -73,7 +73,12 @@ function do_build() {
tsc --emitDeclarationOnly --outFile "${BUILD_DIR}/lib/node-keyboard-loader.d.ts" -p src/keyboards/loaders/tsconfig.node.json
}
do_test() {
test-headless "${SUBPROJECT_NAME}" ""
test-headless-typescript "${SUBPROJECT_NAME}"
}
builder_run_action configure do_configure
builder_run_action clean rm -rf "${BUILD_DIR}"
builder_run_action build do_build
builder_run_action test test-headless "${SUBPROJECT_NAME}" ""
builder_run_action test do_test

View file

@ -3,12 +3,8 @@ export * from "./keyboards/defaultLayouts.js";
export { default as Keyboard } from "./keyboards/keyboard.js";
export * from "./keyboards/keyboard.js";
export { KeyboardHarness, KeyboardKeymanGlobal, MinimalCodesInterface, MinimalKeymanGlobal } from "./keyboards/keyboardHarness.js";
export {
default as KeyboardLoaderBase,
KeyboardLoadErrorBuilder,
KeyboardMissingError,
KeyboardScriptError
} from "./keyboards/keyboardLoaderBase.js";
export { KeyboardLoaderBase } from "./keyboards/keyboardLoaderBase.js";
export { KeyboardLoadErrorBuilder, KeyboardMissingError, KeyboardScriptError, KeyboardDownloadError, InvalidKeyboardError } from './keyboards/keyboardLoadError.js'
export {
CloudKeyboardFont,
internalizeFont,

View file

@ -0,0 +1,104 @@
import { type KeyboardStub } from './keyboardLoaderBase.js';
export interface KeyboardLoadErrorBuilder {
scriptError(err?: Error): void;
missingError(err: Error): void;
keyboardDownloadError(err: Error): void;
invalidKeyboard(err: Error): void;
}
export class KeyboardScriptError extends Error {
public readonly cause;
constructor(msg: string, cause?: Error) {
super(msg);
this.cause = cause;
}
}
export class KeyboardMissingError extends Error {
public readonly cause;
constructor(msg: string, cause?: Error) {
super(msg);
this.cause = cause;
}
}
export class KeyboardDownloadError extends Error {
public readonly cause;
constructor(message: string, cause?: Error) {
super(message);
this.cause = cause;
}
}
export class InvalidKeyboardError extends Error {
public readonly cause;
constructor(message: string, cause?: Error) {
super(message);
this.cause = cause;
}
}
export class UriBasedErrorBuilder implements KeyboardLoadErrorBuilder {
readonly uri: string;
constructor(uri: string) {
this.uri = uri;
}
missingError(err: Error) {
const msg = `Cannot find the keyboard at ${this.uri}.`;
return new KeyboardMissingError(msg, err);
}
scriptError(err: Error) {
const msg = `Error registering the keyboard script at ${this.uri}; it may contain an error.`;
return new KeyboardScriptError(msg, err);
}
keyboardDownloadError(err: Error) {
const msg = `Unable to download keyboard at ${this.uri}`;
return new KeyboardDownloadError(msg, err);
}
invalidKeyboard(err: Error) {
const msg = `${this.uri} is not a valid keyboard file`;
return new InvalidKeyboardError(msg, err);
}
}
export class StubBasedErrorBuilder implements KeyboardLoadErrorBuilder {
readonly stub: KeyboardStub;
constructor(stub: KeyboardStub) {
this.stub = stub;
}
missingError(err: Error) {
const stub = this.stub;
const msg = `Cannot find the ${stub.name} keyboard for ${stub.langName} at ${stub.filename}.`;
return new KeyboardMissingError(msg, err);
}
scriptError(err: Error) {
const stub = this.stub;
const msg = `Error registering the ${stub.name} keyboard for ${stub.langName}; keyboard script at ${stub.filename} may contain an error.`;
return new KeyboardScriptError(msg, err);
}
keyboardDownloadError(err: Error) {
const msg = `Unable to download ${this.stub.name} keyboard for ${this.stub.langName}`;
return new KeyboardDownloadError(msg, err);
}
invalidKeyboard(err: Error) {
const msg = `${this.stub.name} is not a valid keyboard`;
return new InvalidKeyboardError(msg, err);
}
}

View file

@ -1,71 +1,11 @@
import Keyboard from "./keyboard.js";
import { KeyboardHarness } from "./keyboardHarness.js";
import KeyboardProperties from "./keyboardProperties.js";
import { KeyboardLoadErrorBuilder, StubBasedErrorBuilder, UriBasedErrorBuilder } from './keyboardLoadError.js';
type KeyboardStub = KeyboardProperties & { filename: string };
export type KeyboardStub = KeyboardProperties & { filename: string };
export interface KeyboardLoadErrorBuilder {
scriptError(err?: Error): void;
missingError(err: Error): void;
}
export class KeyboardScriptError extends Error {
public readonly cause;
constructor(msg: string, cause?: Error) {
super(msg);
this.cause = cause;
}
}
export class KeyboardMissingError extends Error {
public readonly cause;
constructor(msg: string, cause?: Error) {
super(msg);
this.cause = cause;
}
}
class UriBasedErrorBuilder implements KeyboardLoadErrorBuilder {
readonly uri: string;
constructor(uri: string) {
this.uri = uri;
}
missingError(err: Error) {
const msg = `Cannot find the keyboard at ${this.uri}.`;
return new KeyboardMissingError(msg, err);
}
scriptError(err: Error) {
const msg = `Error registering the keyboard script at ${this.uri}; it may contain an error.`;
return new KeyboardScriptError(msg, err);
}
}
class StubBasedErrorBuilder implements KeyboardLoadErrorBuilder {
readonly stub: KeyboardStub;
constructor(stub: KeyboardStub) {
this.stub = stub;
}
missingError(err: Error) {
const stub = this.stub;
const msg = `Cannot find the ${stub.name} keyboard for ${stub.langName} at ${stub.filename}.`;
return new KeyboardMissingError(msg, err);
}
scriptError(err: Error) {
const stub = this.stub;
const msg = `Error registering the ${stub.name} keyboard for ${stub.langName}; keyboard script at ${stub.filename} may contain an error.`;
return new KeyboardScriptError(msg, err);
}
}
export default abstract class KeyboardLoaderBase {
export abstract class KeyboardLoaderBase {
private _harness: KeyboardHarness;
public get harness(): KeyboardHarness {
@ -76,23 +16,49 @@ export default abstract class KeyboardLoaderBase {
this._harness = harness;
}
/**
* Load a keyboard from a remote or local URI.
*
* @param uri The URI of the keyboard to load.
* @returns A Promise that resolves to the loaded keyboard.
*/
public loadKeyboardFromPath(uri: string): Promise<Keyboard> {
this.harness.install();
const promise = this.loadKeyboardInternal(uri, new UriBasedErrorBuilder(uri));
return promise;
return this.loadKeyboardInternal(uri, new UriBasedErrorBuilder(uri));
}
public loadKeyboardFromStub(stub: KeyboardStub) {
/**
* Load a keyboard from keyboard stub.
*
* @param stub The stub of the keyboard to load.
* @returns A Promise that resolves to the loaded keyboard.
*/
public async loadKeyboardFromStub(stub: KeyboardStub): Promise<Keyboard> {
this.harness.install();
let promise = this.loadKeyboardInternal(stub.filename, new StubBasedErrorBuilder(stub), stub.id);
return promise;
return this.loadKeyboardInternal(stub.filename, new StubBasedErrorBuilder(stub));
}
protected abstract loadKeyboardInternal(
uri: string,
errorBuilder: KeyboardLoadErrorBuilder,
id?: string
): Promise<Keyboard>;
private async loadKeyboardInternal(uri: string, errorBuilder: KeyboardLoadErrorBuilder): Promise<Keyboard> {
const byteArray = await this.loadKeyboardBlob(uri, errorBuilder);
if (byteArray.slice(0, 4) == Uint8Array.from([0x4b, 0x58, 0x54, 0x53])) { // 'KXTS'
// KMX or LDML (KMX+) keyboard
console.error("KMX keyboard loading is not yet implemented!");
return null;
}
let script: string;
try {
script = new TextDecoder('utf-8', { fatal: true }).decode(byteArray);
} catch (e) {
throw errorBuilder.invalidKeyboard(e);
}
// .js keyboard
return await this.loadKeyboardFromScript(script, errorBuilder);
}
protected abstract loadKeyboardBlob(uri: string, errorBuilder: KeyboardLoadErrorBuilder): Promise<Uint8Array>;
protected abstract loadKeyboardFromScript(scriptSrc: string, errorBuilder: KeyboardLoadErrorBuilder): Promise<Keyboard>;
}

View file

@ -2,9 +2,10 @@
///<reference lib="dom" />
import { Keyboard, KeyboardHarness, KeyboardLoaderBase, KeyboardLoadErrorBuilder, MinimalKeymanGlobal } from 'keyman/engine/keyboard';
import { ManagedPromise } from '@keymanapp/web-utils';
import { default as Keyboard } from '../keyboard.js';
import { KeyboardHarness, MinimalKeymanGlobal } from '../keyboardHarness.js';
import { KeyboardLoaderBase } from '../keyboardLoaderBase.js';
import { KeyboardLoadErrorBuilder } from '../keyboardLoadError.js';
export class DOMKeyboardLoader extends KeyboardLoaderBase {
public readonly element: HTMLIFrameElement;
@ -28,54 +29,44 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase {
this.performCacheBusting = cacheBust || false;
}
protected loadKeyboardInternal(
uri: string,
errorBuilder: KeyboardLoadErrorBuilder,
id?: string
): Promise<Keyboard> {
const promise = new ManagedPromise<Keyboard>();
if(this.performCacheBusting) {
protected async loadKeyboardBlob(uri: string, errorBuilder: KeyboardLoadErrorBuilder): Promise<Uint8Array> {
if (this.performCacheBusting) {
uri = this.cacheBust(uri);
}
let response: Response;
try {
const document = this.harness._jsGlobal.document;
const script = document.createElement('script');
if(id) {
script.id = id;
}
document.head.appendChild(script);
script.onerror = (err: any) => {
promise.reject(errorBuilder.missingError(err));
}
script.onload = () => {
if(this.harness.loadedKeyboard) {
const keyboard = this.harness.loadedKeyboard;
this.harness.loadedKeyboard = null;
promise.resolve(keyboard);
} else {
promise.reject(errorBuilder.scriptError());
}
}
// On the oldest mobile devices we support, Promise.finally may not actually exist.
// Fortunately... it's not that hard of an issue to work around.
// Note: es6-shim doesn't polyfill Promise.finally!
promise.then(() => {
// It is safe to remove the script once it has been run (https://stackoverflow.com/a/37393041)
script.remove();
}).catch(() => {
script.remove();
});
// Now that EVERYTHING ELSE is ready, establish the link to the keyboard's script.
script.src = uri;
} catch (err) {
return Promise.reject(err);
response = await fetch(uri);
} catch (e) {
throw errorBuilder.keyboardDownloadError(e);
}
return promise.corePromise;
if (!response.ok) {
throw errorBuilder.keyboardDownloadError(new Error(`HTTP ${response.status} ${response.statusText}`));
}
let buffer: ArrayBuffer;
try {
buffer = await response.arrayBuffer();
} catch (e) {
throw errorBuilder.invalidKeyboard(e);
}
return new Uint8Array(buffer);
}
protected async loadKeyboardFromScript(script: string, errorBuilder: KeyboardLoadErrorBuilder): Promise<Keyboard> {
try {
this.evalScriptInContext(script, this.harness._jsGlobal);
} catch (e) {
throw errorBuilder.scriptError(e);
}
const keyboard = this.harness.loadedKeyboard;
if (!keyboard) {
throw errorBuilder.scriptError();
}
this.harness.loadedKeyboard = null;
return keyboard;
}
private cacheBust(uri: string) {
@ -84,4 +75,14 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase {
// being ignored.
return uri + "?v=" + (new Date()).getTime(); /*cache buster*/
}
private evalScriptInContext(script: string, context: any) {
const f = function (s: string) {
// use indirect eval (eval?.() notation doesn't work because of esbuild bundling)
const evalFunc = eval;
return evalFunc(s);
}
f.call(context, script);
}
}

View file

@ -1,9 +1,13 @@
import { Keyboard, KeyboardHarness, KeyboardLoaderBase, KeyboardLoadErrorBuilder, MinimalKeymanGlobal } from 'keyman/engine/keyboard';
import vm from 'node:vm';
import { readFile } from 'node:fs/promises';
import vm from 'vm';
import fs from 'fs';
import { globalObject } from '@keymanapp/web-utils';
import { default as Keyboard } from '../keyboard.js';
import { KeyboardHarness, MinimalKeymanGlobal } from '../keyboardHarness.js';
import { KeyboardLoaderBase } from '../keyboardLoaderBase.js';
import { KeyboardLoadErrorBuilder } from '../keyboardLoadError.js';
export class NodeKeyboardLoader extends KeyboardLoaderBase {
constructor()
constructor(harness: KeyboardHarness);
@ -22,22 +26,32 @@ export class NodeKeyboardLoader extends KeyboardLoaderBase {
}
}
protected loadKeyboardInternal(uri: string, errorBuilder: KeyboardLoadErrorBuilder): Promise<Keyboard> {
protected async loadKeyboardBlob(uri: string, errorBuilder: KeyboardLoadErrorBuilder): Promise<Uint8Array> {
// `fs` does not like 'file:///'; it IS "File System" oriented, after all, and wants a path, not a URI.
if(uri.indexOf('file:///') == 0) {
if (uri.indexOf('file:///') == 0) {
uri = uri.substring('file:///'.length);
}
let buffer: Buffer;
try {
buffer = await readFile(uri);
} catch (err) {
throw errorBuilder.keyboardDownloadError(err);
}
return Uint8Array.from(buffer);
}
protected async loadKeyboardFromScript(scriptSrc: string, errorBuilder: KeyboardLoadErrorBuilder): Promise<Keyboard> {
let script;
try {
script = new vm.Script(fs.readFileSync(uri).toString());
script = new vm.Script(scriptSrc);
} catch (err) {
return Promise.reject(errorBuilder.missingError(err));
throw errorBuilder.invalidKeyboard(err);
}
try {
script.runInContext(this.harness._jsGlobal);
} catch (err) {
return Promise.reject(errorBuilder.scriptError(err));
throw errorBuilder.scriptError(err);
}
const keyboard = this.harness.loadedKeyboard;

View file

@ -14,6 +14,7 @@ SUBPROJECT_NAME=engine/main
builder_describe "Builds the Keyman Engine for Web's common top-level base classes." \
"@/common/web/keyman-version" \
"@/web/src/engine/core-processor" \
"@/web/src/engine/keyboard" \
"@/web/src/engine/interfaces build" \
"@/web/src/engine/js-processor build" \

View file

@ -1,3 +1,4 @@
## engine/main
# engine/main
This subproject holds modularized code converted from the old, namespaced version of KMW.
This subproject holds modularized code converted from the old, namespaced version of KMW.
Previously it was called `input-processor`.

View file

@ -2,9 +2,10 @@
import ContextWindow from "./contextWindow.js";
import { LanguageProcessor } from "./languageProcessor.js";
import type { ModelSpec } from "keyman/engine/interfaces";
import type { ModelSpec, PathConfiguration } from "keyman/engine/interfaces";
import { globalObject, DeviceSpec } from "@keymanapp/web-utils";
import { CoreProcessor } from "keyman/engine/core-processor";
import { Codes, type Keyboard, type KeyEvent } from "keyman/engine/keyboard";
import {
type Alternate,
@ -34,6 +35,7 @@ export class InputProcessor {
private contextDevice: DeviceSpec;
private kbdProcessor: KeyboardProcessor;
private lngProcessor: LanguageProcessor;
private coreProcessor: CoreProcessor;
private readonly contextCache = new TranscriptionCache();
@ -49,6 +51,11 @@ export class InputProcessor {
this.contextDevice = device;
this.kbdProcessor = new KeyboardProcessor(device, options);
this.lngProcessor = new LanguageProcessor(predictiveTextWorker, this.contextCache);
this.coreProcessor = new CoreProcessor();
}
public async init(paths: PathConfiguration) {
this.coreProcessor.init(paths.basePath);
}
public get languageProcessor(): LanguageProcessor {

View file

@ -238,6 +238,8 @@ export default class KeymanEngine<
// Initialize supplementary plane string extensions
String.kmwEnableSupplementaryPlane(true);
await this.core.init(config.paths);
// Since we're not sandboxing keyboard loads yet, we just use `window` as the jsGlobal object.
// All components initialized below require a properly-configured `config.paths` or similar.
const keyboardLoader = new KeyboardLoader(this.interface, config.applyCacheBusting);

View file

@ -0,0 +1,21 @@
import { assert } from 'chai';
import { CoreProcessor } from 'keyman/engine/core-processor';
const coreurl = '/web/build/engine/core-processor/obj/import/core';
// Test the CoreProcessor interface.
describe('CoreProcessor', function () {
it('can initialize without errors', async function () {
const kp = new CoreProcessor();
assert.isTrue(await kp.init(coreurl));
});
it('can call temp function', async function () {
const kp = new CoreProcessor();
await kp.init(coreurl);
const a = kp.tmp_wasm_attributes();
assert.isNotNull(a);
assert.isNumber(a.max_context);
console.dir(a);
});
});

View file

@ -1,8 +1,9 @@
import { assert } from 'chai';
import { DOMKeyboardLoader } from 'keyman/engine/keyboard/dom-keyboard-loader';
import { extendString, KeyboardHarness, Keyboard, MinimalKeymanGlobal, DeviceSpec, KeyboardKeymanGlobal } from 'keyman/engine/keyboard';
import { extendString, KeyboardHarness, Keyboard, MinimalKeymanGlobal, DeviceSpec, KeyboardKeymanGlobal, KeyboardDownloadError, KeyboardScriptError } from 'keyman/engine/keyboard';
import { KeyboardInterface, Mock } from 'keyman/engine/js-processor';
import { assertThrowsAsync } from 'keyman/tools/testing/test-utils';
declare let window: typeof globalThis;
// KeymanEngine from the web/ folder... when available.
@ -27,6 +28,24 @@ describe('Keyboard loading in DOM', function() {
}
})
it('throws error when keyboard does not exist', async () => {
const harness = new KeyboardInterface(window, MinimalKeymanGlobal);
const keyboardLoader = new DOMKeyboardLoader(harness);
const nonExisting = '/does/not/exist.js';
await assertThrowsAsync(async () => await keyboardLoader.loadKeyboardFromPath(nonExisting),
KeyboardDownloadError, `Unable to download keyboard at ${nonExisting}`);
});
it('throws error when keyboard is invalid', async () => {
const harness = new KeyboardInterface(window, MinimalKeymanGlobal);
const keyboardLoader = new DOMKeyboardLoader(harness);
const nonKeyboardPath = '/common/test/resources/index.mjs';
await assertThrowsAsync(async () => await keyboardLoader.loadKeyboardFromPath(nonKeyboardPath),
KeyboardScriptError, `Error registering the keyboard script at ${nonKeyboardPath}; it may contain an error.`);
});
it('`window`, disabled rule processing', async () => {
const harness = new KeyboardHarness(window, MinimalKeymanGlobal);
let keyboardLoader = new DOMKeyboardLoader(harness);

View file

@ -33,7 +33,7 @@ export default {
nodeResolve: true,
// Top-level, implicit 'default' group
files: [
'src/test/auto/dom/test_init_check.spec.ts',
'web/src/test/auto/dom/test_init_check.spec.ts',
// '**/*.spec.html'
],
groups: [
@ -41,45 +41,50 @@ export default {
name: 'engine/attachment',
// Relative, from the containing package.json
files: [
'build/test/dom/cases/attachment/**/*.spec.html',
'build/test/dom/cases/attachment/**/*.spec.mjs'
'web/build/test/dom/cases/attachment/**/*.spec.html',
'web/build/test/dom/cases/attachment/**/*.spec.mjs'
]
},
{
name: 'app/browser',
// Relative, from the containing package.json
files: ['build/test/dom/cases/browser/**/*.spec.mjs']
files: ['web/build/test/dom/cases/browser/**/*.spec.mjs']
},
{
name: 'engine/core-processor',
// Relative, from the containing package.json
files: ['web/src/test/auto/dom/cases/core-processor/*.spec.ts']
},
{
name: 'engine/dom-utils',
// Relative, from the containing package.json
files: ['build/test/dom/cases/dom-utils/**/*.spec.mjs']
files: ['web/build/test/dom/cases/dom-utils/**/*.spec.mjs']
},
{
name: 'engine/element-wrappers',
// Relative, from the containing package.json
files: ['build/test/dom/cases/element-wrappers/**/*.spec.mjs']
files: ['web/build/test/dom/cases/element-wrappers/**/*.spec.mjs']
},
{
name: 'engine/gesture-processor',
// Relative, from the containing package.json
// Note: here we use the .spec.html file in the src directory!
files: ['src/test/auto/dom/cases/gesture-processor/**/*.spec.html']
files: ['web/src/test/auto/dom/cases/gesture-processor/**/*.spec.html']
},
{
name: 'engine/keyboard',
// Relative, from the containing package.json
files: ['build/test/dom/cases/keyboard/**/*.spec.mjs']
files: ['web/build/test/dom/cases/keyboard/**/*.tests.mjs']
},
{
name: 'engine/keyboard-storage',
// Relative, from the containing package.json
files: ['build/test/dom/cases/keyboard-storage/**/*.spec.mjs']
files: ['web/build/test/dom/cases/keyboard-storage/**/*.spec.mjs']
},
{
name: 'engine/osk',
// Relative, from the containing package.json
files: ['build/test/dom/cases/osk/**/*.spec.mjs']
files: ['web/build/test/dom/cases/osk/**/*.spec.mjs']
}
],
middleware: [
@ -89,6 +94,12 @@ export default {
context.url = '/web/src/test/auto' + context.url;
}
return next();
},
function rewriteWasmContentType(context, next) {
if (context.url.endsWith('.wasm')) {
context.headers['content-type'] = 'application/wasm';
}
return next();
}
],

View file

@ -3,7 +3,7 @@ import { assert } from 'chai';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
import { MinimalKeymanGlobal } from 'keyman/engine/keyboard';
import { DeviceSpec, MinimalKeymanGlobal } from 'keyman/engine/keyboard';
import { KeyboardInterface, Mock } from 'keyman/engine/js-processor';
import { NodeKeyboardLoader } from 'keyman/engine/keyboard/node-keyboard-loader';
@ -11,21 +11,22 @@ describe('Headless keyboard loading', function () {
const laoPath = require.resolve('@keymanapp/common-test-resources/keyboards/lao_2008_basic.js');
const khmerPath = require.resolve('@keymanapp/common-test-resources/keyboards/khmer_angkor.js');
const nonKeyboardPath = require.resolve('@keymanapp/common-test-resources/index.mjs');
const ipaPath = require.resolve('@keymanapp/common-test-resources/keyboards/sil_ipa.js');
// const ipaPath = require.resolve('@keymanapp/common-test-resources/keyboards/sil_ipa.js');
// Common test suite setup.
let device = {
formFactor: 'desktop',
OS: 'windows',
browser: 'native'
const device = {
formFactor: DeviceSpec.FormFactor.Desktop,
OS: DeviceSpec.OperatingSystem.Windows,
browser: DeviceSpec.Browser.Native,
touchable: false
}
describe('Full harness loading', () => {
it('successfully loads', async function () {
// -- START: Standard Recorder-based unit test loading boilerplate --
let harness = new KeyboardInterface({}, MinimalKeymanGlobal);
let keyboardLoader = new NodeKeyboardLoader(harness);
let keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath);
const harness = new KeyboardInterface({}, MinimalKeymanGlobal);
const keyboardLoader = new NodeKeyboardLoader(harness);
const keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath);
harness.activeKeyboard = keyboard;
// -- END: Standard Recorder-based unit test loading boilerplate --
@ -35,9 +36,9 @@ describe('Headless keyboard loading', function () {
it('can evaluate rules', async function () {
// -- START: Standard Recorder-based unit test loading boilerplate --
let harness = new KeyboardInterface({}, MinimalKeymanGlobal);
let keyboardLoader = new NodeKeyboardLoader(harness);
let keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath);
const harness = new KeyboardInterface({}, MinimalKeymanGlobal);
const keyboardLoader = new NodeKeyboardLoader(harness);
const keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath);
harness.activeKeyboard = keyboard;
// -- END: Standard Recorder-based unit test loading boilerplate --
@ -46,8 +47,8 @@ describe('Headless keyboard loading', function () {
});
it('does not change the active kehboard', async function () {
let harness = new KeyboardInterface({}, MinimalKeymanGlobal);
let keyboardLoader = new NodeKeyboardLoader(harness);
const harness = new KeyboardInterface({}, MinimalKeymanGlobal);
const keyboardLoader = new NodeKeyboardLoader(harness);
const lao_keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath);
assert.isNotOk(harness.activeKeyboard);
assert.isOk(lao_keyboard);
@ -66,8 +67,8 @@ describe('Headless keyboard loading', function () {
it('throws distinct errors', async function () {
const invalidPath = 'totally_invalid_path.js';
let harness = new KeyboardInterface({}, MinimalKeymanGlobal);
let keyboardLoader = new NodeKeyboardLoader(harness);
const harness = new KeyboardInterface({}, MinimalKeymanGlobal);
const keyboardLoader = new NodeKeyboardLoader(harness);
let missingError;
try {
await keyboardLoader.loadKeyboardFromPath(invalidPath);

View file

@ -1,94 +0,0 @@
import { assert } from 'chai';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
import { KeyboardHarness, MinimalKeymanGlobal } from 'keyman/engine/keyboard';
import { KeyboardInterface, Mock } from 'keyman/engine/js-processor';
import { NodeKeyboardLoader } from 'keyman/engine/keyboard/node-keyboard-loader';
describe('Headless keyboard loading', function() {
const laoPath = require.resolve('@keymanapp/common-test-resources/keyboards/lao_2008_basic.js');
const khmerPath = require.resolve('@keymanapp/common-test-resources/keyboards/khmer_angkor.js');
const nonKeyboardPath = require.resolve('@keymanapp/common-test-resources/index.mjs');
const ipaPath = require.resolve('@keymanapp/common-test-resources/keyboards/sil_ipa.js');
// Common test suite setup.
let device = {
formFactor: 'desktop',
OS: 'windows',
browser: 'native'
}
describe('Minimal harness loading', () => {
it('successfully loads', async function() {
// -- START: Standard Recorder-based unit test loading boilerplate --
let harness = new KeyboardHarness({}, MinimalKeymanGlobal);
let keyboardLoader = new NodeKeyboardLoader(harness);
let keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath);
// -- END: Standard Recorder-based unit test loading boilerplate --
// Asserts that the harness's loading field is cleared once the load is complete.
assert.isNotOk(harness.loadedKeyboard);
// Asserts that the `activeKeyboard` field was not set by the operation.
assert.isNotOk(harness.activeKeyboard);
// This part provides assurance that the keyboard properly loaded.
assert.equal(keyboard.id, "Keyboard_lao_2008_basic");
});
it('successfully loads (has variable stores)', async () => {
// -- START: Standard Recorder-based unit test loading boilerplate --
let harness = new KeyboardHarness({}, MinimalKeymanGlobal);
let keyboardLoader = new NodeKeyboardLoader(harness);
let keyboard = await keyboardLoader.loadKeyboardFromPath(ipaPath);
// -- END: Standard Recorder-based unit test loading boilerplate --
// This part provides extra assurance that the keyboard properly loaded.
assert.equal(keyboard.id, "Keyboard_sil_ipa");
});
it('cannot evaluate rules', async function() {
// -- START: Standard Recorder-based unit test loading boilerplate --
let harness = new KeyboardHarness({}, MinimalKeymanGlobal);
let keyboardLoader = new NodeKeyboardLoader(harness);
let keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath);
// -- END: Standard Recorder-based unit test loading boilerplate --
// Runs a blank KeyEvent through the keyboard's rule processing...
// but via separate harness configured with a different captured global.
let ruleHarness = new KeyboardInterface({}, MinimalKeymanGlobal);
ruleHarness.activeKeyboard = keyboard;
try {
ruleHarness.processKeystroke(new Mock(), keyboard.constructNullKeyEvent(device));
assert.fail();
} catch (err) {
// Drives home an important detail: the 'global' object is effectively
// closure-captured. (Similar constraints may occur when experimenting with
// 'sandboxed' keyboard loading in the DOM!)
assert.equal(err.message, 'k.KKM is not a function');
}
});
it('accurately determines supported gesture types', async () => {
// -- START: Standard Recorder-based unit test loading boilerplate --
let harness = new KeyboardHarness({}, MinimalKeymanGlobal);
let keyboardLoader = new NodeKeyboardLoader(harness);
let km_keyboard = await keyboardLoader.loadKeyboardFromPath(khmerPath);
// -- END: Standard Recorder-based unit test loading boilerplate --
// `khmer_angkor` - supports longpresses, but not flicks or multitaps.
const desktopLayout = km_keyboard.layout('desktop');
assert.isFalse(desktopLayout.hasFlicks);
assert.isFalse(desktopLayout.hasLongpresses);
assert.isFalse(desktopLayout.hasMultitaps);
const mobileLayout = km_keyboard.layout('phone');
assert.isFalse(mobileLayout.hasFlicks);
assert.isTrue(mobileLayout.hasLongpresses);
assert.isFalse(mobileLayout.hasMultitaps);
});
});
});

View file

@ -0,0 +1,34 @@
import { assert } from 'chai';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
import { KeyboardHarness, MinimalKeymanGlobal, DeviceSpec } from 'keyman/engine/keyboard';
import { NodeKeyboardLoader } from 'keyman/engine/keyboard/node-keyboard-loader';
describe('Keyboard tests', function () {
const khmerPath = require.resolve('@keymanapp/common-test-resources/keyboards/khmer_angkor.js');
it('accurately determines layout properties', async () => {
// -- START: Standard Recorder-based unit test loading boilerplate --
const harness = new KeyboardHarness({}, MinimalKeymanGlobal);
const keyboardLoader = new NodeKeyboardLoader(harness);
const km_keyboard = await keyboardLoader.loadKeyboardFromPath(khmerPath);
// -- END: Standard Recorder-based unit test loading boilerplate --
// `khmer_angkor` - supports longpresses, but not flicks or multitaps.
// Phone supports longpress if the keyboard supports it.
const mobileLayout = km_keyboard.layout(DeviceSpec.FormFactor.Phone);
assert.isTrue(mobileLayout.hasLongpresses);
assert.isFalse(mobileLayout.hasFlicks);
assert.isFalse(mobileLayout.hasMultitaps);
// Desktop doesn't support longpress even if the keyboard supports it.
const desktopLayout = km_keyboard.layout(DeviceSpec.FormFactor.Desktop);
assert.isFalse(desktopLayout.hasLongpresses);
assert.isFalse(desktopLayout.hasFlicks);
assert.isFalse(desktopLayout.hasMultitaps);
});
});

View file

@ -0,0 +1,89 @@
import { assert } from 'chai';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
import { KeyboardHarness, MinimalKeymanGlobal, KeyboardDownloadError, DeviceSpec, InvalidKeyboardError } from 'keyman/engine/keyboard';
import { KeyboardInterface, Mock } from 'keyman/engine/js-processor';
import { NodeKeyboardLoader } from 'keyman/engine/keyboard/node-keyboard-loader';
import { assertThrowsAsync, assertThrows } from 'keyman/tools/testing/test-utils';
describe('Headless keyboard loading', function() {
const laoPath = require.resolve('@keymanapp/common-test-resources/keyboards/lao_2008_basic.js');
const nonKeyboardPath = require.resolve('@keymanapp/common-test-resources/index.mjs');
const ipaPath = require.resolve('@keymanapp/common-test-resources/keyboards/sil_ipa.js');
const nonExisting = '/does/not/exist.js';
// Common test suite setup.
const device = {
formFactor: DeviceSpec.FormFactor.Desktop,
OS: DeviceSpec.OperatingSystem.Windows,
browser: DeviceSpec.Browser.Native,
touchable: false
}
describe('Minimal harness loading', () => {
it('successfully loads a single keyboard from filesystem', async () => {
// -- START: Standard Recorder-based unit test loading boilerplate --
const harness = new KeyboardInterface({}, MinimalKeymanGlobal);
const keyboardLoader = new NodeKeyboardLoader(harness);
const keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath);
// -- END: Standard Recorder-based unit test loading boilerplate --
// Asserts that the harness's loading field is cleared once the load is complete.
assert.isNotOk(harness.loadedKeyboard);
// Asserts that the `activeKeyboard` field was not set by the operation.
assert.isNotOk(harness.activeKeyboard);
// This part provides assurance that the keyboard properly loaded.
assert.equal(keyboard.id, "Keyboard_lao_2008_basic");
});
it('throws error when keyboard does not exist', async () => {
const harness = new KeyboardInterface({}, MinimalKeymanGlobal);
const keyboardLoader = new NodeKeyboardLoader(harness);
await assertThrowsAsync(async () => await keyboardLoader.loadKeyboardFromPath(nonExisting),
KeyboardDownloadError, `Unable to download keyboard at ${nonExisting}`);
});
it('throws error when keyboard is invalid', async () => {
const harness = new KeyboardInterface({}, MinimalKeymanGlobal);
const keyboardLoader = new NodeKeyboardLoader(harness);
await assertThrowsAsync(async () => await keyboardLoader.loadKeyboardFromPath(nonKeyboardPath),
InvalidKeyboardError, `${nonKeyboardPath} is not a valid keyboard file`);
});
it('successfully loads (has variable stores)', async () => {
// -- START: Standard Recorder-based unit test loading boilerplate --
const harness = new KeyboardInterface({}, MinimalKeymanGlobal);
const keyboardLoader = new NodeKeyboardLoader(harness);
const keyboard = await keyboardLoader.loadKeyboardFromPath(ipaPath);
// -- END: Standard Recorder-based unit test loading boilerplate --
// This part provides extra assurance that the keyboard properly loaded.
assert.equal(keyboard.id, "Keyboard_sil_ipa");
});
// TODO-WEB-CORE: figure out what the purpose of this test is
// TODO-WEB-CORE: move to kbdInterface.tests.ts
it('cannot evaluate rules', async function() {
// -- START: Standard Recorder-based unit test loading boilerplate --
const harness = new KeyboardHarness({}, MinimalKeymanGlobal);
const keyboardLoader = new NodeKeyboardLoader(harness);
const keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath);
// -- END: Standard Recorder-based unit test loading boilerplate --
// Runs a blank KeyEvent through the keyboard's rule processing...
// but via separate harness configured with a different captured global.
// This shows an important detail: the 'global' object is effectively
// closure-captured. (Similar constraints may occur when experimenting with
// 'sandboxed' keyboard loading in the DOM!)
const ruleHarness = new KeyboardInterface({}, MinimalKeymanGlobal);
ruleHarness.activeKeyboard = keyboard;
assertThrows(() => ruleHarness.processKeystroke(new Mock(), keyboard.constructNullKeyEvent(device)), 'k.KKM is not a function');
});
});
});

View file

@ -24,7 +24,7 @@ export default {
concurrency: 10,
nodeResolve: true,
files: [
'build/test/integrated//**/*.spec.mjs',
'web/build/test/integrated//**/*.spec.mjs',
// '**/*.spec.html'
],
middleware: [

View file

@ -0,0 +1,10 @@
# Manual tests
To run the the manual tests, start the test web server with:
```bash
cd "$KEYMAN_ROOT"
web/build.sh start
```
Then open <http://localhost:3000> in your browser.

View file

@ -57,4 +57,4 @@ function do_copy() {
}
builder_run_action clean rm -rf "$KEYMAN_ROOT/$DEST"
builder_run_action build do_copy
builder_run_action build do_copy

View file

@ -1,17 +1,15 @@
// JavaScript Document samplehdr.js: Keyboard management for KeymanWeb demonstration pages
/*
/*
This script is designed to test KeymanWeb error message handling.
*/
function loadKeyboards()
{
function loadKeyboards()
{
var kmw=keyman;
// We start by adding a keyboard correctly. It's best to include a 'control' in our experiment.
kmw.addKeyboards({id:'us',name:'English',languages:{id:'en',name:'English'},
filename:'../us-1.0.js'});
// Insert a keyboard that cannot be found.
kmw.addKeyboards({id:'lao_2008_basic',name:'wrong-filename',
languages:{
@ -19,9 +17,9 @@
font:{family:'LaoWeb',source:['../font/saysettha_web.ttf','../font/saysettha_web.woff','../font/saysettha_web.eot']}
},
filename:'./missing_file.js' // Intentional error - the file doesn't exist, so the <script> tag will raise an error event.
});
// Insert a keyboard that will generate a timing error.
});
// Insert a keyboard that will generate a timing error.
kmw.addKeyboards({id:'unparsable',name:'non-parsable',
languages:{
id:'lo',name:'debugging',region:'Asia',
@ -29,14 +27,16 @@
},
filename:'./unparsable.js' // Intentional error - the file has no parsable keyboard, so while the <script> tag will load,
// registration will fail.
});
});
// Insert a keyboard that will generate a timing error.
// Insert a keyboard that will generate a timing error. `timeout.js` doesn't
// exist, but the test server (web/src/tools/testing/test-server/index.cjs)
// has special handling for that URL and times out after 10 seconds.
kmw.addKeyboards({id:'timeout',name:'timeout',
languages:{
id:'lo',name:'debugging',region:'Asia',
font:{family:'LaoWeb',source:['../font/saysettha_web.ttf','../font/saysettha_web.woff','../font/saysettha_web.eot']}
},
filename:'./timeout.js' // Intentional (simulated) error - the file never loads, simulating a server timeout.
});
});
}

View file

@ -65,9 +65,32 @@
<h3>or in this input field:</h3>
<input class='test' value='' placeholder='or here'/>
<h2>Expected error messages:</h2>
<ul>
<li><i>wrong-filename</i> keyboard: "Unable to download wrong-filename keyboard for debugging"</li>
<li><i>non-parsable</i> keyboard: "Error registering the non-parsable keyboard for debugging; keyboard script at ./unparsable.js may contain an error."</li>
<li><i>timeout</i> keyboard: "Sorry, the timeout keyboard for debugging is not currently available."</li>
</ul>
<h3><a href="../index.html">Return to testing home page</a></h3>
</div>
<script>
if (!window.location.href.startsWith('http://localhost:3000')) {
const body = document.getElementsByTagName('body')[0];
body.innerHTML = `<h1>KeymanWeb Sample Page - Error Testing page</h1>
<h2>Unable to load this test page!</h2>
<p>This page has to be loaded through the local test server.</p>
<p>Do the following:</p>
<ol>
<li>Open a terminal and navigate to the Keyman source root directory</li>
<li>Start the test server with <code>web/build.sh start</code></li>
<li>Open <a href="http://localhost:3000/src/test/manual/web/keyboard-errors/index.html">http://localhost:3000/src/test/manual/web/keyboard-errors/index.html</a> in a browser</li>
</ol>
`;
}
</script>
</body>
<!--

View file

@ -1,21 +0,0 @@
(function() {
var me = document.currentScript;
console.log(me);
var onload = me.onload;
if(onload) {
me.onload = null;
}
document.body.addEventListener('load', function(e) {
// Prevent the element's onload event from firing
if(e.srcElement == me || e.target == me) {
e.cancelBubble = true;
}
}, {capture: true});
window.setTimeout(function () {
// Restores the function after a slight delay.
me.onload = onload;
}, 1);
})();

View file

@ -22,7 +22,8 @@ builder_describe "Builds the Keyman Engine for Web's development & unit-testing
"--ci Does nothing for this script" \
":bulk_rendering=testing/bulk_rendering Builds the bulk-rendering tool used to validate changes to OSK display code" \
":recorder=testing/recorder Builds the KMW recorder tool used for development of unit-test resources" \
":sourcemap-root=building/sourcemap-root Builds the sourcemap-cleaning tool used during minification of app/ builds"
":sourcemap-root=building/sourcemap-root Builds the sourcemap-cleaning tool used during minification of app/ builds" \
":test-utils=testing/test-utils Builds the test-utils module"
builder_parse "$@"

View file

@ -0,0 +1,25 @@
const express = require('express')
const path = require('path')
const app = express()
const port = 3000
app.use(express.static(path.join(__dirname, '../../../../')))
// for testing timeout error in web/src/test/manual/web/keyboard-errors
const router = express.Router()
router.get('/src/test/manual/web/keyboard-errors/timeout.js', async (req, res, next) => {
console.log('timeout.js requested')
return new Promise(() => {
setTimeout(() => {
res.set('Content-Type', 'application/json');
res.status(200);
next();
}, 10500); // > ContextManagerBase.TIMEOUT_THRESHOLD (10 seconds)
});
})
app.use(router)
app.listen(port, () => {
console.log(`Keyman test app listening on port ${port}`)
})

View file

@ -0,0 +1,34 @@
#!/usr/bin/env bash
#
# Compile KeymanWeb's automated js/ts test utilities
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../../../../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
SUBPROJECT_NAME=tools/testing/test-utils
. "${KEYMAN_ROOT}/web/common.inc.sh"
. "${KEYMAN_ROOT}/resources/shellHelperFunctions.sh"
################################ Main script ################################
builder_describe "Automated js/ts test utilities for KeymanWeb" \
"clean" \
"configure" \
"build"
builder_describe_outputs \
configure /node_modules \
build "/web/build/${SUBPROJECT_NAME}/obj/index.js"
builder_parse "$@"
do_build ( ) {
compile "${SUBPROJECT_NAME}"
}
builder_run_action configure verify_npm_setup
builder_run_action clean rm -rf "../../../../build/${SUBPROJECT_NAME}/"
builder_run_action build do_build

View file

@ -0,0 +1,31 @@
import { assert } from 'chai';
// export async function assertThrowsAsync(fn: () => Promise<any>, message?: string): Promise<void>;
export async function assertThrowsAsync(fn: () => Promise<any>, type?: any, message?: string): Promise<void> {
assert(!!type || !!message, 'at least one of type or message must be specified');
if (typeof(type) === 'string') {
message = type;
type = undefined;
}
try {
await fn();
assert.fail('Expected function to throw an error, but it did not.');
} catch (err) {
if (type) {
assert.isTrue(err instanceof type, `Expected error to be of type ${type.name}, but got ${err.constructor.name}`);
}
if (message) {
assert.equal((err as Error).message, message);
}
}
}
export function assertThrows(fn: () => any, message?: string): void;
export function assertThrows(fn: () => any, type?: any, message?: string): void {
assert(!!type || !!message, 'at least one of type or message must be specified');
if (typeof(type) === 'string') {
message = type;
type = undefined;
}
assert.throws(fn, type, message);
}

View file

@ -0,0 +1,11 @@
{
"extends": "../../../tsconfig.dom.json",
"compilerOptions": {
"baseUrl": "./",
"outDir": "../../../../build/tools/testing/test-utils/obj/",
"rootDir": ".",
"tsBuildInfoFile": "../../../../build/tools/testing/test-utils/obj/tsconfig.tsbuildinfo"
},
"include": [ "*.ts" ],
}

View file

@ -41,6 +41,8 @@ fi
# End common configs.
builder_run_action test:dom web-test-runner --config "src/test/auto/dom/web-test-runner${WTR_CONFIG}.config.mjs" ${WTR_DEBUG}
cd "${KEYMAN_ROOT}"
builder_run_action test:integrated web-test-runner --config "src/test/auto/integrated/web-test-runner${WTR_CONFIG}.config.mjs" ${WTR_DEBUG}
builder_run_action test:dom web-test-runner --config "web/src/test/auto/dom/web-test-runner${WTR_CONFIG}.config.mjs" ${WTR_DEBUG}
builder_run_action test:integrated web-test-runner --config "web/src/test/auto/integrated/web-test-runner${WTR_CONFIG}.config.mjs" ${WTR_DEBUG}