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

This commit is contained in:
Keyman Server 2025-09-27 06:39:29 +02:00 • committed by GitHub
commit a56110950f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
172 changed files with 4444 additions and 1367 deletions

View file

@ -11,12 +11,20 @@
#if defined(__LP64__) || defined(_LP64)
/* 64-bit, g++ */
#define KMX_64BIT
#define KMX_REQUIRES_REALIGNMENT
#endif
#if defined(_WIN64) && !defined(USE_64)
/* 64-bit, Windows */
#define KMX_64BIT
#define KMX_REQUIRES_REALIGNMENT
#endif
#if defined(__EMSCRIPTEN__)
// Emscripten/WASM. Emscripten even though it uses 32-bit (and not 64-bit
// pointers like the 64-bit architectures above) requires 32-bit alignment
// for pointers which we don't always have in the KMX data from file
// (see #12844).
#define KMX_REQUIRES_REALIGNMENT
#endif
typedef uint32_t KMX_DWORD;

View file

@ -302,11 +302,15 @@ typedef uint8_t (*km_core_keyboard_imx_platform)(km_core_state*, uint32_t, void*
## Description
An error code mechanism similar to COM’s `HRESULT` scheme (unlike COM, any
An error code mechanism similar to COM's `HRESULT` scheme (unlike COM, any
non-zero value is an error).
## Specification
-->
// keep in sync with web/src/engine/core-processor/src/core-factory.ts
// (see https://github.com/emscripten-core/emscripten/issues/18585)
<!--
```c */
enum km_core_status_codes {
KM_CORE_STATUS_OK = 0,
@ -1162,7 +1166,7 @@ returned by [km_core_keyboard_load_from_blob].
```c */
KMN_API
void
km_core_keyboard_dispose(km_core_keyboard *keyboard);
km_core_keyboard_dispose(km_core_keyboard const* keyboard);
/*
```
@ -1380,7 +1384,7 @@ the environment passed.
```c */
KMN_API
km_core_status
km_core_state_create(km_core_keyboard *keyboard,
km_core_state_create(km_core_keyboard const *keyboard,
km_core_option_item const *env,
km_core_state **out);
@ -1466,7 +1470,7 @@ invalid.
```c */
KMN_API
void
km_core_state_dispose(km_core_state *state);
km_core_state_dispose(km_core_state const *state);
/*
```
@ -1534,7 +1538,7 @@ Returns a debug formatted string of the context from the state.
```c */
KMN_API
km_core_cu *
km_core_state_context_debug(km_core_state *state, km_core_debug_context_type context_type);
km_core_state_context_debug(const km_core_state *state, km_core_debug_context_type context_type);
/*
```
@ -1673,7 +1677,7 @@ the state may also be modified.
```c */
KMN_API
km_core_status
km_core_process_event(km_core_state *state,
km_core_process_event(km_core_state const *state,
km_core_virtual_key vk,
uint16_t modifier_state,
uint8_t is_key_down,
@ -1729,7 +1733,7 @@ the state may also be modified.
KMN_API
km_core_status
km_core_event(
km_core_state *state,
km_core_state const *state,
uint32_t event,
void* data
);

View file

@ -33,7 +33,7 @@
#undef _kmn_export_flag
#undef _kmn_import_flag
#undef _kmn_static_flag
#else // How MSVC sepcifies function level attributes adn deprecation
#else // How MSVC sepcifies function level attributes and deprecation
#define _kmn_and
#define _kmn_tag_fn(a) __declspec(a)
#define _kmn_deprecated_flag deprecated

View file

@ -37,7 +37,10 @@ endif
# shared with a number of subdirs
if cpp_compiler.get_id() == 'emscripten'
wasm_exported_runtime_methods = '-sEXPORTED_RUNTIME_METHODS=[\'UTF8ToString\',\'stringToNewUTF8\']'
wasm_exported_runtime_methods = '-sEXPORTED_RUNTIME_METHODS=[\'UTF8ToString\',\'stringToNewUTF8\',\'wasmExports\']'
# For Google Test
add_global_arguments('-pthread', language: [ 'cpp', 'c' ] )
endif
subdir('docs/internal')

View file

@ -40,18 +40,16 @@ namespace
} // namespace
km_core_status
km_core_keyboard_load_from_blob(
keyboard_load_from_blob_internal(
const km_core_path_name kb_name,
const void* blob,
const size_t blob_size,
const std::vector<uint8_t>& buf,
km_core_keyboard** keyboard
) {
assert(keyboard);
if (!keyboard || !blob) {
if (!keyboard) {
return KM_CORE_STATUS_INVALID_ARGUMENT;
}
std::vector<uint8_t> buf((uint8_t*)blob, (uint8_t*)blob + blob_size);
*keyboard = nullptr;
try {
abstract_processor* kp = processor_factory(kb_name, buf);
@ -67,8 +65,23 @@ km_core_keyboard_load_from_blob(
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
) {
if (!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);
}
void
km_core_keyboard_dispose(km_core_keyboard *keyboard)
km_core_keyboard_dispose(km_core_keyboard const* keyboard)
{
delete keyboard;
}

View file

@ -15,7 +15,7 @@
km_core_status
km_core_event(
km_core_state *state,
km_core_state const *state,
uint32_t event,
void* data
) {
@ -37,19 +37,20 @@ km_core_event(
return KM_CORE_STATUS_INVALID_ARGUMENT;
}
return state->processor().external_event(state, event, data);
return const_cast<km_core_state*>(state)->processor().external_event(const_cast<km_core_state*>(state), event, data);
}
km_core_status
km_core_process_event(km_core_state *state,
km_core_process_event(km_core_state const *state_,
km_core_virtual_key vk,
uint16_t modifier_state,
uint8_t is_key_down,
uint16_t event_flags) {
assert(state != nullptr);
if(state == nullptr) {
assert(state_ != nullptr);
if(state_ == nullptr) {
return KM_CORE_STATUS_INVALID_ARGUMENT;
}
km_core_state *state = const_cast<km_core_state*>(state_);
km_core_status status = state->processor().process_event(state, vk, modifier_state, is_key_down, event_flags);
if (state_should_invalidate_context(state, vk, modifier_state, is_key_down, event_flags)) {

View file

@ -28,7 +28,7 @@ using namespace km::core;
// Forward declarations
class context;
km_core_status km_core_state_create(km_core_keyboard * keyboard,
km_core_status km_core_state_create(km_core_keyboard const *keyboard,
km_core_option_item const *env,
km_core_state ** out)
{
@ -38,7 +38,7 @@ km_core_status km_core_state_create(km_core_keyboard * keyboard,
try
{
*out = new km_core_state(static_cast<abstract_processor&>(*keyboard), env);
*out = new km_core_state(static_cast<abstract_processor&>(*const_cast<km_core_keyboard*>(keyboard)), env);
}
catch (std::bad_alloc &)
{
@ -60,7 +60,7 @@ km_core_status km_core_state_clone(km_core_state const *state,
}
void km_core_state_dispose(km_core_state *state)
void km_core_state_dispose(km_core_state const *state)
{
delete state;
}
@ -301,10 +301,11 @@ km_core_cu * _new_error_string(std::u16string const str) {
}
km_core_cu * km_core_state_context_debug(
km_core_state *state,
const km_core_state *state_,
km_core_debug_context_type context_type
) {
km_core_context_item * context_items = nullptr;
km_core_state* state = const_cast<km_core_state*>(state_);
if(context_type == KM_CORE_DEBUG_CONTEXT_INTERMEDIATE) {
if(km_core_state_get_intermediate_context(state, &context_items) != KM_CORE_STATUS_OK) {

View file

@ -68,11 +68,11 @@ KMX_BOOL KMX_ProcessEvent::LoadKeyboardFromBlob(
*lpKeyboard = NULL;
#ifdef KMX_64BIT
#ifdef KMX_REQUIRES_REALIGNMENT
// allocate enough memory for expanded data structure + original data.
// Expanded data structure is double the size of data on disk (8-byte
// pointers) - on disk the "pointers" are relative to the beginning of
// the file.
// Expanded data structure is no more than double the size of data on
// disk (8-byte pointers for 64-bit architectures) - on disk the
// "pointers" are relative to the beginning of the file.
// We save the original data at the end of buf; we don't copy strings, so
// those will remain in the location at the end of the buffer.
buf = new KMX_BYTE[sz * 3];
@ -84,7 +84,7 @@ KMX_BOOL KMX_ProcessEvent::LoadKeyboardFromBlob(
DebugLog("Not allocmem");
return FALSE;
}
#ifdef KMX_64BIT
#ifdef KMX_REQUIRES_REALIGNMENT
filebase = buf + sz * 2;
#else
filebase = buf;
@ -103,7 +103,7 @@ KMX_BOOL KMX_ProcessEvent::LoadKeyboardFromBlob(
return FALSE;
}
#ifdef KMX_64BIT
#ifdef KMX_REQUIRES_REALIGNMENT
kbp = CopyKeyboard(buf, filebase);
#else
kbp = FixupKeyboard(buf, filebase);
@ -132,13 +132,15 @@ PKMX_WCHAR KMX_ProcessEvent::StringOffset(PKMX_BYTE base, KMX_DWORD offset)
return (PKMX_WCHAR)(base + offset);
}
#ifdef KMX_64BIT
#ifdef KMX_REQUIRES_REALIGNMENT
/**
CopyKeyboard will copy the data read into bufp from x86-sized structures into
x64-sized structures starting at `base`
* After this function finishes, we still need to keep the original data because
we don't copy the strings
This method is used on 64-bit architectures.
This method is used on 64-bit architectures as well as with Emscripten.
Emscripten requires 32-bit alignment for pointers which we don't always
have in the KMX data (see #12844).
*/
LPKEYBOARD KMX_ProcessEvent::CopyKeyboard(PKMX_BYTE bufp, PKMX_BYTE base)
{

View file

@ -60,7 +60,7 @@ private:
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
#ifdef KMX_REQUIRES_REALIGNMENT
LPKEYBOARD CopyKeyboard(PKMX_BYTE bufp, PKMX_BYTE base);
#else
LPKEYBOARD FixupKeyboard(PKMX_BYTE bufp, PKMX_BYTE base);

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

@ -32,8 +32,6 @@ else
endif
if cpp_compiler.get_id() == 'emscripten'
# TODO: why do we need this defn here?
defns += ['-DKM_CORE_LIBRARY']
icu_if_not_on_wasm = []
else
# only include this if NOT on wasm.
@ -54,19 +52,21 @@ if cpp_compiler.get_id() == 'emscripten'
'-g', '-Wlimited-postlink-optimizations',
'-lembind'
]
util_normalize_table_generator = executable(
'util_normalize_table_generator',
['util_normalize_table_generator.cpp'],
cpp_args: defns + warns,
include_directories: [inc],
link_args: links,
dependencies: [icu_uc, icu_i18n],
)
util_normalize_table_generator = executable('util_normalize_table_generator',
['util_normalize_table_generator.cpp'],
cpp_args: defns + warns,
include_directories: [inc],
link_args: links,
dependencies: [icu_uc, icu_i18n],
)
util_normalize_table_h = custom_target('util_normalize_table.h',
output: 'util_normalize_table.h',
command: [util_normalize_table_generator],
capture:true)
util_normalize_table_h = custom_target(
'util_normalize_table.h',
output: 'util_normalize_table.h',
command: [util_normalize_table_generator],
capture:true
)
generated_headers += util_normalize_table_h
endif
@ -122,6 +122,7 @@ api_files = files(
'km_core_state_api.cpp',
'km_core_debug_api.cpp',
'km_core_processevent_api.cpp',
'wasm.cpp',
)
core_files = files(
@ -137,6 +138,33 @@ mock_files = files(
'mock/mock_processor.cpp',
)
extra_defns = []
extra_links = []
if cpp_compiler.get_id() == 'emscripten'
extra_defns += ['-g']
extra_links += ['-sSAFE_HEAP=1', wasm_exported_runtime_methods,
# Forcing inclusion of debug symbols
'-g', '-Wlimited-postlink-optimizations',
'--emit-tsd', 'keymancore.d.ts',
'-lembind'
]
host_links = [
'--whole-archive',
'-sALLOW_MEMORY_GROWTH=1',
'-sMODULARIZE=1', '-sEXPORT_NAME=createCoreProcessor',
'-sEXPORT_ES6',
'-sENVIRONMENT=web,webview',
# '-sERROR_ON_UNDEFINED_SYMBOLS=0'
]
node_links = [
'--whole-archive',
'-sALLOW_MEMORY_GROWTH=1',
'-sMODULARIZE=1', '-sEXPORT_NAME=createCoreProcessor',
'-sEXPORT_ES6',
'-sENVIRONMENT=node',
]
endif
lib = library('keymancore',
api_files,
core_files,
@ -144,8 +172,8 @@ lib = library('keymancore',
mock_files,
version_res,
generated_headers,
cpp_args: defns + warns + flags,
link_args: links,
cpp_args: defns + extra_defns + warns + flags,
link_args: links + extra_links,
version: lib_version,
include_directories: inc,
pic: true,
@ -164,3 +192,31 @@ pkg.generate(
description: 'Keyman processor for KMN keyboards.',
subdirs: headerdirs,
libraries: lib)
if cpp_compiler.get_id() == 'emscripten'
# Build an executable for web/webview
host = executable('km-core',
cpp_args: defns + extra_defns,
include_directories: inc,
link_args: links + host_links + extra_links,
objects: lib.extract_all_objects(recursive: false))
# Build an executable for node (used for testing)
host = executable('km-core-node',
cpp_args: defns + extra_defns,
include_directories: inc,
link_args: links + node_links + extra_links,
objects: lib.extract_all_objects(recursive: false),
name_suffix: 'mjs')
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

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

@ -0,0 +1,288 @@
#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h>
#include <emscripten/bind.h>
#include <vector>
#ifdef __cplusplus
#define EXTERN extern "C" EMSCRIPTEN_KEEPALIVE
#else
#define EXTERN EMSCRIPTEN_KEEPALIVE
#endif
#include <keyman_core.h>
#include "processor.hpp"
#include "state.hpp"
namespace em = emscripten;
// This little bit of magic gives us implicit bindings for any `std::vector`,
// so long as we bind `emscripten::value_object<T>`, and comes from:
// https://github.com/emscripten-core/emscripten/issues/11070#issuecomment-717675128
namespace emscripten {
namespace internal {
template <typename T, typename Allocator>
struct BindingType<std::vector<T, Allocator>> {
using ValBinding = BindingType<val>;
using WireType = ValBinding::WireType;
#if __EMSCRIPTEN_major__ == 3 && __EMSCRIPTEN_minor__ == 1 && __EMSCRIPTEN_tiny__ >= 60
// emscripten-core/emscripten#21692
static WireType toWireType(const std::vector<T, Allocator> &vec, rvp::default_tag) {
return ValBinding::toWireType(val::array(vec), rvp::default_tag{});
}
#else
static WireType toWireType(const std::vector<T, Allocator> &vec) {
return ValBinding::toWireType(val::array(vec));
}
#endif
static std::vector<T, Allocator> fromWireType(WireType value) {
return vecFromJSArray<T>(ValBinding::fromWireType(value));
}
};
template <typename T>
struct TypeID<T,
typename std::enable_if_t<std::is_same<
typename Canonicalized<T>::type,
std::vector<typename Canonicalized<T>::type::value_type,
typename Canonicalized<T>::type::allocator_type>>::value>> {
static constexpr TYPEID get() { return TypeID<val>::get(); }
};
} // namespace internal
} // namespace emscripten
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;
}
struct km_core_option_item_wasm {
std::u16string key;
std::u16string value;
uint8_t scope;
};
class km_core_keyboard_attrs_wasm {
public:
std::u16string version_string;
std::u16string id;
std::vector<km_core_option_item_wasm> default_options;
};
class km_core_actions_wasm {
public:
unsigned int code_points_to_delete;
std::u32string output;
std::vector<km_core_option_item_wasm> persist_options;
bool do_alert;
bool emit_keystroke;
int new_caps_lock_state;
std::u32string deleted_context;
};
template <typename T> class CoreReturn {
public:
CoreReturn(int status = 0, const T* obj = nullptr) : status(status), object(obj) {
}
void setStatus(int status) {
this->status = status;
}
int getStatus() const {
return status;
}
void setObject(const T* obj) {
object = obj;
}
const T* getObject() const {
return object;
}
private:
int status;
const T* object;
};
km_core_status
keyboard_load_from_blob_internal(const km_core_path_name kb_name, const std::vector<uint8_t>& buf, km_core_keyboard** keyboard);
EMSCRIPTEN_KEEPALIVE const CoreReturn<km_core_keyboard>*
km_core_keyboard_load_from_blob_wasm(
std::string kb_name,
const emscripten::val& blob_val
) {
std::vector<uint8_t> blob = emscripten::convertJSArrayToNumberVector<uint8_t>(blob_val);
km_core_keyboard* keyboard_ptr = nullptr;
km_core_status retVal = ::keyboard_load_from_blob_internal(kb_name.c_str(), blob, &keyboard_ptr);
return new CoreReturn<km_core_keyboard>(retVal, keyboard_ptr);
}
EMSCRIPTEN_KEEPALIVE const CoreReturn<km_core_keyboard_attrs_wasm>*
km_core_keyboard_get_attrs_wasm(const km_core_keyboard* keyboard) {
const km_core_keyboard_attrs* attrs = nullptr;
km_core_status retVal = km_core_keyboard_get_attrs(keyboard, &attrs);
km_core_keyboard_attrs_wasm* attrs_wasm = nullptr;
if (attrs) {
attrs_wasm = new km_core_keyboard_attrs_wasm();
attrs_wasm->version_string = std::u16string(attrs->version_string);
attrs_wasm->id = std::u16string(attrs->id);
for (const km_core_option_item* item = attrs->default_options; item->key; ++item) {
km_core_option_item_wasm item_wasm;
item_wasm.key = std::u16string(item->key);
item_wasm.value = std::u16string(item->value);
item_wasm.scope = item->scope;
attrs_wasm->default_options.push_back(item_wasm);
}
}
return new CoreReturn<km_core_keyboard_attrs_wasm>(retVal, attrs_wasm);
}
EMSCRIPTEN_KEEPALIVE const CoreReturn<km_core_state>*
km_core_state_create_wasm(const km_core_keyboard* keyboard,
const std::vector<km_core_option_item_wasm>& env) {
km_core_option_item* env_c = new km_core_option_item[env.size() + 1];
for (size_t i = 0; i < env.size(); ++i) {
env_c[i].key = env[i].key.c_str();
env_c[i].value = env[i].value.c_str();
env_c[i].scope = env[i].scope;
}
env_c[env.size()] = {nullptr, nullptr, 0};
km_core_state* state = nullptr;
km_core_status retVal = km_core_state_create(keyboard, env_c, &state);
delete[] env_c;
return new CoreReturn<km_core_state>(retVal, state);
}
EMSCRIPTEN_KEEPALIVE const CoreReturn<km_core_state>*
km_core_state_clone_wasm(const km_core_state* state) {
km_core_state* new_state = nullptr;
km_core_status retVal = km_core_state_clone(state, &new_state);
return new CoreReturn<km_core_state>(retVal, new_state);
}
EMSCRIPTEN_KEEPALIVE int
km_core_state_context_set_if_needed_wasm(
km_core_state const* state,
std::u16string application_context) {
return km_core_state_context_set_if_needed(const_cast<km_core_state*>(state), application_context.c_str());
}
EMSCRIPTEN_KEEPALIVE std::u16string
km_core_state_context_debug_wasm(const km_core_state* state, int context_type) {
std::u16string context;
km_core_cu* context_c = km_core_state_context_debug(state, static_cast<km_core_debug_context_type>(context_type));
if (context_c) {
context = std::u16string(context_c);
km_core_cu_dispose(context_c);
}
return context;
}
EMSCRIPTEN_KEEPALIVE km_core_actions_wasm*
km_core_state_get_actions_wasm(km_core_state const *state) {
km_core_actions const* actions = km_core_state_get_actions(state);
km_core_actions_wasm* actions_wasm = new km_core_actions_wasm();
actions_wasm->code_points_to_delete = actions->code_points_to_delete;
actions_wasm->output = std::u32string(actions->output);
actions_wasm->do_alert = actions->do_alert;
actions_wasm->emit_keystroke = actions->emit_keystroke;
actions_wasm->new_caps_lock_state = actions->new_caps_lock_state;
actions_wasm->deleted_context = std::u32string(actions->deleted_context);
for (const km_core_option_item* item = actions->persist_options; item->key; ++item) {
km_core_option_item_wasm item_wasm;
item_wasm.key = std::u16string(item->key);
item_wasm.value = std::u16string(item->value);
item_wasm.scope = item->scope;
actions_wasm->persist_options.push_back(item_wasm);
}
return actions_wasm;
}
EMSCRIPTEN_BINDINGS(core_interface) {
em::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, em::allow_raw_pointers())
;
em::value_object<km_core_option_item_wasm>("km_core_option_item")
.field("key", &km_core_option_item_wasm::key)
.field("value", &km_core_option_item_wasm::value)
.field("scope", &km_core_option_item_wasm::scope);
em::function("tmp_wasm_attributes", &tmp_wasm_attributes);
// Unfortunately embind has an open issue with enums and typescript where it
// only generates a type for the enum, but not the values in a usable way.
// Therefore it's not much use to expose the enum here.
// See https://github.com/emscripten-core/emscripten/issues/18585
// em::enum_<km_core_status_codes>("km_core_status_codes")
// .value("OK", KM_CORE_STATUS_OK)
// .value("NO_MEM", KM_CORE_STATUS_NO_MEM)
// .value("IO_ERROR", KM_CORE_STATUS_IO_ERROR)
// .value("INVALID_ARGUMENT", KM_CORE_STATUS_INVALID_ARGUMENT)
// .value("KEY_ERROR", KM_CORE_STATUS_KEY_ERROR)
// .value("INSUFFICENT_BUFFER", KM_CORE_STATUS_INSUFFICENT_BUFFER)
// .value("INVALID_UTF", KM_CORE_STATUS_INVALID_UTF)
// .value("INVALID_KEYBOARD", KM_CORE_STATUS_INVALID_KEYBOARD)
// .value("NOT_IMPLEMENTED", KM_CORE_STATUS_NOT_IMPLEMENTED)
// .value("OS_ERROR", KM_CORE_STATUS_OS_ERROR);
em::class_<km_core_keyboard>("km_core_keyboard");
em::class_<CoreReturn<km_core_keyboard>>("CoreKeyboardReturn")
.property("status", &CoreReturn<km_core_keyboard>::getStatus)
.property("object", &CoreReturn<km_core_keyboard>::getObject, em::allow_raw_pointers());
em::class_<CoreReturn<km_core_keyboard_attrs_wasm>>("CoreKeyboardAttrsReturn")
.property("status", &CoreReturn<km_core_keyboard_attrs_wasm>::getStatus)
.property("object", &CoreReturn<km_core_keyboard_attrs_wasm>::getObject, em::allow_raw_pointers());
em::class_<km_core_keyboard_attrs_wasm>("km_core_keyboard_attrs")
.property("version_string", &km_core_keyboard_attrs_wasm::version_string)
.property("id", &km_core_keyboard_attrs_wasm::id)
.property("default_options", &km_core_keyboard_attrs_wasm::default_options);
em::class_<km_core_actions_wasm>("km_core_actions")
.property("code_points_to_delete", &km_core_actions_wasm::code_points_to_delete)
.property("output", &km_core_actions_wasm::output)
.property("persist_options", &km_core_actions_wasm::persist_options)
.property("do_alert", &km_core_actions_wasm::do_alert)
.property("emit_keystroke", &km_core_actions_wasm::emit_keystroke)
.property("new_caps_lock_state", &km_core_actions_wasm::new_caps_lock_state)
.property("deleted_context", &km_core_actions_wasm::deleted_context);
em::class_<km_core_state>("km_core_state");
em::class_<CoreReturn<km_core_state>>("CoreStateReturn")
.property("status", &CoreReturn<km_core_state>::getStatus)
.property("object", &CoreReturn<km_core_state>::getObject, em::allow_raw_pointers());
em::function("keyboard_load_from_blob", &km_core_keyboard_load_from_blob_wasm, em::allow_raw_pointers());
em::function("keyboard_dispose", &km_core_keyboard_dispose, em::allow_raw_pointers());
em::function("keyboard_get_attrs", &km_core_keyboard_get_attrs_wasm, em::allow_raw_pointers());
em::function("state_create", &km_core_state_create_wasm, em::allow_raw_pointers());
em::function("state_clone", &km_core_state_clone_wasm, em::allow_raw_pointers());
em::function("state_dispose", &km_core_state_dispose, em::allow_raw_pointers());
em::function("process_event", &km_core_process_event, em::allow_raw_pointers());
em::function("state_context_set_if_needed", &km_core_state_context_set_if_needed_wasm, em::allow_raw_pointers());
em::function("state_context_clear", &km_core_state_context_clear, em::allow_raw_pointers());
em::function("state_context_debug", &km_core_state_context_debug_wasm, em::allow_raw_pointers());
em::function("state_get_actions", &km_core_state_get_actions_wasm, em::allow_raw_pointers());
}
#endif

View file

@ -10,10 +10,7 @@
cmpfiles = ['-c', 'import sys; a = open(sys.argv[1], \'r\').read(); b = open(sys.argv[2], \'r\').read(); sys.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

@ -239,7 +239,7 @@ kbd_log = custom_target(test_kbd + '.kmx'.underscorify(),
input: kbd_src,
command: kmc_cmd + ['build', '--debug', '--no-compiler-version', '@INPUT@', '--out-file', kbd_obj]
)
test('imx_list', imx_e, depends: kbd_log, args: [kbd_obj] )
test('imx_list', imx_e, depends: kbd_log, args: [kbd_obj] )
external_e = executable('ext_event_tests', ['kmx_external_event.tests.cpp', common_test_files],
cpp_args: defns + warns,
@ -258,4 +258,4 @@ kbd_log = custom_target(test_kbd + '.kmx'.underscorify(),
input: kbd_src,
command: kmc_cmd + ['build', '--debug', '--no-compiler-version', '@INPUT@', '--out-file', kbd_obj]
)
test('ext_event', external_e, depends: kbd_log, args: [kbd_obj] )
test('ext_event', external_e, depends: kbd_log, args: [kbd_obj] )

View file

@ -53,3 +53,4 @@ subdir('utftest')
subdir('kmnkbd')
subdir('kmx')
subdir('ldml')
subdir('wasm')

View file

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

View file

@ -0,0 +1,5 @@
if cpp_compiler.get_id() == 'emscripten'
npm = find_program('npm', required: true)
test('wasm-tests', npm, args: ['test'], workdir: meson.current_source_dir())
endif

1425
core/tests/unit/wasm/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,16 @@
{
"name": "wasm-tests",
"version": "1.0.0",
"description": "Unit tests for WASM version of Core",
"main": "tests.js",
"type": "module",
"scripts": {
"test": "mocha ."
},
"author": "SIL Global",
"license": "MIT",
"devDependencies": {
"chai": "^5.1.2",
"mocha": "^11.0.1"
}
}

View file

@ -0,0 +1,149 @@
import { assert } from 'chai';
import { fileURLToPath, pathToFileURL } from 'url';
import fs from 'fs';
import path from 'path';
let km_core = null;
describe('Unit tests for wasm Core API', function () {
let __dirname = null;
before(async function () {
// Define __dirname for ES modules
const filename = fileURLToPath(import.meta.url);
__dirname = path.join(path.dirname(filename), '../../../build/wasm/debug');
let km_core_file = path.join(__dirname, 'src/km-core-node.mjs');
if (!fs.existsSync(km_core_file)) {
__dirname = path.join(path.dirname(filename), '../../../build/wasm/release');
km_core_file = path.join(__dirname, 'src/km-core-node.mjs');
}
const wasmdir = path.dirname(km_core_file);
const module = await import(pathToFileURL(km_core_file));
const createCoreProcessor = module.default;
km_core = await createCoreProcessor({
locateFile: function (path, scriptDirectory) {
return wasmdir + '/' + path;
}
});
});
it('can load keyboard from blob', function () {
// Setup
const kmxdata = fs.readFileSync(path.join(__dirname, 'tests/unit/kmx/k_020___deadkeys_and_backspace.kmx'));
// Execute
const result = km_core.keyboard_load_from_blob('k_020___deadkeys_and_backspace', kmxdata);
// Verify
assert.equal(result.status, 0);
assert.isOk(result.object);
});
function loadKeyboard(name) {
const kmxdata = fs.readFileSync(path.join(__dirname, `tests/unit/kmx/${name}.kmx`));
const result = km_core.keyboard_load_from_blob(name, kmxdata);
assert.equal(result.status, 0);
assert.isOk(result.object);
return result.object;
}
function createState(keyboardName) {
const keyboard = loadKeyboard(keyboardName);
const state = km_core.state_create(keyboard, []);
assert.equal(state.status, 0);
assert.isOk(state.object);
return state.object;
}
it('can dispose keyboard', function () {
// Setup
const keyboard = loadKeyboard('k_020___deadkeys_and_backspace');
// Execute
km_core.keyboard_dispose(keyboard);
});
it('can get keyboard attributes', function () {
// Setup
const keyboard = loadKeyboard('k_022___options_with_preset');
// Execute
const result = km_core.keyboard_get_attrs(keyboard);
// Verify
assert.equal(result.status, 0);
assert.isOk(result.object);
assert.equal(result.object.id, 'k_022___options_with_preset');
assert.equal(result.object.version_string, '0.0');
assert.deepEqual(result.object.default_options, [{ key: 'foo', value: '0', scope: 1 }]);
});
it('can create the state object', function () {
// Setup
const keyboard = loadKeyboard('k_020___deadkeys_and_backspace');
// Execute
const result = km_core.state_create(keyboard, []);
// Verify
assert.equal(result.status, 0);
assert.isOk(result.object);
});
it('can dispose the state object', function () {
// Setup
const keyboard = loadKeyboard('k_020___deadkeys_and_backspace');
const state = km_core.state_create(keyboard, []);
// Execute
km_core.state_dispose(state.object);
});
it('can clone the state object', function () {
// Setup
const keyboard = loadKeyboard('k_020___deadkeys_and_backspace');
const state = km_core.state_create(keyboard, []);
// Execute
const result = km_core.state_clone(state.object);
// Verify
assert.equal(result.status, 0);
assert.isOk(result.object);
// TODO-web-core: add deeper tests
});
it('can process event', function () {
// Setup
const state = createState('k_020___deadkeys_and_backspace');
// Execute
const status = km_core.process_event(state, 0x20, 0, 1, 0);
// Verify
assert.equal(status, 0);
const actions = km_core.state_get_actions(state);
assert.equal(actions.output, ' ');
assert.lengthOf(actions.persist_options, 0);
assert.equal(actions.do_alert, false);
assert.equal(actions.emit_keystroke, false);
assert.equal(actions.new_caps_lock_state, -1);
assert.equal(actions.deleted_context, '');
});
it('can set the context', function () {
// Setup
const state = createState('k_020___deadkeys_and_backspace');
// Execute
const status = km_core.state_context_set_if_needed(state, 'abc');
// Verify
assert.equal(status, 1); // KM_CORE_CONTEXT_STATUS_UPDATED
const context = km_core.state_context_debug(state, 0);
assert.equal(context, '|abc| (len: 3) [ U+0061 U+0062 U+0063 ]');
});
});

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

@ -346,7 +346,7 @@ LPKMX_KEYBOARD CopyKeyboard(PKMX_BYTE bufp, PKMX_BYTE base) {
* @return pointer to the keyboard
*/
LPKMX_KEYBOARD KMX_FixupKeyboard(PKMX_BYTE bufp, PKMX_BYTE base, KMX_DWORD dwFileSize) {
UNREFERENCED_PARAMETER(dwFileSize);
// UNREFERENCED_PARAMETER(dwFileSize);
KMX_DWORD i, j;
PCOMP_KEYBOARD ckbp = (PCOMP_KEYBOARD)base;
@ -369,9 +369,9 @@ LPKMX_KEYBOARD KMX_FixupKeyboard(PKMX_BYTE bufp, PKMX_BYTE base, KMX_DWORD dwFil
for (gp = kbp->dpGroupArray, cgp = (PCOMP_GROUP)gp, i = 0; i < kbp->cxGroupArray; i++, gp++, cgp++) {
gp->dpName = KMX_StringOffset(base, cgp->dpName);
gp->dpKeyArray = (LPKMX_KEY)(base + cgp->dpKeyArray);
if (cgp->dpMatch != NULL)
if (cgp->dpMatch != 0)
gp->dpMatch = (PKMX_WCHAR)(base + cgp->dpMatch);
if (cgp->dpNoMatch != NULL)
if (cgp->dpNoMatch != 0)
gp->dpNoMatch = (PKMX_WCHAR)(base + cgp->dpNoMatch);
for (kp = gp->dpKeyArray, ckp = (PCOMP_KEY)kp, j = 0; j < gp->cxKeyArray; j++, kp++, ckp++) {

1
package-lock.json generated
View file

@ -14197,6 +14197,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",
"tsx": "^4.19.0"
}

View file

@ -14,7 +14,7 @@ set -eu
# Prevents 'clear' on exit of mingw64 bash shell
SHLVL=0
function _builder_full_not_a_builder_script() {
function __builder_full_not_a_builder_script() {
if ! _builder_has_function_been_called builder_describe; then
builder_echo warning "builder_describe was never called; script is not a valid builder script"
exit 1
@ -25,7 +25,7 @@ function _builder_full_not_a_builder_script() {
fi
}
trap _builder_full_not_a_builder_script exit
trap __builder_full_not_a_builder_script exit
# This will also import /resources/builder.inc.sh
. "${BASH_SOURCE[0]%/*}/builder-basic.inc.sh"

View file

@ -28,7 +28,7 @@ KEYMAN_MIN_TARGET_VERSION_WEB_SAFARI=13.0 # iOS 13.0, macOS 10.13.6+
# Dependency minimum 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/common/web-utils Low-level, headless utility methods and classes used across multiple modules" \
":engine/dom-utils A common subset of function used for DOM calculations, layout, etc" \
":engine/events Specialized classes utilized to support KMW API events" \
@ -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" \
@ -122,6 +125,9 @@ build_action() {
tsc --project "${KEYMAN_ROOT}/web/src/test/auto/tsconfig.json"
mkdir -p "${KEYMAN_ROOT}/web/build/test/dom/cases/core-processor/import/core/"
cp "${KEYMAN_ROOT}/web/src/engine/core-processor/src/import/core/keymancore.d.ts" "${KEYMAN_ROOT}/web/build/test/dom/cases/core-processor/import/core/"
for dir in \
"${KEYMAN_ROOT}/web/build/test/dom/cases"/*/ \
"${KEYMAN_ROOT}/web/build/test/integrated/" \
@ -180,6 +186,9 @@ builder_run_child_actions build:engine/keyboard-storage
builder_run_child_actions build:engine/predictive-text
# Uses engine/interfaces, engine/keyboard-storage, engine/predictive-text, & engine/osk
builder_run_child_actions build:engine/core-processor
# Uses engine/interfaces, engine/keyboard-storage, & engine/osk
builder_run_child_actions build:engine/main
# Uses all but engine/element-wrappers and engine/attachment
@ -214,3 +223,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

@ -65,8 +65,8 @@ A cache of recent context-state transitions is stored at `keyman.core.contextCac
For JS-keyboard keystroke processing, a `Mock` clone of the context is generated before any actual keyboard rule checks are applied. This provides a clear "before" state (eventually saved at `Transcription.preInput`) useful for determining the scope of the keystroke's changes once processing is completed via `buildTranscriptionFrom`.
Once keystroke processing is completed by a JS-keyboard, the JS-processor constructs a `RuleBehavior` object describing all primary and side effects of the keystroke. Defined at [web/src/engine/js-processor/src/ruleBehavior.ts](https://github.com/keymanapp/keyman/blob/master/web/src/engine/js-processor/src/ruleBehavior.ts), all of its fields aside from `transcription` are specific to JS-keyboard side effects, some of which do need special handling and support outside of the keystroke processor. None of these side effects apply for common-case keystrokes and so have default handling in place within the engine for cases where they are not needed.
Once keystroke processing is completed by a JS-keyboard, the JS-processor constructs a `ProcessorAction` object describing all primary and side effects of the keystroke. Defined at [web/src/engine/keyboard/src/keyboards/processorAction.ts](https://github.com/keymanapp/keyman/blob/master/web/src/engine/keyboard/src/keyboards/processorAction.ts), all of its fields aside from `transcription` are specific to JS-keyboard side effects, some of which do need special handling and support outside of the keystroke processor. None of these side effects apply for common-case keystrokes and so have default handling in place within the engine for cases where they are not needed.
---
In case referenced classes/files have moved: this doc was last updated in 18.0-beta, based upon commit d1e45a0df49f81597887577a8c83af36d4a85283
In case referenced classes/files have moved: this doc was last updated in 19.0-alpha, based upon PR #14001.

View file

@ -212,13 +212,13 @@ host cleanly, as it may have special implications for the site or host app.
With that out of the way, the JS keyboard's main group is then provided with the
`KeyEvent` object in order to evaluate the keyboard's rules against the
keystroke. This involves constructing a `RuleBehavior` object to track any and
keystroke. This involves constructing a `ProcessorAction` object to track any and
all needed side-effects for the keystroke. Such changes are generally not
applied to the engine at large at this time in order to prevent unwanted side
effects that may arise to and from predictive text operations that follow
shortly afterward.
The `KeyboardInterface` class defines the internal API used by keyboards for
The `JSKeyboardInterface` class defines the internal API used by keyboards for
their evaluation of keyboard rules.
Once the keyboard's script returns control to the main engine, the engine then
@ -233,7 +233,7 @@ keyboard rules that is applied. (A keyboard's source must _explicitly_ specify
to not emit anything for such keys if such an outcome is desired.) This occurs
regardless of the keystroke's source, as such effects are necessary to
incorporate for predictive-text operations. If no such rule exists, the
processor will add a note to the `RuleBehavior` object that default processing
processor will add a note to the `ProcessorAction` object that default processing
could not be provided and is thus needed from the engine's host.
### KMX keyboards & KMX+ (LDML) keyboards
@ -263,12 +263,12 @@ detected.
### Keystroke post-processing
At this stage, the side-effects desired for the original keystroke, as saved to
its `RuleBehavior`, are then applied. Note that this includes changes to the
its `ProcessorAction`, are then applied. Note that this includes changes to the
`&layer` store - this update will also trigger an event that updates the state
of the OSK to match the desired layer.
Once that is done, the engine will then save data relevant to the original
context state, the `KeyEvent`, and the `RuleBehavior` to the "context cache"
context state, the `KeyEvent`, and the `ProcessorAction` to the "context cache"
supporting multitap context reversion. These effects include layer-change
effects that result from JS keyboard rules, which modify the KMN-style keyboard
stores `&oldlayer` and `&newlayer` in preparation for `begin PostKeystroke`
@ -298,4 +298,4 @@ whether or not native handling for the keystroke should be triggered - such as
for TAB and ENTER keys, which may have semantic command-like functionality
depending on the state of the Web engine's host - be that a web page or an
Android/iOS app utilizing the keyboard. ENTER keys are often used to submit
forms, and this helps ensure we trigger such functionality when appropriate.
forms, and this helps ensure we trigger such functionality when appropriate.

View file

@ -19,9 +19,9 @@ When `true`, the keystroke processor will need to simulate the results of "stand
On-screen keyboard modules and hardware-keyboard event interception modules will produce instances of `KeyEvent` as noted above. Once this is completed, both module types will _signal_ the keystroke event by raising an event with ID `keyevent` and provide the `KeyEvent` instance as its first parameter. The main module of the engine will then forward that `KeyEvent` object to the input processing pipeline via `InputProcessor.processKeyEvent` (see below).
## Output: RuleBehavior
## Output: ProcessorAction
Once keystroke processing is completed, the outward-facing components of the engine expect to receive a `RuleBehavior` object describing all primary and side effects of the keystroke. Defined at [web/src/engine/js-processor/src/ruleBehavior.ts](https://github.com/keymanapp/keyman/blob/master/web/src/engine/js-processor/src/ruleBehavior.ts), all of its fields aside from `transcription` are specific to existing JS-keyboard side effects. (Certain Keyman language features can make permanent side-effect changes to state that should only be persisted for the "true" keystroke - not for any predictive-text 'alternative' keys.)
Once keystroke processing is completed, the outward-facing components of the engine expect to receive a `ProcessorAction` object describing all primary and side effects of the keystroke. Defined at [web/src/engine/keyboard/src/keyboards/processorAction.ts](https://github.com/keymanapp/keyman/blob/master/web/src/engine/keyboard/src/keyboards/processorAction.ts), all of its fields aside from `transcription` are specific to existing JS-keyboard side effects. (Certain Keyman language features can make permanent side-effect changes to state that should only be persisted for the "true" keystroke - not for any predictive-text 'alternative' keys.)
See also: [context-state-management.md](context-state-management.md#js-keyboard-keystroke-processing)
@ -31,7 +31,7 @@ See also: [context-state-management.md](context-state-management.md#js-keyboard
The "first stop" for incoming keystrokes is the `InputProcessor`, found at [web/src/engine/main/src/headless/inputProcessor.ts](https://github.com/keymanapp/keyman/blob/master/web/src/engine/main/src/headless/.inputProcessor.ts), through its `processKeyEvent` method. This class manages higher-level functionality triggered by keystroke events while deferring actual interpretation of the incoming keystroke further down the line. Of particular note is that it also handles control-flows that require restoration of previously-occuring contexts.
This class is the connection point for generating prediction requests and receiving corresponding suggestions. In order to facilitate higher-quality predictive-text when enabled, the `InputProcessor` will _also_ generate and trigger processing for nearby keys. This process allows transforms, reorders, and KMN keyboard rules to take effect and be used as alternative context roots for predictions. These are generally run against `Mock`-based clones of the true context source and are additionally prevented from triggering long-term side-effects, such as changes to KMN-keyboard variable stores, by only calling `RuleBehavior.finalize` for the true input keystroke's result object.
This class is the connection point for generating prediction requests and receiving corresponding suggestions. In order to facilitate higher-quality predictive-text when enabled, the `InputProcessor` will _also_ generate and trigger processing for nearby keys. This process allows transforms, reorders, and KMN keyboard rules to take effect and be used as alternative context roots for predictions. These are generally run against `Mock`-based clones of the true context source and are additionally prevented from triggering long-term side-effects, such as changes to KMN-keyboard variable stores, by only calling `KeyboardProcessor.finalizeProcessorAction` for the true input keystroke's result object.
Keys generated by OSK multitap need special handling here as well; they should always be applied to the context state as it existed at the time of the initial tap. To facilitate this, the `InputProcessor` will directly rewind the active context-source to match the corresponding context state before requesting that the `KeyEvent` be processed.
@ -64,18 +64,17 @@ The method linked above is the primary entrypoint for rule processing of individ
#### JS-keyboard interfacing
Certain Keyman language features can make permanent side-effect changes to state. In order to prevent these from taking place for every keystroke, the method that interfaces with JS keyboards - `KeyboardInterface.process` - saves the context state (as a `Mock`) and current variable store values, then prepares a fresh `RuleBehavior` instance, before passing control off to the keyboard's backing script. (Note that `KeyboardInterface` itself primarily consists of keyboard-script API called by JS-keyboard script.)
- `epic/web-core` note: renamed to `JSKeyboardInterface`.
Certain Keyman language features can make permanent side-effect changes to state. In order to prevent these from taking place for every keystroke, the method that interfaces with JS keyboards - `JSKeyboardInterface.process` - saves the context state (as a `Mock`) and current variable store values, then prepares a fresh `ProcessorAction` instance, before passing control off to the keyboard's backing script. (Note that `JSKeyboardInterface` itself primarily consists of keyboard-script API called by JS-keyboard script.)
A few of the keyboard-script API methods will mark `RuleBehavior` properties directly when called, but the bulk of its data will be set once the keyboard-script returns control to Keyman Engine for Web. At this time, variable store values will also be reverted to prevent possible cross-contamination effects when predictive text is active - they're reapplied later if `RuleBehavior.finalize` is leveraged on the resulting instance. Components documented in [context-state-management.md](./context-state-management.md) are then leveraged to determine the total change to context caused by the keystroke.
A few of the keyboard-script API methods will mark `ProcessorAction` properties directly when called, but the bulk of its data will be set once the keyboard-script returns control to Keyman Engine for Web. At this time, variable store values will also be reverted to prevent possible cross-contamination effects when predictive text is active - they're reapplied later if `KeyboardProcessor.finalizeProcessorAction` is leveraged on the resulting instance. Components documented in [context-state-management.md](./context-state-management.md) are then leveraged to determine the total change to context caused by the keystroke.
Note that for JS-keyboards, in 18.0 and before the true keystroke is processed against the _true_ context state, not a cloned copy, and thus its changes are applied immediately.
#### Keystroke-default emulation
Note that browser-default keystroke processing is defined within the `DefaultRules` class found at [web/src/engine/keyboard/src/defaultRules.ts](https://github.com/keymanapp/keyman/blob/master/web/src/engine/keyboard/src/defaultRules.ts). It currently produces a (possibly `null`) string output rather than a `RuleBehavior`, but the latter can be easily constructed based on the returned string and the existing context state.
Note that browser-default keystroke processing is defined within the `DefaultRules` class found at [web/src/engine/keyboard/src/defaultRules.ts](https://github.com/keymanapp/keyman/blob/master/web/src/engine/keyboard/src/defaultRules.ts). It currently produces a (possibly `null`) string output rather than a `ProcessorAction`, but the latter can be easily constructed based on the returned string and the existing context state.
- For `epic/web-core`, it may be wise to spin the `DefaultRules` component off as its own processor, serving as a backup for _all_ keystroke processing variants.
----
In case referenced classes/files have moved: this doc was last updated in 18.0-beta, based upon commit d1e45a0df49f81597887577a8c83af36d4a85283
In case referenced classes/files have moved: this doc was last updated in 19.0-alpha, based upon PR #14001.

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

@ -1,5 +1,6 @@
import { type KeyboardInterface } from 'keyman/engine/js-processor';
import { DesignIFrame, OutputTarget } from 'keyman/engine/element-wrappers';
import { type JSKeyboardInterface } from 'keyman/engine/js-processor';
import { JSKeyboard, type KeyboardMinimalInterface } from 'keyman/engine/keyboard';
import { DesignIFrame, OutputTargetElementWrapper } from 'keyman/engine/element-wrappers';
// Utility object used to handle beep (keyboard error response) operations.
class BeepData {
@ -17,9 +18,9 @@ class BeepData {
}
export class BeepHandler {
readonly keyboardInterface: KeyboardInterface;
readonly keyboardInterface: KeyboardMinimalInterface;
constructor(keyboardInterface: KeyboardInterface) {
constructor(keyboardInterface: KeyboardMinimalInterface) {
this.keyboardInterface = keyboardInterface;
}
@ -32,8 +33,8 @@ export class BeepHandler {
* @param {Object} Pelem element to flash
* Description Flash body as substitute for audible beep; notify embedded device to vibrate
*/
beep(outputTarget: OutputTarget<any>) {
if(!(outputTarget instanceof OutputTarget)) {
beep(outputTarget: OutputTargetElementWrapper<any>) {
if (!(outputTarget instanceof OutputTargetElementWrapper)) {
return;
}
@ -75,11 +76,13 @@ export class BeepHandler {
* Description Reset/terminate beep or flash (not currently used: Aug 2011)
*/
readonly reset = () => {
this.keyboardInterface.resetContextCache();
// TODO-web-core: implement for KMX keyboards if needed
if (this.keyboardInterface.activeKeyboard instanceof JSKeyboard) {
(this.keyboardInterface as JSKeyboardInterface).resetContextCache();
}
let Lbo;
this._BeepTimeout = 0;
for(Lbo=0;Lbo<this._BeepObjects.length;Lbo++) { // I1511 - array prototype extended
for(let Lbo=0;Lbo<this._BeepObjects.length;Lbo++) { // I1511 - array prototype extended
this._BeepObjects[Lbo].reset();
}
this._BeepObjects = [];

View file

@ -1,7 +1,8 @@
import { EngineConfiguration, InitOptionSpec, InitOptionDefaults } from "keyman/engine/main";
import { OutputTarget as DOMOutputTarget } from 'keyman/engine/element-wrappers';
import { isEmptyTransform, OutputTarget, RuleBehavior } from 'keyman/engine/js-processor';
import { OutputTargetElementWrapper as DOMOutputTarget } from 'keyman/engine/element-wrappers';
import { OutputTargetInterface, ProcessorAction } from 'keyman/engine/keyboard';
import { isEmptyTransform } from 'keyman/engine/js-processor';
import { AlertHost } from "./utils/alertHost.js";
import { whenDocumentReady } from "./utils/documentReady.js";
@ -65,7 +66,7 @@ export class BrowserConfiguration extends EngineConfiguration {
return baseReport;
}
onRuleFinalization(ruleBehavior: RuleBehavior, outputTarget: OutputTarget) {
onRuleFinalization(ruleBehavior: ProcessorAction, outputTarget: OutputTargetInterface) {
// TODO: Patch up to modularized form. But that doesn't exist yet for some of these...
// If the transform isn't empty, we've changed text - which should produce a 'changed' event in the DOM.

View file

@ -1,12 +1,12 @@
import { type Keyboard, KeyboardScriptError } from 'keyman/engine/keyboard';
import { JSKeyboard, type Keyboard, KeyboardScriptError } from 'keyman/engine/keyboard';
import { type KeyboardStub } from 'keyman/engine/keyboard-storage';
import { CookieSerializer } from 'keyman/engine/dom-utils';
import { eventOutputTarget, outputTargetForElement, PageContextAttachment } from 'keyman/engine/attachment';
import { DomEventTracker, LegacyEventEmitter } from 'keyman/engine/events';
import { DesignIFrame, OutputTarget, nestedInstanceOf } from 'keyman/engine/element-wrappers';
import { DesignIFrame, OutputTargetElementWrapper, nestedInstanceOf } from 'keyman/engine/element-wrappers';
import {
ContextManagerBase,
type KeyboardInterface,
type KeyboardInterfaceBase,
LegacyAPIEvents
} from 'keyman/engine/main';
import { BrowserConfiguration } from './configuration.js';
@ -23,10 +23,12 @@ export interface KeyboardCookie {
* has the same directionality, text runs will be re-ordered which is confusing and causes
* incorrect caret positioning
*
* @param {Object} Ptarg Target element
* @param {Object} Ptarg Target element
* @param {Keyboard} activeKeyboard The active keyboard
*/
function _SetTargDir(Ptarg: HTMLElement, activeKeyboard: Keyboard) {
const elDir = activeKeyboard?.isRTL ? 'rtl' : 'ltr';
// TODO-web-core: do we need to support RTL in Core?
const elDir = activeKeyboard instanceof JSKeyboard && activeKeyboard?.isRTL ? 'rtl' : 'ltr';
if(Ptarg) {
if(Ptarg instanceof Ptarg.ownerDocument.defaultView.HTMLInputElement
@ -41,14 +43,14 @@ function _SetTargDir(Ptarg: HTMLElement, activeKeyboard: Keyboard) {
}
export default class ContextManager extends ContextManagerBase<BrowserConfiguration> {
private _activeKeyboard: {keyboard: Keyboard, metadata: KeyboardStub};
private _activeKeyboard: {keyboard: JSKeyboard, metadata: KeyboardStub};
private cookieManager = new CookieSerializer<KeyboardCookie>('KeymanWeb_Keyboard');
readonly focusAssistant = new FocusAssistant(() => this.activeTarget?.isForcingScroll());
readonly page: PageContextAttachment;
private mostRecentTarget: OutputTarget<any>;
private currentTarget: OutputTarget<any>;
private mostRecentTarget: OutputTargetElementWrapper<any>;
private currentTarget: OutputTargetElementWrapper<any>;
private globalKeyboard: {keyboard: Keyboard, metadata: KeyboardStub};
private globalKeyboard: {keyboard: JSKeyboard, metadata: KeyboardStub};
private _eventsObj: () => LegacyEventEmitter<LegacyAPIEvents>;
private domEventTracker = new DomEventTracker();
@ -173,7 +175,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
});
}
get activeTarget(): OutputTarget<any> {
get activeTarget(): OutputTargetElementWrapper<any> {
/*
* Assumption: the maintainingFocus flag may only be set when there is a current target.
* This is not enforced proactively at present, but the assumption should hold. (2023-05-03)
@ -182,7 +184,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
return this.currentTarget || (maintainingFocus ? this.mostRecentTarget : null);
}
get lastActiveTarget(): OutputTarget<any> {
get lastActiveTarget(): OutputTargetElementWrapper<any> {
return this.mostRecentTarget;
}
@ -227,7 +229,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
}
}
public setActiveTarget(target: OutputTarget<any>, sendEvents?: boolean) {
public setActiveTarget(target: OutputTargetElementWrapper<any>, sendEvents?: boolean) {
const previousTarget = this.mostRecentTarget;
const originalTarget = this.activeTarget; // may differ, depending on focus state.
@ -342,7 +344,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
this.focusAssistant.restoringFocus = false;
}
insertText(kbdInterface: KeyboardInterface<ContextManager>, Ptext: string, PdeadKey: number) {
insertText(kbdInterface: KeyboardInterfaceBase<ContextManager>, Ptext: string, PdeadKey: number) {
// Find the correct output target to manipulate. The user has likely be interacting with a
// 'help page' keyboard, like desktop `sil_euro_latin`, and active browser focus on the
// original context element may have been lost.
@ -367,7 +369,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
*
* This is based on the current `.activeTarget` and its related attachment metadata.
*/
protected currentKeyboardSrcTarget(): OutputTarget<any> {
protected currentKeyboardSrcTarget(): OutputTargetElementWrapper<any> {
const target = this.currentTarget || this.mostRecentTarget;
if(this.isTargetKeyboardIndependent(target)) {
@ -377,7 +379,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
}
}
private isTargetKeyboardIndependent(target: OutputTarget<any>): boolean {
private isTargetKeyboardIndependent(target: OutputTargetElementWrapper<any>): boolean {
const attachmentInfo = target?.getElement()._kmwAttachment;
// If null or undefined, we're in 'global' mode.
@ -385,7 +387,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
}
// Note: is part of the keyboard activation process. Not to be called directly by published API.
activateKeyboardForTarget(kbd: {keyboard: Keyboard, metadata: KeyboardStub}, target: OutputTarget<any>) {
activateKeyboardForTarget(kbd: { keyboard: JSKeyboard, metadata: KeyboardStub }, target: OutputTargetElementWrapper<any>) {
const attachment = target?.getElement()._kmwAttachment;
if(!attachment) {
@ -419,7 +421,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
* @param target
* @param metadata
*/
public setKeyboardForTarget(target: OutputTarget<any>, kbdId: string, langId: string) {
public setKeyboardForTarget(target: OutputTargetElementWrapper<any>, kbdId: string, langId: string) {
if(target instanceof DesignIFrame) {
console.warn("'keymanweb.setKeyboardForControl' cannot set keyboard on iframes.");
return;
@ -454,7 +456,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
}
}
public getKeyboardStubForTarget(target: OutputTarget<any>) {
public getKeyboardStubForTarget(target: OutputTargetElementWrapper<any>) {
if(!this.isTargetKeyboardIndependent(target)) {
return this.globalKeyboard.metadata;
} else {
@ -612,7 +614,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
* The return value indicates whether (true) or not (false) the calling event handler
* should be terminated immediately after the call.
*/
_CommonFocusHelper(outputTarget: OutputTarget<any>): boolean {
_CommonFocusHelper(outputTarget: OutputTargetElementWrapper<any>): boolean {
const focusAssistant = this.focusAssistant;
const activeKeyboard = this.activeKeyboard?.keyboard;
@ -736,7 +738,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
return true;
}
doChangeEvent(target: OutputTarget<any>) {
doChangeEvent(target: OutputTargetElementWrapper<any>) {
if(target.changed) {
const event = new Event('change', {"bubbles": true, "cancelable": false});
target.getElement().dispatchEvent(event);

View file

@ -2,9 +2,9 @@ import { ModifierKeyConstants } from '@keymanapp/common-types';
import {
Codes,
DefaultRules,
type KeyEvent
type KeyEvent,
type OutputTargetInterface
} from 'keyman/engine/keyboard';
import { type OutputTarget } from 'keyman/engine/js-processor';
import ContextManager from './contextManager.js';
@ -30,9 +30,9 @@ export default class DefaultBrowserRules extends DefaultRules {
}
/**
* applyCommand - used when a RuleBehavior represents a non-text "command" within the Engine.
* applyCommand - used when a ProcessorAction represents a non-text "command" within the Engine.
*/
applyCommand(Lkc: KeyEvent, outputTarget: OutputTarget): void {
applyCommand(Lkc: KeyEvent, outputTarget: OutputTargetInterface): void {
const code = this.codeForEvent(Lkc);
const moveToNext = (back: boolean) => {

View file

@ -1,5 +1,5 @@
import { Codes, DeviceSpec, KeyEvent, KeyMapping, Keyboard } from 'keyman/engine/keyboard';
import { KeyboardProcessor } from 'keyman/engine/js-processor';
import { Codes, DeviceSpec, KeyEvent, KeyMapping, JSKeyboard } from 'keyman/engine/keyboard';
import { JSKeyboardProcessor } from 'keyman/engine/js-processor';
import { ModifierKeyConstants } from '@keymanapp/common-types';
import { HardKeyboardBase, processForMnemonicsAndLegacy } from 'keyman/engine/main';
@ -10,7 +10,7 @@ import { eventOutputTarget, outputTargetForElement } from 'keyman/engine/attachm
import ContextManager from './contextManager.js';
type KeyboardState = {
activeKeyboard: Keyboard,
activeKeyboard: JSKeyboard,
modStateFlags: number,
baseLayout: string
}
@ -38,15 +38,23 @@ export function _GetEventKeyCode(e: KeyboardEvent) {
// Keeping this as a separate function affords us the opportunity to unit-test the method more simply.
/**
* Function _GetKeyEventProperties
* Scope Private
* @param {Event} e Event object
* @return {Object.<string,*>} KMW keyboard event object:
* Description Get object with target element, key code, shift state, virtual key state
* Lcode=keyCode
* Lmodifiers=shiftState
* LisVirtualKeyCode e.g. ctrl/alt key
* LisVirtualKey e.g. Virtual key or non-keypress event
* Translate a browser KeyboardEvent into a KeymanWeb KeyEvent
*
* The function calculates the true state of the keyboard's modifiers, taking
* into account chiral modifiers where applicable.
* Special handling is included for AltGr emulation and browser quirks, particularly
* for Firefox, which may use different key codes for certain keys. The code remaps
* these as needed for consistency. It adds the OS meta key if pressed, ensuring
* that system shortcuts can bypass Keyman processing. Adjustments are made for
* mnemonic and legacy keyboards, before finally returning a KeyEvent object
* with the computed key code, modifiers and state.
*
* @param {KeyboardEvent} e Event object
* @param {KeyboardState} keyboardState Keyboard state object
* @param {DeviceSpec} device Device object
* @return {KeyEvent} KeymanWeb KeyEvent object, or null
* for duplicate/spurious events or if
* there is no key code.
*/
export function preprocessKeyboardEvent(e: KeyboardEvent, keyboardState: KeyboardState, device: DeviceSpec): KeyEvent {
if(e.cancelBubble === true) {
@ -60,7 +68,6 @@ export function preprocessKeyboardEvent(e: KeyboardEvent, keyboardState: Keyboar
// Stage 1 - track the true state of the keyboard's modifiers.
const prevModState = keyboardState.modStateFlags;
let curModState = 0x0000;
let ctrlEvent = false, altEvent = false;
const keyCodes = Codes.keyCodes;
@ -97,17 +104,18 @@ export function preprocessKeyboardEvent(e: KeyboardEvent, keyboardState: Keyboar
* `e.location != 0` if true matches condition 1 and matches condition 2 if false.
*/
let curModState = 0x0000;
curModState |= (e.getModifierState("Shift") ? 0x10 : 0);
if(e.getModifierState("Control")) {
curModState |= ((e.location != 0 && ctrlEvent) ?
(e.location == 1 ? ModifierKeyConstants.LCTRLFLAG : ModifierKeyConstants.RCTRLFLAG) : // Condition 1
prevModState & 0x0003); // Condition 2
prevModState & 0x0003 /* LCTRLFLAG | RCTRLFLAG */); // Condition 2
}
if(e.getModifierState("Alt")) {
curModState |= ((e.location != 0 && altEvent) ?
(e.location == 1 ? ModifierKeyConstants.LALTFLAG : ModifierKeyConstants.RALTFLAG) : // Condition 1
prevModState & 0x000C); // Condition 2
prevModState & 0x000C /* LALTFLAG | RALTFLAG */); // Condition 2
}
// Stage 2 - detect state key information. It can be looked up per keypress with no issue.
@ -122,7 +130,7 @@ export function preprocessKeyboardEvent(e: KeyboardEvent, keyboardState: Keyboar
curModState |= Lstates;
// Stage 3 - Set our modifier state tracking variable and perform basic AltGr-related management.
const LmodifierChange = keyboardState.modStateFlags != curModState;
const LmodifierChanged = keyboardState.modStateFlags != curModState;
// KeyboardState update: save our known modifier/state analysis bits.
// Note: `keyboardState` is typically the full-fledged KeyboardProcessor instance. As a result,
@ -140,8 +148,8 @@ export function preprocessKeyboardEvent(e: KeyboardEvent, keyboardState: Keyboar
curModState &= ~ModifierKeyConstants.LCTRLFLAG;
}
const modifierBitmasks = Codes.modifierBitmasks;
// Stage 4 - map the modifier set to the appropriate keystroke's modifiers.
const modifierBitmasks = Codes.modifierBitmasks;
const activeKeyboard = keyboardState.activeKeyboard;
let Lmodifiers: number;
if(activeKeyboard && activeKeyboard.isChiral) {
@ -187,7 +195,7 @@ export function preprocessKeyboardEvent(e: KeyboardEvent, keyboardState: Keyboar
Lcode: Lcode,
Lmodifiers: Lmodifiers,
Lstates: Lstates,
LmodifierChange: LmodifierChange,
LmodifierChange: LmodifierChanged,
// This is based on a KeyboardEvent, so it's not considered 'synthetic' within web-core.
isSynthetic: false
});
@ -210,13 +218,13 @@ export default class HardwareEventKeyboard extends HardKeyboardBase {
// - `modStateFlags`
// - `baseLayout`
// - `doModifierPress()` - for modifier updates on key-up.
private readonly processor: KeyboardProcessor;
private readonly processor: JSKeyboardProcessor;
private readonly contextManager: ContextManager;
private domEventTracker = new DomEventTracker();
private swallowKeypress: boolean = false;
constructor(hardDevice: DeviceSpec, processor: KeyboardProcessor, contextManager: ContextManager) {
constructor(hardDevice: DeviceSpec, processor: JSKeyboardProcessor, contextManager: ContextManager) {
super();
this.hardDevice = hardDevice;
this.contextManager = contextManager;

View file

@ -1,11 +1,11 @@
import { type OutputTarget } from 'keyman/engine/element-wrappers';
import { type OutputTargetElementWrapper } from 'keyman/engine/element-wrappers';
import { FloatingOSKView } from 'keyman/engine/osk';
import { KeyboardInterface as KeyboardInterfaceBase } from 'keyman/engine/main';
import { KeyboardInterfaceBase } from 'keyman/engine/main';
import ContextManager from './contextManager.js';
import { KeymanEngine } from './keymanEngine.js';
export default class KeyboardInterface extends KeyboardInterfaceBase<ContextManager> {
export class KeyboardInterface extends KeyboardInterfaceBase<ContextManager> {
constructor(
_jsGlobal: any,
engine: KeymanEngine,
@ -29,7 +29,7 @@ export default class KeyboardInterface extends KeyboardInterfaceBase<ContextMana
/**
* Legacy entry points (non-standard names)- included only to allow existing IME keyboards to continue to be used
*/
getLastActiveElement(): OutputTarget<any> {
getLastActiveElement(): OutputTargetElementWrapper<any> {
return this.engine.contextManager.lastActiveTarget;
}

View file

@ -1,13 +1,13 @@
import { KeymanWebKeyboard } from '@keymanapp/common-types';
import { KeymanEngineBase, DeviceDetector } from 'keyman/engine/main';
import { getAbsoluteY } from 'keyman/engine/dom-utils';
import { OutputTarget } from 'keyman/engine/element-wrappers';
import { OutputTargetElementWrapper } from 'keyman/engine/element-wrappers';
import {
TwoStateActivator,
VisualKeyboard
} from 'keyman/engine/osk';
import { ErrorStub, KeyboardStub, CloudQueryResult, toPrefixedKeyboardId as prefixed } from 'keyman/engine/keyboard-storage';
import { DeviceSpec, Keyboard } from "keyman/engine/keyboard";
import { DeviceSpec, JSKeyboard, Keyboard } from "keyman/engine/keyboard";
import KeyboardObject = KeymanWebKeyboard.KeyboardObject;
import * as views from './viewsAnchorpoint.js';
@ -26,7 +26,7 @@ import { UtilApiEndpoint} from './utilApiEndpoint.js';
import { UIModule } from './uiModuleInterface.js';
import { HotkeyManager } from './hotkeyManager.js';
import { BeepHandler } from './beepHandler.js';
import KeyboardInterface from './keyboardInterface.js';
import { KeyboardInterface } from './keyboardInterface.js';
import { WorkerFactory } from '@keymanapp/lexical-model-layer/web';
export class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, ContextManager, HardwareEventKeyboard> {
@ -76,7 +76,7 @@ export class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, Context
// Scrolls the document-body to ensure that a focused element remains visible after the OSK appears.
this.contextManager.on('targetchange', (target) => {
const e = (target as OutputTarget<any>)?.getElement();
const e = (target as OutputTargetElementWrapper<any>)?.getElement();
if(this.osk) {
(this.osk.activationModel as TwoStateActivator<HTMLElement>).activationTrigger = e;
}
@ -192,11 +192,11 @@ export class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, Context
// globe key - are accessible.
//
// The `super` call above initializes `keyboardRequisitioner`, as needed here.
this.keyboardRequisitioner.cloudQueryEngine.once('unboundregister', () => {
this.keyboardRequisitioner.cloudQueryEngine.once('unboundregister', async () => {
if(!this.contextManager.activeKeyboard?.keyboard) {
// Autoselects this.keyboardRequisitioner.cache.defaultStub, which will be
// set to an actual keyboard on mobile devices.
this.setActiveKeyboard('', '');
await this.setActiveKeyboard('', '');
}
});
@ -277,7 +277,7 @@ export class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, Context
* Function setKeyboardForControl
* Scope Public
* @param {Element} Pelem Control element
* @param {string|null=} Pkbd Keyboard (Clears the set keyboard if set to null.)
* @param {string|null=} Pkbd JSKeyboard (Clears the set keyboard if set to null.)
* @param {string|null=} Plc Language Code
* Description Set default keyboard for the control
*
@ -391,12 +391,12 @@ export class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, Context
* This is a public API function documented at
* https://help.keyman.com/developer/engine/web/current-version/reference/core/getKeyboard.
*
* @param {Object} Lstub Keyboard stub object
* @param {Object} Lkbd Keyboard script object
* @param {Object} Lstub JSKeyboard stub object
* @param {Object} Lkbd JSKeyboard script object
* @return {Object} Copy of keyboard identification strings
*
*/
private _GetKeyboardDetail = function(Lstub: KeyboardStub, Lkbd: Keyboard) { // I2078 - Full keyboard detail
private _GetKeyboardDetail = function(Lstub: KeyboardStub, Lkbd: JSKeyboard) { // I2078 - Full keyboard detail
const Lr = {
Name: Lstub.KN,
InternalName: Lstub.KI,
@ -437,13 +437,14 @@ export class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, Context
if(kbdDetail.KeyboardID){
kbd = this.keyboardRequisitioner.cache.getKeyboard(kbdDetail.KeyboardID);
} else {
kbd = new Keyboard(k0);
kbd = new JSKeyboard(k0);
}
} else {
kbd = this.core.activeKeyboard;
}
return kbd && kbd.isCJK;
// TODO-web-core: implement for KMX keyboards if needed
return kbd && kbd instanceof JSKeyboard && kbd.isCJK;
}
/**
@ -459,7 +460,12 @@ export class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, Context
const stub = this.keyboardRequisitioner.cache.getStub(PInternalName, PlgCode);
const keyboard = this.keyboardRequisitioner.cache.getKeyboardForStub(stub);
return stub && this._GetKeyboardDetail(stub, keyboard);
if (keyboard instanceof JSKeyboard) {
return stub && this._GetKeyboardDetail(stub, keyboard);
} else {
// TODO-web-core: implement for KMX keyboards if needed
return null;
}
}
/**
@ -484,8 +490,12 @@ export class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, Context
// In Chrome, (including on Android), Array.prototype.find() requires Chrome 45.
// This is a later version than the default on our oldest-supported Android devices.
const Lkbd = cache.getKeyboardForStub(Lstub);
const Lrn = this._GetKeyboardDetail(Lstub, Lkbd); // I2078 - Full keyboard detail
Lr.push(Lrn);
if (Lkbd instanceof JSKeyboard) {
const Lrn = this._GetKeyboardDetail(Lstub, Lkbd); // I2078 - Full keyboard detail
Lr.push(Lrn);
} else {
// TODO-web-core: implement for KMX keyboards if needed
}
}
return Lr;
}
@ -682,6 +692,10 @@ export class KeymanEngine extends KeymanEngineBase<BrowserConfiguration, Context
}
PKbd = PKbd || this.core.activeKeyboard;
if (!(PKbd instanceof JSKeyboard)) {
// TODO-web-core: implement for KMX keyboards if needed
return null;
}
const Pstub = this.keyboardRequisitioner.cache.getStub(PKbd);
// help.keyman.com will set this function in place to specify the desired

View file

@ -3,5 +3,5 @@ export { default as ContextManager, KeyboardCookie } from "./contextManager.js";
export { preprocessKeyboardEvent, default as HardwareEventKeyboard } from './hardwareEventKeyboard.js';
export { KeymanEngine } from './keymanEngine.js';
export { default as KeyboardInterface } from './keyboardInterface.js';
export { KeyboardInterface } from './keyboardInterface.js';
export { UIModule } from './uiModuleInterface.js';

View file

@ -64,6 +64,10 @@ compile_and_copy() {
mkdir -p "$KEYMAN_ROOT/web/build/app/resources/ui"
cp -R "$KEYMAN_ROOT/web/src/resources/ui/." "$KEYMAN_ROOT/web/build/app/resources/ui/"
# 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/ui/debug/"
cp "${KEYMAN_ROOT}/web/build/engine/core-processor/obj/import/core/"km-core.{js,wasm} "${KEYMAN_ROOT}/web/build/app/ui/release/"
# Update the build/publish copy of our build artifacts
prepare
}

View file

@ -450,7 +450,7 @@ if(!keymanweb) {
* @param {Object} e event
* Description Change active keyboard in response to user selection event
*/
private readonly SelectKeyboardChange = async (e: Event) => {
private readonly SelectKeyboardChange = async (e: Event): Promise<void> => {
keymanweb.activatingUI(true);
if(this.KeyboardSelector.value != '-') {

View file

@ -820,7 +820,7 @@ if(!keymanweb) {
* @param {boolean} updateKeyman
* @return {boolean}
**/
private async selectKeyboard(event: Event, lang: LanguageEntry, kbd: KeyboardDetail, updateKeyman: boolean) {
private async selectKeyboard(event: Event, lang: LanguageEntry, kbd: KeyboardDetail, updateKeyman: boolean): Promise<boolean> {
keymanweb.activatingUI(true);
if(this.selectedLanguage) {
@ -943,7 +943,7 @@ if(!keymanweb) {
* @return {boolean}
* Description Update the UI when all keyboards disabled by user
**/
private readonly offButtonClickEvent = async (event: Event) => {
private readonly offButtonClickEvent = async (event: Event): Promise<boolean> => {
if(this.toolbarNode.className != 'kmw_controls_disabled') {
this.hideKeyboardsForLanguage(null);
if(this.selectedLanguage) {

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

@ -1,5 +1,6 @@
import { type Keyboard } from 'keyman/engine/keyboard';
import { Mock, OutputTarget, Transcription, findCommonSubstringEndIndex, isEmptyTransform, TextTransform } from 'keyman/engine/js-processor';
import { JSKeyboard, Keyboard, OutputTargetInterface, Transcription, TextTransform } from 'keyman/engine/keyboard';
// TODO-web-core: remove usage of OutputTargetBase, use OutputTargetInterface instead
import { Mock, findCommonSubstringEndIndex, isEmptyTransform, OutputTargetBase } from 'keyman/engine/js-processor';
import { KeyboardStub } from 'keyman/engine/keyboard-storage';
import { ContextManagerBase } from 'keyman/engine/main';
import { WebviewConfiguration } from './configuration.js';
@ -30,7 +31,8 @@ export class ContextHost extends Mock {
let transform: TextTransform = null;
if(transcription) {
const preInput = transcription.preInput;
//TODO-web-core: shouldn't need cast in the future?
const preInput = transcription.preInput as Mock;
// If our saved state matches the `preInput` from the incoming transcription, just reuse its transform.
// Will generally not match during multitap operations, though.
//
@ -58,9 +60,10 @@ export class ContextHost extends Mock {
this.savedState = Mock.from(this);
}
restoreTo(original: OutputTarget): void {
restoreTo(original: OutputTargetInterface): void {
this.savedState = Mock.from(this);
super.restoreTo(original);
// TODO-web-core
super.restoreTo(original as OutputTargetBase);
}
updateContext(text: string, selStart: number, selEnd: number): boolean {
@ -117,7 +120,7 @@ export default class ContextManager extends ContextManagerBase<WebviewConfigurat
// yet to be modularized at this time, though.)
private _rawContext: ContextHost;
private _activeKeyboard: {keyboard: Keyboard, metadata: KeyboardStub};
private _activeKeyboard: {keyboard: JSKeyboard, metadata: KeyboardStub};
constructor(engineConfig: WebviewConfiguration) {
super(engineConfig);
@ -137,7 +140,7 @@ export default class ContextManager extends ContextManagerBase<WebviewConfigurat
return this._activeKeyboard;
}
activateKeyboardForTarget(kbd: {keyboard: Keyboard, metadata: KeyboardStub}, target: OutputTarget) {
activateKeyboardForTarget(kbd: { keyboard: JSKeyboard, metadata: KeyboardStub }, target: OutputTargetInterface) {
// `target` is irrelevant for `app/webview`, as it'll only ever use 'global' keyboard settings.
// Clone the object to prevent accidental by-reference changes.
@ -204,7 +207,10 @@ export default class ContextManager extends ContextManagerBase<WebviewConfigurat
// That said, it's best to keep it around for now and verify later.
if(originalKeyboard?.metadata?.id == activatingKeyboard?.metadata?.id) {
activatingKeyboard.keyboard = activatingKeyboard.keyboard.then((kbd) => {
kbd.refreshLayouts()
// TODO-web-core: Do we need to refresh layouts for KMX keyboards also?
if (kbd instanceof JSKeyboard) {
kbd.refreshLayouts();
}
return kbd;
});
}

View file

@ -1,6 +1,5 @@
import { DeviceSpec, DefaultRules } from 'keyman/engine/keyboard';
import { RuleBehavior } from 'keyman/engine/js-processor';
import { KeymanEngineBase, KeyboardInterface } from 'keyman/engine/main';
import { DeviceSpec, DefaultRules, ProcessorAction } from 'keyman/engine/keyboard';
import { KeymanEngineBase, KeyboardInterfaceBase } from 'keyman/engine/main';
import { AnchoredOSKView, ViewConfiguration, StaticActivator } from 'keyman/engine/osk';
import { getAbsoluteX, getAbsoluteY } from 'keyman/engine/dom-utils';
import { toPrefixedKeyboardId, toUnprefixedKeyboardId } from 'keyman/engine/keyboard-storage';
@ -18,7 +17,7 @@ export class KeymanEngine extends KeymanEngineBase<WebviewConfiguration, Context
constructor(workerFactory: WorkerFactory, sourceUri: string) {
const config = new WebviewConfiguration(sourceUri); // currently set to perform device auto-detect.
config.onRuleFinalization = (ruleBehavior: RuleBehavior) => {
config.onRuleFinalization = (ruleBehavior: ProcessorAction) => {
(this.context as ContextHost).updateHost(ruleBehavior.transcription);
}
@ -35,7 +34,7 @@ export class KeymanEngine extends KeymanEngineBase<WebviewConfiguration, Context
return {
// The `engine` parameter cannot be supplied with the constructing instance before calling
// `super`, hence the 'fun' rigging to supply it _from_ `super` via this closure.
keyboardInterface: new KeyboardInterface(window, engine, config.stubNamespacer),
keyboardInterface: new KeyboardInterfaceBase(window, engine, config.stubNamespacer),
defaultOutputRules: new DefaultRules()
};
});

View file

@ -1,10 +1,10 @@
import { OutputTarget } from 'keyman/engine/element-wrappers';
import { OutputTargetElementWrapper } from 'keyman/engine/element-wrappers';
export class AttachmentInfo {
/**
* Provides the core interface between the DOM and the actual keyboard.
*/
interface: OutputTarget<any>;
interface: OutputTargetElementWrapper<any>;
/**
* Tracks the control's independent keyboard selection, when applicable.
@ -21,7 +21,7 @@ export class AttachmentInfo {
*/
inputMode?: string;
constructor(eleInterface: OutputTarget<any>, kbd: string, touch?: boolean) {
constructor(eleInterface: OutputTargetElementWrapper<any>, kbd: string, touch?: boolean) {
this.interface = eleInterface;
this.keyboard = kbd;
}

View file

@ -2,7 +2,7 @@ import { nestedInstanceOf } from "keyman/engine/element-wrappers";
/**
* Given a DOM event related to an KMW-attached element, this function determines
* the corresponding OutputTarget.
* the corresponding OutputTargetInterface.
* @param e
* @returns
*/
@ -13,7 +13,7 @@ export function eventOutputTarget(e: Event) {
/**
* Given a DOM event related to an KMW-attached element, this function determines
* the corresponding OutputTarget.
* the corresponding OutputTargetInterface.
* @param e
* @returns
*/
@ -42,7 +42,7 @@ export function outputTargetForElement(Ltarg: HTMLElement) {
}
}
// Step 2: With the most likely host element determined, obtain the corresponding OutputTarget
// Step 2: With the most likely host element determined, obtain the corresponding OutputTargetInterface
// instance.
return Ltarg._kmwAttachment?.interface;
}

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-full.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
SUBPROJECT_NAME=engine/core-processor
. "$KEYMAN_ROOT/resources/build/utils.inc.sh"
. "$KEYMAN_ROOT/resources/build/node.inc.sh"
. "$KEYMAN_ROOT/web/common.inc.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/keymancore.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() {
node_select_version_and_npm_ci
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/keymancore.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/keymancore.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,43 @@
// Keyman is copyright (C) SIL International. MIT License.
import { type MainModule as KMXCoreModule } from './import/core/keymancore.js';
// Unfortunately embind has an open issue with enums and typescript where it
// only generates a type for the enum, but not the values in a usable way.
// So we have to re-define the enum here.
// See https://github.com/emscripten-core/emscripten/issues/18585
// NOTE: Keep in sync with core/include/keyman/keyman_core_api.h#L311
export enum KM_CORE_STATUS {
OK = 0,
NO_MEM = 1,
IO_ERROR = 2,
INVALID_ARGUMENT = 3,
KEY_ERROR = 4,
INSUFFICENT_BUFFER = 5,
INVALID_UTF = 6,
INVALID_KEYBOARD = 7,
NOT_IMPLEMENTED = 8,
OS_ERROR = 0x80000000
}
export class KM_Core {
private static km_core: KMXCoreModule = null;
static get instance(): KMXCoreModule {
return this.km_core;
}
public static async createCoreProcessor(baseurl: string): Promise<KMXCoreModule> {
const module = await import(baseurl + '/km-core.js');
const createCoreProcessor = module.default;
const km_core = createCoreProcessor({
locateFile: function (path: string, scriptDirectory: string) {
return baseurl + '/' + path;
}
});
km_core.then((core: KMXCoreModule) => {
this.km_core = core;
});
return km_core;
}
}

View file

@ -0,0 +1,3 @@
export { KM_Core, KM_CORE_STATUS } from './KM_Core.js';
import { type km_core_keyboard } from './import/core/keymancore.js';
export { km_core_keyboard };

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

@ -1,4 +1,4 @@
import OutputTarget from './outputTarget.js';
import { OutputTargetElementWrapper } from './outputTargetElementWrapper.js';
import { KMWString } from '@keymanapp/web-utils';
class SelectionCaret {
@ -21,7 +21,7 @@ class SelectionRange {
}
}
export default class ContentEditable extends OutputTarget<{}> {
export class ContentEditable extends OutputTargetElementWrapper<{}> {
root: HTMLElement;
constructor(ele: HTMLElement) {

View file

@ -1,4 +1,4 @@
import OutputTarget from './outputTarget.js';
import { OutputTargetElementWrapper } from './outputTargetElementWrapper.js';
import { KMWString } from '@keymanapp/web-utils';
class SelectionCaret {
@ -32,7 +32,7 @@ class StyleCommand {
}
}
export default class DesignIFrame extends OutputTarget<{}> {
export class DesignIFrame extends OutputTargetElementWrapper<{}> {
root: HTMLIFrameElement;
doc: Document;
docRoot: HTMLElement;

View file

@ -1,7 +1,7 @@
export { default as wrapElement } from './wrapElement.js';
export { default as ContentEditable } from './contentEditable.js';
export { default as DesignIFrame } from './designIFrame.js';
export { default as Input } from './input.js';
export { default as OutputTarget } from './outputTarget.js';
export { default as TextArea } from './textarea.js';
export { wrapElement } from './wrapElement.js';
export { ContentEditable } from './contentEditable.js';
export { DesignIFrame } from './designIFrame.js';
export { Input } from './input.js';
export { OutputTargetElementWrapper } from './outputTargetElementWrapper.js';
export { TextArea } from './textarea.js';
export { nestedInstanceOf } from './utils.js';

View file

@ -1,4 +1,4 @@
import OutputTarget from './outputTarget.js';
import { OutputTargetElementWrapper } from './outputTargetElementWrapper.js';
import { KMWString } from '@keymanapp/web-utils';
interface EventMap {
@ -22,7 +22,7 @@ interface EventMap {
'unhandlednewline': (element: HTMLInputElement) => void
}
export default class Input extends OutputTarget<EventMap> {
export class Input extends OutputTargetElementWrapper<EventMap> {
root: HTMLInputElement;
/**

View file

@ -1,7 +1,7 @@
import { OutputTarget as OutputTargetBase } from "keyman/engine/js-processor";
import { OutputTargetBase } from "keyman/engine/js-processor";
import { EventEmitter } from 'eventemitter3';
export default abstract class OutputTarget<EventMap extends EventEmitter.ValidEventTypes> extends OutputTargetBase {
export abstract class OutputTargetElementWrapper<EventMap extends EventEmitter.ValidEventTypes> extends OutputTargetBase {
// JS/TS can't do true multiple inheritance, so we maintain class events on a readonly field.
public readonly events: EventEmitter<EventMap, this> = new EventEmitter<EventMap, this>();

View file

@ -1,7 +1,7 @@
import OutputTarget from './outputTarget.js';
import { OutputTargetElementWrapper } from './outputTargetElementWrapper.js';
import { KMWString } from '@keymanapp/web-utils';
export default class TextArea extends OutputTarget<{}> {
export class TextArea extends OutputTargetElementWrapper<{}> {
root: HTMLTextAreaElement;
/**

View file

@ -1,11 +1,11 @@
import type OutputTarget from './outputTarget.js';
import Input from './input.js';
import TextArea from './textarea.js';
import DesignIFrame from './designIFrame.js';
import ContentEditable from './contentEditable.js';
import { type OutputTargetElementWrapper } from './outputTargetElementWrapper.js';
import { Input } from './input.js';
import { TextArea } from './textarea.js';
import { DesignIFrame } from './designIFrame.js';
import { ContentEditable } from './contentEditable.js';
import { nestedInstanceOf } from './utils.js';
export default function wrapElement(e: HTMLElement): OutputTarget<any> {
export function wrapElement(e: HTMLElement): OutputTargetElementWrapper<any> {
// Complex type scoping is implemented here so that kmwutils.ts is not a dependency for test compilations.
if(nestedInstanceOf(e, "HTMLInputElement")) {

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

@ -1,6 +1,6 @@
import { LexicalModelTypes } from '@keymanapp/common-types';
import { EventEmitter } from "eventemitter3";
import { OutputTarget } from "keyman/engine/keyboard";
import { OutputTargetInterface } from "keyman/engine/keyboard";
export class ReadySuggestions {
suggestions: LexicalModelTypes.Suggestion[];
@ -45,10 +45,10 @@ export interface LanguageProcessorEventMap {
/**
* Is called synchronously once suggestion application is successful and the context has been updated.
*
* @param outputTarget The `OutputTarget` representation of the context the suggestion was applied to.
* @param outputTarget The `OutputTargetInterface` representation of the context the suggestion was applied to.
* @returns
*/
'suggestionapplied': (outputTarget: OutputTarget) => boolean
'suggestionapplied': (outputTarget: OutputTargetInterface) => boolean
}
@ -56,7 +56,7 @@ export interface LanguageProcessorSpec extends EventEmitter<LanguageProcessorEve
get state(): StateChangeEnum;
invalidateContext(outputTarget: OutputTarget, layerId: string): Promise<LexicalModelTypes.Suggestion[]>;
invalidateContext(outputTarget: OutputTargetInterface, layerId: string): Promise<LexicalModelTypes.Suggestion[]>;
/**
*
@ -66,9 +66,9 @@ export interface LanguageProcessorSpec extends EventEmitter<LanguageProcessorEve
* required because layerid can be changed by PostKeystroke
* @returns
*/
applySuggestion(suggestion: LexicalModelTypes.Suggestion, outputTarget: OutputTarget, getLayerId: () => string): Promise<LexicalModelTypes.Reversion>;
applySuggestion(suggestion: LexicalModelTypes.Suggestion, outputTarget: OutputTargetInterface, getLayerId: () => string): Promise<LexicalModelTypes.Reversion>;
applyReversion(reversion: LexicalModelTypes.Reversion, outputTarget: OutputTarget): Promise<LexicalModelTypes.Suggestion[]>;
applyReversion(reversion: LexicalModelTypes.Reversion, outputTarget: OutputTargetInterface): Promise<LexicalModelTypes.Suggestion[]>;
get wordbreaksAfterSuggestions(): boolean;

View file

@ -4,7 +4,7 @@ import Keep = LexicalModelTypes.Keep;
import Reversion = LexicalModelTypes.Reversion;
import Suggestion = LexicalModelTypes.Suggestion;
import { type LanguageProcessorSpec , ReadySuggestions, type InvalidateSourceEnum, StateChangeHandler } from './languageProcessor.interface.js';
import { type OutputTarget } from "keyman/engine/keyboard";
import { type OutputTargetInterface } from "keyman/engine/keyboard";
interface PredictionContextEventMap {
update: (suggestions: Suggestion[]) => void;
@ -41,13 +41,13 @@ export default class PredictionContext extends EventEmitter<PredictionContextEve
/**
* Represents the active context used when requesting and applying predictive-text operations.
*/
private _currentTarget: OutputTarget;
private _currentTarget: OutputTargetInterface;
public get currentTarget(): OutputTarget {
public get currentTarget(): OutputTargetInterface {
return this._currentTarget;
}
public setCurrentTarget(target: OutputTarget): Promise<Suggestion[]> {
public setCurrentTarget(target: OutputTargetInterface): Promise<Suggestion[]> {
const originalTarget = this._currentTarget;
this._currentTarget = target;

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

@ -1,11 +1,8 @@
export { default as KeyboardProcessor } from "./keyboardProcessor.js";
export * from "./keyboardProcessor.js";
export { default as RuleBehavior } from "./ruleBehavior.js";
export * from './kbdInterface.js';
export { default as KeyboardInterface } from "./kbdInterface.js";
export * from "./systemStores.js";
export { BeepHandler, JSKeyboardProcessor, LogMessageHandler, ProcessorInitOptions } from "./jsKeyboardProcessor.js";
export { JSKeyboardInterface, KeyInformation, StoreNonCharEntry } from "./jsKeyboardInterface.js";
export * from "./deadkeys.js";
export { default as OutputTarget } from "./outputTarget.js";
export * from "./outputTarget.js";
export { type ComplexKeyboardStore } from "./stores.js";
export { OutputTargetBase } from "./outputTargetBase.js";
export * from "./outputTargetBase.js";
export { Mock } from "./mock.js";
export * from "./stringDivergence.js";

View file

@ -7,12 +7,27 @@
import { type DeviceSpec, KMWString } from "@keymanapp/web-utils";
import { ModifierKeyConstants } from '@keymanapp/common-types';
import { Codes, type KeyEvent, KeyMapping, Keyboard, KeyboardHarness, KeyboardKeymanGlobal, VariableStoreDictionary } from "keyman/engine/keyboard";
import type OutputTarget from './outputTarget.js';
import {
Codes,
JSKeyboard,
KeyboardHarness,
KeyboardKeymanGlobal,
KeyMapping,
MutableSystemStore,
SystemStore,
SystemStoreIDs,
type KeyEvent,
type OutputTargetInterface,
ProcessorAction,
VariableStore,
VariableStoreDictionary,
VariableStoreSerializer,
} from "keyman/engine/keyboard";
import { type OutputTargetBase } from './outputTargetBase.js';
import { type Deadkey } from './deadkeys.js';
import { Mock } from "./mock.js";
import RuleBehavior from "./ruleBehavior.js";
import { ComplexKeyboardStore, type KeyboardStore, KeyboardStoreElement, SystemStoreIDs, SystemStore, MutableSystemStore, PlatformSystemStore, VariableStore, VariableStoreSerializer } from "./systemStores.js";
import { PlatformSystemStore } from './platformSystemStore.js';
import { ComplexKeyboardStore, type KeyboardStore, KeyboardStoreElement } from "./stores.js";
//#endregion
@ -165,22 +180,22 @@ class CachedContextEx {
//#endregion
export default class KeyboardInterface extends KeyboardHarness {
export class JSKeyboardInterface extends KeyboardHarness {
static readonly GLOBAL_NAME = 'KeymanWeb';
cachedContext: CachedContext = new CachedContext();
cachedContextEx: CachedContextEx = new CachedContextEx();
ruleContextEx: CachedContextEx;
activeTargetOutput: OutputTarget;
ruleBehavior: RuleBehavior;
activeTargetOutput: OutputTargetInterface;
ruleBehavior: ProcessorAction;
systemStores: {[storeID: number]: SystemStore};
_AnyIndices: number[] = []; // AnyIndex - array of any/index match indices
// Must be accessible to some of the keyboard API methods.
activeKeyboard: Keyboard;
activeKeyboard: JSKeyboard;
activeDevice: DeviceSpec;
variableStoreSerializer?: VariableStoreSerializer;
@ -222,7 +237,7 @@ export default class KeyboardInterface extends KeyboardHarness {
/**
* Function registerKeyboard KR
* Scope Public
* @param {Object} Pk Keyboard object
* @param {Object} Pk JSKeyboard object
* Description Registers a keyboard with KeymanWeb once its script has fully loaded.
*
* In web-core, this also activates the keyboard; in other modules, this method
@ -231,17 +246,17 @@ export default class KeyboardInterface extends KeyboardHarness {
registerKeyboard(Pk: any): void {
// NOTE: This implementation is web-core specific and is intentionally replaced, whole-sale,
// by DOM-aware code.
const keyboard = new Keyboard(Pk);
const keyboard = new JSKeyboard(Pk);
this.loadedKeyboard = keyboard;
}
/**
* Get *cached or uncached* keyboard context for a specified range, relative to caret
*
* @param {number} n Number of characters to move back from caret
* @param {number} ln Number of characters to return
* @param {Object} Pelem Element to work with (must be currently focused element)
* @return {string} Context string
* @param {number} n Number of characters to move back from caret
* @param {number} ln Number of characters to return
* @param {Object} outputTarget Element to work with (must be currently focused element)
* @return {string} Context string
*
* Example [abcdef|ghi] as INPUT, with the caret position marked by |:
* KC(2,1,Pelem) == "e"
@ -249,7 +264,7 @@ export default class KeyboardInterface extends KeyboardHarness {
* KC(10,10,Pelem) == "abcdef" i.e. return as much as possible of the requested string
*/
context(n: number, ln: number, outputTarget: OutputTarget): string {
context(n: number, ln: number, outputTarget: OutputTargetInterface): string {
const v = this.cachedContext.get(n, ln);
if(v !== null) {
return v;
@ -263,17 +278,17 @@ export default class KeyboardInterface extends KeyboardHarness {
/**
* Get (uncached) keyboard context for a specified range, relative to caret
*
* @param {number} n Number of characters to move back from caret
* @param {number} ln Number of characters to return
* @param {Object} Pelem Element to work with (must be currently focused element)
* @return {string} Context string
* @param {number} n Number of characters to move back from caret
* @param {number} ln Number of characters to return
* @param {Object} outputTarget Element to work with (must be currently focused element)
* @return {string} Context string
*
* Example [abcdef|ghi] as INPUT, with the caret position marked by |:
* KC(2,1,Pelem) == "e"
* KC(3,3,Pelem) == "def"
* KC(10,10,Pelem) == "XXXXabcdef" i.e. return as much as possible of the requested string, where X = \uFFFE
*/
private KC_(n: number, ln: number, outputTarget: OutputTarget): string {
private KC_(n: number, ln: number, outputTarget: OutputTargetInterface): string {
let tempContext = '';
// If we have a selection, we have an empty context
@ -289,9 +304,9 @@ export default class KeyboardInterface extends KeyboardHarness {
/**
* Function nul KN
* Scope Public
* @param {number} n Length of context to check
* @param {Object} Ptarg Element to work with (must be currently focused element)
* @return {boolean} True if length of context is less than or equal to n
* @param {number} n Length of context to check
* @param {Object} outputTarget Element to work with (must be currently focused element)
* @return {boolean} True if length of context is less than or equal to n
* Description Test length of context, return true if the length of the context is less than or equal to n
*
* Example [abc|def] as INPUT, with the caret position marked by |:
@ -299,7 +314,7 @@ export default class KeyboardInterface extends KeyboardHarness {
* KN(2,Pelem) == FALSE
* KN(4,Pelem) == TRUE
*/
nul(n: number, outputTarget: OutputTarget): boolean {
nul(n: number, outputTarget: OutputTargetInterface): boolean {
const cx=this.context(n+1, 1, outputTarget);
// With #31, the result will be a replacement character if context is empty.
@ -309,31 +324,31 @@ export default class KeyboardInterface extends KeyboardHarness {
/**
* Function contextMatch KCM
* Scope Public
* @param {number} n Number of characters to move back from caret
* @param {Object} Ptarg Focused element
* @param {string} val String to match
* @param {number} ln Number of characters to return
* @return {boolean} True if selected context matches val
* @param {number} n Number of characters to move back from caret
* @param {Object} outputTarget Focused element
* @param {string} val String to match
* @param {number} ln Number of characters to return
* @return {boolean} True if selected context matches val
* Description Test keyboard context for match
*/
contextMatch(n: number, outputTarget: OutputTarget, val: string, ln: number): boolean {
contextMatch(n: number, outputTarget: OutputTargetInterface, val: string, ln: number): boolean {
const cx=this.context(n, ln, outputTarget);
if(cx === val) {
return true; // I3318
}
outputTarget.deadkeys().resetMatched(); // I3318
(outputTarget as OutputTargetBase).deadkeys().resetMatched(); // I3318
return false;
}
/**
* Builds the *cached or uncached* keyboard context for a specified range, relative to caret
*
* @param {number} n Number of characters to move back from caret
* @param {number} ln Number of characters to return
* @param {Object} Pelem Element to work with (must be currently focused element)
* @return {Array} Context array (of strings and numbers)
* @param {number} n Number of characters to move back from caret
* @param {number} ln Number of characters to return
* @param {Object} outputTarget Element to work with (must be currently focused element)
* @return {Array} Context array (of strings and numbers)
*/
private _BuildExtendedContext(n: number, ln: number, outputTarget: OutputTarget): CachedExEntry {
private _BuildExtendedContext(n: number, ln: number, outputTarget: OutputTargetBase): CachedExEntry {
let cache: CachedExEntry = this.cachedContextEx.get(n, ln);
if(cache !== null) {
return cache;
@ -382,15 +397,15 @@ export default class KeyboardInterface extends KeyboardHarness {
/**
* Function fullContextMatch KFCM
* Scope Private
* @param {number} n Number of characters to move back from caret
* @param {Object} Ptarg Focused element
* @param {Array} rule An array of ContextEntries to match.
* @return {boolean} True if the fully-specified rule context matches the current KMW state.
* @param {number} n Number of characters to move back from caret
* @param {Object} outputTarget Focused element
* @param {Array} rule An array of ContextEntries to match.
* @return {boolean} True if the fully-specified rule context matches the current KMW state.
*
* A KMW 10+ function designed to bring KMW closer to Keyman Desktop functionality,
* near-directly modeling (externally) the compiled form of Desktop rules' context section.
*/
fullContextMatch(n: number, outputTarget: OutputTarget, rule: ContextEntry[]): boolean {
fullContextMatch(n: number, outputTarget: OutputTargetBase, rule: ContextEntry[]): boolean {
// Stage one: build the context index map.
const fullContext = this._BuildExtendedContext(n, rule.length, outputTarget);
this.ruleContextEx = this.cachedContextEx.clone();
@ -559,7 +574,7 @@ export default class KeyboardInterface extends KeyboardHarness {
const modifierBitmask = bitmask & Codes.modifierBitmasks["ALL"];
const stateBitmask = bitmask & Codes.stateBitmasks["ALL"];
const eventModifiers = KeyboardInterface.matchModifiersToRuleChirality(e.Lmodifiers, Lruleshift);
const eventModifiers = JSKeyboardInterface.matchModifiersToRuleChirality(e.Lmodifiers, Lruleshift);
if(e.vkCode > 255) {
keyCode = e.vkCode; // added to support extended (touch-hold) keys for mnemonic layouts
@ -574,7 +589,7 @@ export default class KeyboardInterface extends KeyboardHarness {
retVal = (keyCode == Lrulekey); // I3318, I3555
}
if(!retVal) {
this.activeTargetOutput.deadkeys().resetMatched(); // I3318
(this.activeTargetOutput as OutputTargetBase).deadkeys().resetMatched(); // I3318
}
return retVal; // I3318
};
@ -608,23 +623,23 @@ export default class KeyboardInterface extends KeyboardHarness {
/**
* Function deadkeyMatch KDM
* Scope Public
* @param {number} n offset from current cursor position
* @param {Object} Ptarg target element
* @param {number} d deadkey
* @return {boolean} True if deadkey found selected context matches val
* @param {number} n offset from current cursor position
* @param {Object} outputTarget target element
* @param {number} d deadkey
* @return {boolean} True if deadkey found selected context matches val
* Description Match deadkey at current cursor position
*/
deadkeyMatch(n: number, outputTarget: OutputTarget, d: number): boolean {
deadkeyMatch(n: number, outputTarget: OutputTargetInterface, d: number): boolean {
return outputTarget.hasDeadkeyMatch(n, d);
}
/**
* Function beep KB
* Scope Public
* @param {Object} Pelem element to flash
* @param {Object} outputTarget element to flash
* Description Flash body as substitute for audible beep; notify embedded device to vibrate
*/
beep(outputTarget: OutputTarget): void {
beep(outputTarget: OutputTargetInterface): void {
this.resetContextCache();
// Denote as part of the matched rule's behavior.
@ -711,13 +726,13 @@ export default class KeyboardInterface extends KeyboardHarness {
/**
* Function indexOutput KIO
* Scope Public
* @param {number} Pdn no of character to overwrite (delete)
* @param {string} Ps string
* @param {number} Pn index
* @param {Object} Pelem element to output to
* @param {number} Pdn no of character to overwrite (delete)
* @param {string} Ps string
* @param {number} Pn index
* @param {Object} outputTarget element to output to
* Description Output a character selected from the string according to the offset in the index array
*/
indexOutput(Pdn: number, Ps: KeyboardStore, Pn: number, outputTarget: OutputTarget): void {
indexOutput(Pdn: number, Ps: KeyboardStore, Pn: number, outputTarget: OutputTargetInterface): void {
this.resetContextCache();
const assertNever = function(x: never): never {
@ -750,17 +765,16 @@ export default class KeyboardInterface extends KeyboardHarness {
/**
* Function deleteContext KDC
* Scope Public
* @param {number} dn number of context entries to overwrite
* @param {Object} Pelem element to output to
* @param {string} s string to output
* @param {number} dn number of context entries to overwrite
* @param {Object} outputTarget element to output to
* Description Keyboard output
*/
deleteContext(dn: number, outputTarget: OutputTarget): void {
deleteContext(dn: number, outputTarget: OutputTargetInterface): void {
let context: CachedExEntry;
// We want to control exactly which deadkeys get removed.
if(dn > 0) {
context = this._BuildExtendedContext(dn, dn, outputTarget);
context = this._BuildExtendedContext(dn, dn, (outputTarget as OutputTargetBase));
let nulCount = 0;
for(let i=0; i < context.valContext.length; i++) {
@ -768,7 +782,7 @@ export default class KeyboardInterface extends KeyboardHarness {
if(dk) {
// Remove deadkey in context.
outputTarget.deadkeys().remove(dk);
(outputTarget as OutputTargetBase).deadkeys().remove(dk);
// Reduce our reported context size.
dn--;
@ -787,7 +801,7 @@ export default class KeyboardInterface extends KeyboardHarness {
}
// If a matched deadkey hasn't been deleted, we don't WANT to delete it.
outputTarget.deadkeys().resetMatched();
(outputTarget as OutputTargetBase).deadkeys().resetMatched();
// Why reinvent the wheel? Delete the remaining characters by 'inserting a blank string'.
this.output(dn, outputTarget, '');
@ -796,17 +810,17 @@ export default class KeyboardInterface extends KeyboardHarness {
/**
* Function output KO
* Scope Public
* @param {number} dn number of characters to overwrite
* @param {Object} Pelem element to output to
* @param {string} s string to output
* @param {number} dn number of characters to overwrite
* @param {Object} outputTarget element to output to
* @param {string} s string to output
* Description Keyboard output
*/
output(dn: number, outputTarget: OutputTarget, s:string): void {
output(dn: number, outputTarget: OutputTargetInterface, s:string): void {
this.resetContextCache();
outputTarget.saveProperties();
outputTarget.clearSelection();
outputTarget.deadkeys().deleteMatched(); // I3318
(outputTarget as OutputTargetBase).deadkeys().deleteMatched(); // I3318
if(dn >= 0) {
// Automatically manages affected deadkey positions. Does not delete deadkeys b/c legacy behavior support.
outputTarget.deleteCharsBeforeCaret(dn);
@ -824,11 +838,11 @@ export default class KeyboardInterface extends KeyboardHarness {
* @alias KCXO
* @public
* @param {number} Pdn number of characters to delete left of cursor
* @param {OutputTarget} outputTarget target to output to
* @param {OutputTargetInterface} outputTarget target to output to
* @param {number} contextLength length of current rule context to retrieve
* @param {number} contextOffset offset from start of current rule context, 1-based
*/
contextExOutput(Pdn: number, outputTarget: OutputTarget, contextLength: number, contextOffset: number): void {
contextExOutput(Pdn: number, outputTarget: OutputTargetInterface, contextLength: number, contextOffset: number): void {
this.resetContextCache();
if(Pdn >= 0) {
@ -849,12 +863,12 @@ export default class KeyboardInterface extends KeyboardHarness {
/**
* Function deadkeyOutput KDO
* Scope Public
* @param {number} Pdn no of character to overwrite (delete)
* @param {Object} Pelem element to output to
* @param {number} Pd deadkey id
* @param {number} Pdn no of character to overwrite (delete)
* @param {OutputTargetInterface} outputTarget element to output to
* @param {number} Pd deadkey id
* Description Record a deadkey at current cursor position, deleting Pdn characters first
*/
deadkeyOutput(Pdn: number, outputTarget: OutputTarget, Pd: number): void {
deadkeyOutput(Pdn: number, outputTarget: OutputTargetInterface, Pd: number): void {
this.resetContextCache();
if(Pdn >= 0) {
@ -868,12 +882,12 @@ export default class KeyboardInterface extends KeyboardHarness {
/**
* KIFS compares the content of a system store with a string value
*
* @param {number} systemId ID of the system store to test (only TSS_LAYER currently supported)
* @param {string} strValue String value to compare to
* @param {Object} Pelem Currently active element (may be needed by future tests)
* @return {boolean} True if the test succeeds
* @param {number} systemId ID of the system store to test (only TSS_LAYER currently supported)
* @param {string} strValue String value to compare to
* @param {OutputTargetInterface} outputTarget Currently active element (may be needed by future tests)
* @return {boolean} True if the test succeeds
*/
ifStore(systemId: number, strValue: string, outputTarget: OutputTarget): boolean {
ifStore(systemId: number, strValue: string, outputTarget: OutputTargetInterface): boolean {
let result=true;
const store = this.systemStores[systemId];
if(store) {
@ -885,16 +899,16 @@ export default class KeyboardInterface extends KeyboardHarness {
/**
* KSETS sets the value of a system store to a string
*
* @param {number} systemId ID of the system store to set (only TSS_LAYER currently supported)
* @param {string} strValue String to set as the system store content
* @param {Object} Pelem Currently active element (may be needed in future tests)
* @return {boolean} True if command succeeds
* (i.e. for TSS_LAYER, if the layer is successfully selected)
* @param {number} systemId ID of the system store to set (only TSS_LAYER currently supported)
* @param {string} strValue String to set as the system store content
* @param {OutputTargetInterface} outputTarget Currently active element (may be needed in future tests)
* @return {boolean} True if command succeeds
* (i.e. for TSS_LAYER, if the layer is successfully selected)
*
* Note that option/variable stores are instead set within keyboard script code, as they only
* affect keyboard behavior.
*/
setStore(systemId: number, strValue: string, outputTarget: OutputTarget): boolean {
setStore(systemId: number, strValue: string, outputTarget: OutputTargetInterface): boolean {
this.resetContextCache();
// Unique case: we only allow set(&layer) ops from keyboard rules triggered by touch OSKs.
if(systemId == SystemStoreIDs.TSS_LAYER && this.activeDevice.touchable) {
@ -964,7 +978,7 @@ export default class KeyboardInterface extends KeyboardHarness {
this.cachedContextEx.reset();
}
defaultBackspace(outputTarget: OutputTarget) {
defaultBackspace(outputTarget: OutputTargetInterface) {
if(outputTarget.isSelectionEmpty()) {
// Delete the character left of the caret
this.output(1, outputTarget, "");
@ -980,9 +994,9 @@ export default class KeyboardInterface extends KeyboardHarness {
* @param {Object} outputTarget The target receiving input
* @param {Object} keystroke The input keystroke (with its properties) to be mapped by the keyboard.
* Description Calls the keyboard's `begin newContext` group
* @returns {RuleBehavior} Record of commands and state changes that result from executing `begin NewContext`
* @returns {ProcessorAction} Record of commands and state changes that result from executing `begin NewContext`
*/
processNewContextEvent(outputTarget: OutputTarget, keystroke: KeyEvent): RuleBehavior {
processNewContextEvent(outputTarget: OutputTargetBase, keystroke: KeyEvent): ProcessorAction {
if(!this.activeKeyboard) {
throw "No active keyboard for keystroke processing!";
}
@ -995,9 +1009,9 @@ export default class KeyboardInterface extends KeyboardHarness {
* @param {Object} outputTarget The target receiving input
* @param {Object} keystroke The input keystroke with relevant properties to be mapped by the keyboard.
* Description Calls the keyboard's `begin postKeystroke` group
* @returns {RuleBehavior} Record of commands and state changes that result from executing `begin PostKeystroke`
* @returns {ProcessorAction} Record of commands and state changes that result from executing `begin PostKeystroke`
*/
processPostKeystroke(outputTarget: OutputTarget, keystroke: KeyEvent): RuleBehavior {
processPostKeystroke(outputTarget: OutputTargetBase, keystroke: KeyEvent): ProcessorAction {
if(!this.activeKeyboard) {
throw "No active keyboard for keystroke processing!";
}
@ -1010,16 +1024,16 @@ export default class KeyboardInterface extends KeyboardHarness {
* @param {Object} outputTarget The target receiving input
* @param {Object} keystroke The input keystroke (with its properties) to be mapped by the keyboard.
* Description Encapsulates calls to keyboard input processing.
* @returns {RuleBehavior} Record of commands and state changes that result from executing `begin Unicode`
* @returns {ProcessorAction} Record of commands and state changes that result from executing `begin Unicode`
*/
processKeystroke(outputTarget: OutputTarget, keystroke: KeyEvent): RuleBehavior {
processKeystroke(outputTarget: OutputTargetBase, keystroke: KeyEvent): ProcessorAction {
if(!this.activeKeyboard) {
throw "No active keyboard for keystroke processing!";
}
return this.process(this.activeKeyboard.process.bind(this.activeKeyboard), outputTarget, keystroke, false);
}
private process(callee: (outputTarget: OutputTarget, keystroke: KeyEvent) => boolean, outputTarget: OutputTarget, keystroke: KeyEvent, readonly: boolean): RuleBehavior {
private process(callee: (outputTarget: OutputTargetBase, keystroke: KeyEvent) => boolean, outputTarget: OutputTargetBase, keystroke: KeyEvent, readonly: boolean): ProcessorAction {
// Clear internal state tracking data from prior keystrokes.
if(!outputTarget) {
throw "No target specified for keyboard output!";
@ -1041,7 +1055,7 @@ export default class KeyboardInterface extends KeyboardHarness {
const cachedVariableStores = this.activeKeyboard.variableStores;
// Establishes the results object, allowing corresponding commands to set values here as appropriate.
this.ruleBehavior = new RuleBehavior();
this.ruleBehavior = new ProcessorAction();
// Ensure the settings are in place so that KIFS/ifState activates and deactivates
// the appropriate rule(s) for the modeled device.
@ -1055,7 +1069,7 @@ export default class KeyboardInterface extends KeyboardHarness {
// Finalize the rule's results.
this.ruleBehavior.transcription = outputTarget.buildTranscriptionFrom(preInput, keystroke, readonly);
// We always backup the changes to variable stores to the RuleBehavior, to
// We always backup the changes to variable stores to the ProcessorAction, to
// be applied during finalization, then restore them to the cached initial
// values to avoid side-effects with predictive text mocks.
this.ruleBehavior.variableStores = this.activeKeyboard.variableStores;
@ -1089,7 +1103,7 @@ export default class KeyboardInterface extends KeyboardHarness {
}
/**
* Publishes the KeyboardInterface's shorthand API names. As this assigns the current functions
* Publishes the JSKeyboardInterface's shorthand API names. As this assigns the current functions
* held by the longform versions, note that this should be called after replacing any of them via
* JS method extension.
*
@ -1100,7 +1114,7 @@ export default class KeyboardInterface extends KeyboardHarness {
// Keyboard callbacks
const prototype = this.prototype;
const exportKBCallback = function(miniName: string, longName: keyof KeyboardInterface) {
const exportKBCallback = function(miniName: string, longName: keyof JSKeyboardInterface) {
if(prototype[longName]) {
// @ts-ignore
prototype[miniName] = prototype[longName];
@ -1139,5 +1153,5 @@ export default class KeyboardInterface extends KeyboardHarness {
(function() {
// This will be the only call within the keyboard module.
KeyboardInterface.__publishShorthandAPI();
JSKeyboardInterface.__publishShorthandAPI();
}());

View file

@ -9,42 +9,41 @@
import { EventEmitter } from 'eventemitter3';
import { ModifierKeyConstants } from '@keymanapp/common-types';
import {
Codes, type Keyboard, MinimalKeymanGlobal, KeyEvent, Layouts,
DefaultRules, EmulationKeystrokes
Codes, type JSKeyboard, MinimalKeymanGlobal, KeyEvent, Layouts,
DefaultRules, EmulationKeystrokes, type MutableSystemStore,
OutputTargetInterface, ProcessorAction, SystemStoreIDs
} from "keyman/engine/keyboard";
import { Mock } from "./mock.js";
import type OutputTarget from "./outputTarget.js";
import RuleBehavior from "./ruleBehavior.js";
import KeyboardInterface from './kbdInterface.js';
import { type OutputTargetBase } from "./outputTargetBase.js";
import { JSKeyboardInterface } from './jsKeyboardInterface.js';
import { DeviceSpec, globalObject, KMWString } from "@keymanapp/web-utils";
import { type MutableSystemStore, SystemStoreIDs } from "./systemStores.js";
// #endregion
// Also relies on @keymanapp/web-utils, which is included via tsconfig.json.
export type BeepHandler = (outputTarget: OutputTarget) => void;
export type BeepHandler = (outputTarget: OutputTargetInterface) => void;
export type LogMessageHandler = (str: string) => void;
export interface ProcessorInitOptions {
baseLayout?: string;
keyboardInterface?: KeyboardInterface;
keyboardInterface?: JSKeyboardInterface;
defaultOutputRules?: DefaultRules; // Takes the class def object, not an instance thereof.
}
interface EventMap {
statekeychange: (stateKeys: typeof KeyboardProcessor.prototype.stateKeys) => void;
statekeychange: (stateKeys: typeof JSKeyboardProcessor.prototype.stateKeys) => void;
}
export default class KeyboardProcessor extends EventEmitter<EventMap> {
public static readonly DEFAULT_OPTIONS: ProcessorInitOptions = {
export class JSKeyboardProcessor extends EventEmitter<EventMap> {
private static readonly DEFAULT_OPTIONS: ProcessorInitOptions = {
baseLayout: 'us',
defaultOutputRules: new DefaultRules()
};
// Tracks the simulated value for supported state keys, allowing the OSK to mirror a physical keyboard for them.
// Using the exact keyCode name from the Codes definitions will allow for certain optimizations elsewhere in the code.
stateKeys = {
public stateKeys = {
"K_CAPS":false,
"K_NUMLOCK":false,
"K_SCROLL":false
@ -53,45 +52,45 @@ export default class KeyboardProcessor extends EventEmitter<EventMap> {
// Tracks the most recent modifier state information in order to quickly detect changes
// in keyboard state not otherwise captured by the hosting page in the browser.
// Needed for AltGr simulation.
modStateFlags: number = 0;
public modStateFlags: number = 0;
keyboardInterface: KeyboardInterface;
public keyboardInterface: JSKeyboardInterface;
/**
* Indicates the device (platform) to be used for non-keystroke events,
* such as those sent to `begin postkeystroke` and `begin newcontext`
* entry points.
*/
contextDevice: DeviceSpec;
public contextDevice: DeviceSpec;
baseLayout: string;
public baseLayout: string;
defaultRules: DefaultRules;
public defaultRules: DefaultRules;
// Callbacks for various feedback types
beepHandler?: BeepHandler;
warningLogger?: LogMessageHandler;
errorLogger?: LogMessageHandler;
public beepHandler?: BeepHandler;
public warningLogger?: LogMessageHandler;
public errorLogger?: LogMessageHandler;
constructor(device: DeviceSpec, options?: ProcessorInitOptions) {
super();
if(!options) {
options = KeyboardProcessor.DEFAULT_OPTIONS;
options = JSKeyboardProcessor.DEFAULT_OPTIONS;
}
this.contextDevice = device;
this.baseLayout = options.baseLayout || KeyboardProcessor.DEFAULT_OPTIONS.baseLayout;
this.keyboardInterface = options.keyboardInterface || new KeyboardInterface(globalObject(), MinimalKeymanGlobal);
this.defaultRules = options.defaultOutputRules || KeyboardProcessor.DEFAULT_OPTIONS.defaultOutputRules;
this.baseLayout = options.baseLayout || JSKeyboardProcessor.DEFAULT_OPTIONS.baseLayout;
this.keyboardInterface = options.keyboardInterface || new JSKeyboardInterface(globalObject(), MinimalKeymanGlobal);
this.defaultRules = options.defaultOutputRules || JSKeyboardProcessor.DEFAULT_OPTIONS.defaultOutputRules;
}
public get activeKeyboard(): Keyboard {
public get activeKeyboard(): JSKeyboard {
return this.keyboardInterface.activeKeyboard;
}
public set activeKeyboard(keyboard: Keyboard) {
public set activeKeyboard(keyboard: JSKeyboard) {
this.keyboardInterface.activeKeyboard = keyboard;
// All old deadkeys and keyboard-specific cache should immediately be invalidated
@ -99,7 +98,7 @@ export default class KeyboardProcessor extends EventEmitter<EventMap> {
this.resetContext();
}
get layerStore(): MutableSystemStore {
public get layerStore(): MutableSystemStore {
return this.keyboardInterface.systemStores[SystemStoreIDs.TSS_LAYER] as MutableSystemStore;
}
@ -121,16 +120,17 @@ export default class KeyboardProcessor extends EventEmitter<EventMap> {
}
/**
* Get the default RuleBehavior for the specified key, attempting to mimic standard browser defaults
* Get the default ProcessorAction for the specified key, attempting to mimic standard browser defaults
* where and when appropriate.
*
* @param {object} Lkc The pre-analyzed KeyEvent object
* @param {boolean} outputTarget The OutputTarget receiving the KeyEvent
* @param {OutputTargetBase} outputTarget The output target receiving the KeyEvent
* @param {boolean} readonly True if the target is read-only
* @return {string}
*/
defaultRuleBehavior(Lkc: KeyEvent, outputTarget: OutputTarget, readonly: boolean): RuleBehavior {
private defaultRuleBehavior(Lkc: KeyEvent, outputTarget: OutputTargetBase, readonly: boolean): ProcessorAction {
const preInput = Mock.from(outputTarget, readonly);
const ruleBehavior = new RuleBehavior();
const ruleBehavior = new ProcessorAction();
let matched = false;
let char = '';
@ -182,7 +182,7 @@ export default class KeyboardProcessor extends EventEmitter<EventMap> {
this.keyboardInterface.output(0, outputTarget, char);
}
} else {
// No match, no default RuleBehavior.
// No match, no default ProcessorAction.
return null;
}
}
@ -198,20 +198,20 @@ export default class KeyboardProcessor extends EventEmitter<EventMap> {
return ruleBehavior;
}
processNewContextEvent(device: DeviceSpec, outputTarget: OutputTarget): RuleBehavior {
private processNewContextEvent(device: DeviceSpec, outputTarget: OutputTargetBase): ProcessorAction {
return this.activeKeyboard ?
this.keyboardInterface.processNewContextEvent(outputTarget, this.activeKeyboard.constructNullKeyEvent(device, this.stateKeys)) :
null;
}
processPostKeystroke(device: DeviceSpec, outputTarget: OutputTarget): RuleBehavior {
public processPostKeystroke(device: DeviceSpec, outputTarget: OutputTargetBase): ProcessorAction {
return this.activeKeyboard ?
this.keyboardInterface.processPostKeystroke(outputTarget, this.activeKeyboard.constructNullKeyEvent(device, this.stateKeys)) :
null;
}
processKeystroke(keyEvent: KeyEvent, outputTarget: OutputTarget): RuleBehavior {
let matchBehavior: RuleBehavior;
public processKeystroke(keyEvent: KeyEvent, outputTarget: OutputTargetBase): ProcessorAction {
let matchBehavior: ProcessorAction;
// Before keyboard rules apply, check if the left-context is empty.
const nothingDeletable = KMWString.length(outputTarget.getTextBeforeCaret()) == 0 && outputTarget.isSelectionEmpty();
@ -249,7 +249,7 @@ export default class KeyboardProcessor extends EventEmitter<EventMap> {
if(!matchBehavior) {
matchBehavior = defaultBehavior;
} else {
matchBehavior.mergeInDefaults(defaultBehavior);
this.mergeInOtherProcessorAction(matchBehavior, defaultBehavior);
}
matchBehavior.triggerKeyDefault = false; // We've triggered it successfully.
} // If null, we must rely on something else (like the browser, in DOM-aware code) to fulfill the default.
@ -267,7 +267,7 @@ export default class KeyboardProcessor extends EventEmitter<EventMap> {
* @return {boolean} Always true
* Description Updates the current shift state within KMW, updating the OSK's visualization thereof.
*/
_UpdateVKShift(e: KeyEvent): boolean {
private _UpdateVKShift(e: KeyEvent): boolean {
let keyShiftState=0;
const lockNames = ['CAPS', 'NUM_LOCK', 'SCROLL_LOCK'] as const;
@ -343,7 +343,7 @@ export default class KeyboardProcessor extends EventEmitter<EventMap> {
}
}
getLayerId(modifier: number): string {
private getLayerId(modifier: number): string {
return Layouts.getLayerId(modifier);
}
@ -354,7 +354,7 @@ export default class KeyboardProcessor extends EventEmitter<EventMap> {
* @param {string} keyName key identifier
* @return {boolean} return true if keyboard layer changed
*/
selectLayer(keyEvent: KeyEvent): boolean {
public selectLayer(keyEvent: KeyEvent): boolean {
const keyName = keyEvent.kName;
let nextLayer = keyEvent.kNextLayer;
const isChiral = this.activeKeyboard && this.activeKeyboard.isChiral;
@ -437,7 +437,7 @@ export default class KeyboardProcessor extends EventEmitter<EventMap> {
*
* @param {string} id layer id (e.g. ctrlshift)
*/
updateLayer(keyEvent: KeyEvent, id: string) {
private updateLayer(keyEvent: KeyEvent, id: string) {
const activeLayer = this.layerId;
let s = activeLayer;
@ -539,7 +539,7 @@ export default class KeyboardProcessor extends EventEmitter<EventMap> {
// Returns true if the key event is a modifier press, allowing keyPress to return selectively
// in those cases.
doModifierPress(Levent: KeyEvent, outputTarget: OutputTarget, isKeyDown: boolean): boolean {
public doModifierPress(Levent: KeyEvent, outputTarget: OutputTargetBase, isKeyDown: boolean): boolean {
if(!this.activeKeyboard) {
return false;
}
@ -570,19 +570,19 @@ export default class KeyboardProcessor extends EventEmitter<EventMap> {
* e.g. by focus change, selection change, keyboard change, etc.
*
* @param {Object} outputTarget The OutputTarget that has focus
* @returns {Object} A RuleBehavior object describing the cumulative effects of
* @returns {Object} A ProcessorAction object describing the cumulative effects of
* all matched keyboard rules
*/
performNewContextEvent(outputTarget: OutputTarget): RuleBehavior {
private performNewContextEvent(outputTarget: OutputTargetBase): ProcessorAction {
const ruleBehavior = this.processNewContextEvent(this.contextDevice, outputTarget);
if(ruleBehavior) {
ruleBehavior.finalize(this, outputTarget, true);
if (ruleBehavior) {
this.finalizeProcessorAction(ruleBehavior, outputTarget);
}
return ruleBehavior;
}
resetContext(target?: OutputTarget) {
public resetContext(target?: OutputTargetBase) {
this.layerId = 'default';
// Make sure all deadkeys for the context get cleared properly.
@ -601,7 +601,7 @@ export default class KeyboardProcessor extends EventEmitter<EventMap> {
}
};
setNumericLayer(device: DeviceSpec) {
public setNumericLayer(device: DeviceSpec) {
if (this.activeKeyboard) {
const layout = this.activeKeyboard.layout(device.formFactor);
if(layout.getLayer('numeric')) {
@ -609,4 +609,76 @@ export default class KeyboardProcessor extends EventEmitter<EventMap> {
}
}
};
public finalizeProcessorAction(data: ProcessorAction, outputTarget: OutputTargetInterface): void {
if (!data.transcription) {
throw "Cannot finalize a ProcessorAction with no transcription.";
}
if (this.beepHandler && data.beep) {
this.beepHandler(outputTarget);
}
for (const storeID in data.setStore) {
const sysStore = this.keyboardInterface.systemStores[storeID];
if (sysStore) {
try {
sysStore.set(data.setStore[storeID]);
} catch (error) {
if (this.errorLogger) {
this.errorLogger("Rule attempted to perform illegal operation - 'platform' may not be changed.");
}
}
} else if (this.warningLogger) {
this.warningLogger("Unknown store affected by keyboard rule: " + storeID);
}
}
this.keyboardInterface.applyVariableStores(data.variableStores);
if (this.keyboardInterface.variableStoreSerializer) {
for (const storeID in data.saveStore) {
this.keyboardInterface.variableStoreSerializer.saveStore(this.activeKeyboard.id, storeID, data.saveStore[storeID]);
}
}
if (data.triggersDefaultCommand) {
const keyEvent = data.transcription.keystroke;
this.defaultRules.applyCommand(keyEvent, outputTarget);
}
if (this.warningLogger && data.warningLog) {
this.warningLogger(data.warningLog);
} else if (this.errorLogger && data.errorLog) {
this.errorLogger(data.errorLog);
}
}
/**
* Merges default-related behaviors from another ProcessorAction into this one. Assumes that the current instance
* "came first" chronologically. Both RuleBehaviors must be sourced from the same keystroke.
*
* Intended use: merging rule-based behavior with default key behavior during scenarios like those described
* at https://github.com/keymanapp/keyman/pull/4350#issuecomment-768753852.
*
* This function does not attempt a "complete" merge for two fully-constructed RuleBehaviors! Things
* WILL break for unintended uses.
* @param other
*/
private mergeInOtherProcessorAction(first: ProcessorAction, other: ProcessorAction): void {
const keystroke = first.transcription.keystroke;
const keyFromOther = other.transcription.keystroke;
if (keystroke.Lcode != keyFromOther.Lcode || keystroke.Lmodifiers != keyFromOther.Lmodifiers) {
throw "ProcessorAction default-merge not supported unless keystrokes are identical!";
}
first.triggersDefaultCommand = first.triggersDefaultCommand || other.triggersDefaultCommand;
const mergingMock = Mock.from(first.transcription.preInput, false);
mergingMock.apply(first.transcription.transform);
mergingMock.apply(other.transcription.transform);
first.transcription = mergingMock.buildTranscriptionFrom(first.transcription.preInput, keystroke, false, first.transcription.alternates);
}
}

View file

@ -1,7 +1,8 @@
import OutputTarget from './outputTarget.js';
import { OutputTargetInterface } from 'keyman/engine/keyboard';
import { OutputTargetBase } from './outputTargetBase.js';
import { KMWString } from '@keymanapp/web-utils';
export class Mock extends OutputTarget {
export class Mock extends OutputTargetBase {
text: string;
selStart: number;
@ -25,10 +26,18 @@ export class Mock extends OutputTarget {
this.selForward = this.selEnd >= this.selStart;
}
static assertIsOutputTargetBase(outputTarget: OutputTargetInterface): asserts outputTarget is OutputTargetBase {
if (!(outputTarget instanceof OutputTargetBase)) {
throw new TypeError("outputTarget is not a OutputTargetBase");
}
}
// Clones the state of an existing EditableElement, creating a Mock version of its state.
static from(outputTarget: OutputTarget, readonly?: boolean) {
static from(outputTarget: OutputTargetInterface, readonly?: boolean): Mock {
let clone: Mock;
this.assertIsOutputTargetBase(outputTarget);
if (outputTarget instanceof Mock) {
// Avoids the need to run expensive kmwstring.ts `length()`
// calculations when deep-copying Mock instances.
@ -56,7 +65,7 @@ export class Mock extends OutputTarget {
}
// Also duplicate deadkey state! (Needed for fat-finger ops.)
clone.setDeadkeys(outputTarget.deadkeys());
clone.setDeadkeys((outputTarget as OutputTargetBase).deadkeys());
return clone;
}

View file

@ -1,7 +1,7 @@
import { KMWString } from "@keymanapp/web-utils";
import { Alternate, OutputTargetInterface, TextTransform, Transcription } from 'keyman/engine/keyboard';
import { findCommonSubstringEndIndex } from "./stringDivergence.js";
import { Mock } from "./mock.js";
import { OutputTarget as OutputTargetInterface } from 'keyman/engine/keyboard';
// Defines deadkey management in a manner attachable to each element interface.
import { type KeyEvent } from 'keyman/engine/keyboard';
@ -17,54 +17,7 @@ export function isEmptyTransform(transform: LexicalModelTypes.Transform) {
return transform.insert === '' && transform.deleteLeft === 0 && (transform.deleteRight ?? 0) === 0;
}
export class TextTransform implements LexicalModelTypes.Transform {
readonly insert: string;
readonly deleteLeft: number;
readonly deleteRight: number;
readonly erasedSelection: boolean;
id: number;
constructor(insert: string, deleteLeft: number, deleteRight: number, erasedSelection: boolean) {
this.insert = insert;
this.deleteLeft = deleteLeft;
this.deleteRight = deleteRight;
this.erasedSelection = erasedSelection;
}
public static readonly nil = new TextTransform('', 0, 0, false);
}
export class Transcription {
readonly token: number;
readonly keystroke: KeyEvent;
readonly transform: TextTransform;
alternates: Alternate[]; // constructed after the rest of the transcription.
readonly preInput: Mock;
private static tokenSeed: number = 0;
constructor(keystroke: KeyEvent, transform: TextTransform, preInput: Mock, alternates?: Alternate[]/*, removedDks: Deadkey[], insertedDks: Deadkey[]*/) {
const token = this.token = Transcription.tokenSeed++;
this.keystroke = keystroke;
this.transform = transform;
this.alternates = alternates;
this.preInput = preInput;
this.transform.id = this.token;
// Assign the ID to each alternate, as well.
if(alternates) {
alternates.forEach(function(alt) {
alt.sample.id = token;
});
}
}
}
export type Alternate = LexicalModelTypes.ProbabilityMass<LexicalModelTypes.Transform>;
export default abstract class OutputTarget implements OutputTargetInterface {
export abstract class OutputTargetBase implements OutputTargetInterface {
private _dks: DeadkeyTracker;
constructor() {
@ -122,7 +75,7 @@ export default abstract class OutputTarget implements OutputTargetInterface {
* As such, it assumes that the caret is immediately after any inserted text.
* @param from An output target (preferably a Mock) representing the prior state of the input/output system.
*/
buildTransformFrom(original: OutputTarget): TextTransform {
buildTransformFrom(original: OutputTargetInterface): TextTransform {
const toLeft = this.getTextBeforeCaret();
const fromLeft = original.getTextBeforeCaret();
@ -143,7 +96,7 @@ export default abstract class OutputTarget implements OutputTargetInterface {
return new TextTransform(insertedText, deletedLeft, deletedRight, original.getSelectedText() && !this.getSelectedText());
}
buildTranscriptionFrom(original: OutputTarget, keyEvent: KeyEvent, readonly: boolean, alternates?: Alternate[]): Transcription {
buildTranscriptionFrom(original: OutputTargetInterface, keyEvent: KeyEvent, readonly: boolean, alternates?: Alternate[]): Transcription {
const transform = this.buildTransformFrom(original);
// If we ever decide to re-add deadkey tracking, this is the place for it.
@ -155,7 +108,7 @@ export default abstract class OutputTarget implements OutputTargetInterface {
* Restores the `OutputTarget` to the indicated state. Designed for use with `Transcription.preInput`.
* @param original An `OutputTarget` (usually a `Mock`).
*/
restoreTo(original: OutputTarget) {
restoreTo(original: OutputTargetBase) {
this.clearSelection();
// We currently do not restore selected text; the mechanism isn't supported at present for
// all output target types - especially in regard to re-selecting the text if restored.

View file

@ -0,0 +1,84 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* &platform system store implementation (.js keyboards)
*/
import { SystemStore, SystemStoreIDs, type KeyboardHarness } from 'keyman/engine/keyboard';
/**
* Handles checks against the current platform.
*/
export class PlatformSystemStore extends SystemStore {
private readonly kbdInterface: KeyboardHarness;
constructor(keyboardInterface: KeyboardHarness) {
super(SystemStoreIDs.TSS_PLATFORM);
this.kbdInterface = keyboardInterface;
}
matches(value: string) {
const constraints=value.split(' ');
const device = this.kbdInterface.activeDevice;
for(let i=0; i<constraints.length; i++) {
let constraint=constraints[i].toLowerCase();
switch(constraint) {
case 'touch':
case 'hardware':
if(device.touchable != (constraint == 'touch')) {
return false;
}
break;
case 'macos':
case 'mac':
constraint = 'macosx';
// fall through
case 'macosx':
case 'windows':
case 'android':
case 'ios':
case 'linux':
if(device.OS != constraint) {
return false;
}
break;
case 'tablet':
case 'phone':
case 'desktop':
if(device.formFactor != constraint) {
return false;
}
break;
case 'web':
if(device.browser == 'native') {
return false; // web matches anything other than 'native'
}
break;
case 'native':
// This will return true for embedded KeymanWeb
case 'chrome':
case 'firefox':
case 'safari':
case 'edge':
case 'opera':
if(device.browser != constraint) {
return false;
}
break;
default:
return false;
}
}
// Everything we checked against was valid and had matches - it's a match!
return true;
}
}

View file

@ -1,138 +0,0 @@
import KeyboardProcessor from "./keyboardProcessor.js";
import { VariableStoreDictionary } from "keyman/engine/keyboard";
import OutputTarget, { type Transcription } from './outputTarget.js';
import { Mock } from "./mock.js";
import { type VariableStore } from "./systemStores.js";
import { LexicalModelTypes } from '@keymanapp/common-types';
/**
* Represents the commands and state changes that result from a matched keyboard rule.
*/
export default class RuleBehavior {
/**
* The before-and-after Transform from matching a keyboard rule. May be `null`
* if no keyboard rules were matched for the keystroke.
*/
transcription: Transcription = null;
/**
* Indicates whether or not a BEEP command was issued by the matched keyboard rule.
*/
beep?: boolean;
/**
* A set of changed store values triggered by the matched keyboard rule.
*/
setStore: {[id: number]: string} = {};
/**
* A set of variable stores with save requests triggered by the matched keyboard rule
*/
saveStore: {[name: string]: VariableStore} = {};
/**
* A set of variable stores with possible changes to be applied during finalization.
*/
variableStores: VariableStoreDictionary = {};
/**
* Denotes a non-output default behavior; this should be evaluated later, against the true keystroke.
*/
triggersDefaultCommand: boolean = false;
/**
* Denotes error log messages generated when attempting to generate this behavior.
*/
errorLog?: string;
/**
* Denotes warning log messages generated when attempting to generate this behavior.
*/
warningLog?: string;
/**
* If predictive text is active, contains a Promise returning predictive Suggestions.
*/
predictionPromise?: Promise<LexicalModelTypes.Suggestion[]>;
/**
* In reference to https://github.com/keymanapp/keyman/pull/4350#issuecomment-768753852:
*
* If the final group processed is a context and keystroke group (using keys),
* and there is no nomatch rule, and the keystroke is not matched in the group,
* the keystroke's default behavior should trigger, regardless of whether or not any
* rules in prior groups matched.
*/
triggerKeyDefault?: boolean;
finalize(processor: KeyboardProcessor, outputTarget: OutputTarget, readonly: boolean) {
if(!this.transcription) {
throw "Cannot finalize a RuleBehavior with no transcription.";
}
if(processor.beepHandler && this.beep) {
processor.beepHandler(outputTarget);
}
for(const storeID in this.setStore) {
const sysStore = processor.keyboardInterface.systemStores[storeID];
if(sysStore) {
try {
sysStore.set(this.setStore[storeID]);
} catch (error) {
if(processor.errorLogger) {
processor.errorLogger("Rule attempted to perform illegal operation - 'platform' may not be changed.");
}
}
} else if(processor.warningLogger) {
processor.warningLogger("Unknown store affected by keyboard rule: " + storeID);
}
}
processor.keyboardInterface.applyVariableStores(this.variableStores);
if(processor.keyboardInterface.variableStoreSerializer) {
for(const storeID in this.saveStore) {
processor.keyboardInterface.variableStoreSerializer.saveStore(processor.activeKeyboard.id, storeID, this.saveStore[storeID]);
}
}
if(this.triggersDefaultCommand) {
const keyEvent = this.transcription.keystroke;
processor.defaultRules.applyCommand(keyEvent, outputTarget);
}
if(processor.warningLogger && this.warningLog) {
processor.warningLogger(this.warningLog);
} else if(processor.errorLogger && this.errorLog) {
processor.errorLogger(this.errorLog);
}
}
/**
* Merges default-related behaviors from another RuleBehavior into this one. Assumes that the current instance
* "came first" chronologically. Both RuleBehaviors must be sourced from the same keystroke.
*
* Intended use: merging rule-based behavior with default key behavior during scenarios like those described
* at https://github.com/keymanapp/keyman/pull/4350#issuecomment-768753852.
*
* This function does not attempt a "complete" merge for two fully-constructed RuleBehaviors! Things
* WILL break for unintended uses.
* @param other
*/
mergeInDefaults(other: RuleBehavior) {
const keystroke = this.transcription.keystroke;
const keyFromOther = other.transcription.keystroke;
if(keystroke.Lcode != keyFromOther.Lcode || keystroke.Lmodifiers != keyFromOther.Lmodifiers) {
throw "RuleBehavior default-merge not supported unless keystrokes are identical!";
}
this.triggersDefaultCommand = this.triggersDefaultCommand || other.triggersDefaultCommand;
const mergingMock = Mock.from(this.transcription.preInput, false);
mergingMock.apply(this.transcription.transform);
mergingMock.apply(other.transcription.transform);
this.transcription = mergingMock.buildTranscriptionFrom(this.transcription.preInput, keystroke, false, this.transcription.alternates);
}
}

View file

@ -0,0 +1,20 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Definitions for basic stores (see kmn reference)
*/
import { StoreNonCharEntry } from './jsKeyboardInterface.js';
/*
* Type alias definitions to reflect the parameters of the fullContextMatch() callback (KMW 10+).
* No constructors or methods since keyboards will not utilize the same backing prototype, and
* property names are shorthanded to promote minification.
*/
type PlainKeyboardStore = string;
export type KeyboardStoreElement = (string | StoreNonCharEntry);
export type ComplexKeyboardStore = KeyboardStoreElement[];
export type KeyboardStore = PlainKeyboardStore | ComplexKeyboardStore;

View file

@ -1,160 +0,0 @@
import { type KeyboardHarness } from 'keyman/engine/keyboard';
import { StoreNonCharEntry } from './kbdInterface.js';
export enum SystemStoreIDs {
TSS_LAYER = 33,
TSS_PLATFORM = 31,
TSS_NEWLAYER = 42,
TSS_OLDLAYER = 43
}
/*
* Type alias definitions to reflect the parameters of the fullContextMatch() callback (KMW 10+).
* No constructors or methods since keyboards will not utilize the same backing prototype, and
* property names are shorthanded to promote minification.
*/
type PlainKeyboardStore = string;
export type KeyboardStoreElement = (string | StoreNonCharEntry);
export type ComplexKeyboardStore = KeyboardStoreElement[];
export type KeyboardStore = PlainKeyboardStore | ComplexKeyboardStore;
export type VariableStore = { [name: string]: string };
export interface VariableStoreSerializer {
loadStore(keyboardID: string, storeName: string): VariableStore;
saveStore(keyboardID: string, storeName: string, storeMap: VariableStore): void;
}
/**
* Defines common behaviors associated with system stores.
*/
export abstract class SystemStore {
public readonly id: number;
constructor(id: number) {
this.id = id;
}
abstract matches(value: string): boolean;
set(value: string): void {
throw new Error("System store with ID " + this.id + " may not be directly set.");
}
}
/**
* A handler designed to receive feedback whenever a system store's value is changed.
* @param source The system store being mutated, before the value change occurs.
* @param newValue The new value being set
* @returns `false` / `undefined` to allow the change, `true` to block the change.
*/
export type SystemStoreMutationHandler = (source: MutableSystemStore, newValue: string) => boolean;
export class MutableSystemStore extends SystemStore {
private _value: string;
handler?: SystemStoreMutationHandler = null;
constructor(id: number, defaultValue: string) {
super(id);
this._value = defaultValue;
}
get value() {
return this._value;
}
matches(value: string) {
return this._value == value;
}
set(value: string) {
// Even if things stay the same, we should still signal this.
// It's important for tracking if a rule directly set the layer
// versus if it passively remained.
if(this.handler) {
if(this.handler(this, value)) {
return;
}
}
this._value = value;
}
}
/**
* Handles checks against the current platform.
*/
export class PlatformSystemStore extends SystemStore {
private readonly kbdInterface: KeyboardHarness;
constructor(keyboardInterface: KeyboardHarness) {
super(SystemStoreIDs.TSS_PLATFORM);
this.kbdInterface = keyboardInterface;
}
matches(value: string) {
const constraints=value.split(' ');
const device = this.kbdInterface.activeDevice;
for(let i=0; i<constraints.length; i++) {
let constraint=constraints[i].toLowerCase();
switch(constraint) {
case 'touch':
case 'hardware':
if(device.touchable != (constraint == 'touch')) {
return false;
}
break;
case 'macos':
case 'mac':
constraint = 'macosx';
// fall through
case 'macosx':
case 'windows':
case 'android':
case 'ios':
case 'linux':
if(device.OS != constraint) {
return false;
}
break;
case 'tablet':
case 'phone':
case 'desktop':
if(device.formFactor != constraint) {
return false;
}
break;
case 'web':
if(device.browser == 'native') {
return false; // web matches anything other than 'native'
}
break;
case 'native':
// This will return true for embedded KeymanWeb
case 'chrome':
case 'firefox':
case 'safari':
case 'edge':
case 'opera':
if(device.browser != constraint) {
return false;
}
break;
default:
return false;
}
}
// Everything we checked against was valid and had matches - it's a match!
return true;
}
}

View file

@ -9,5 +9,8 @@
"rootDir": "./src"
},
"references": [
{ "path": "../keyboard"},
],
"include": [ "**/*.ts" ],
}

View file

@ -115,6 +115,10 @@ export default class KeyboardRequisitioner {
});
}
shutdown(): void{
this.cache.shutdown();
}
addKeyboardArray(x: (string|RawKeyboardMetadata)[]): Promise<(KeyboardStub | ErrorStub)[]> {
const completeStubs: KeyboardStub[] = [];
const incompleteStubs: KeyboardStub[] = [];

View file

@ -1,4 +1,4 @@
import { Keyboard, KeyboardLoaderBase as KeyboardLoader } from "keyman/engine/keyboard";
import { type Keyboard, JSKeyboard, KeyboardLoaderBase as KeyboardLoader, KMXKeyboard } from "keyman/engine/keyboard";
import { EventEmitter } from "eventemitter3";
import KeyboardStub from "./keyboardStub.js";
@ -52,6 +52,16 @@ export default class StubAndKeyboardCache extends EventEmitter<EventMap> {
this.keyboardLoader = keyboardLoader;
}
shutdown(): void {
for (const kbdId in this.keyboardTable) {
const kbd = this.keyboardTable[kbdId];
if (kbd instanceof KMXKeyboard) {
(kbd as KMXKeyboard).shutdown();
}
// TODO-web-core: do we have to do something if instanceof Promise?
}
}
getKeyboardForStub(stub: KeyboardStub): Keyboard {
return stub ? this.getKeyboard(stub.KI) : null;
}
@ -146,7 +156,7 @@ export default class StubAndKeyboardCache extends EventEmitter<EventMap> {
keyboardID = prefixed(keyboardID);
const cachedEntry = this.keyboardTable[keyboardID];
if(cachedEntry instanceof Keyboard) {
if(cachedEntry instanceof JSKeyboard) {
return Promise.resolve(cachedEntry);
} else if(cachedEntry instanceof Promise) {
return cachedEntry;
@ -166,7 +176,9 @@ export default class StubAndKeyboardCache extends EventEmitter<EventMap> {
promise.then((kbd) => {
// Overrides the built-in ID in case of keyboard namespacing.
kbd.scriptObject["KI"] = keyboardID;
if (kbd instanceof JSKeyboard) {
kbd.scriptObject["KI"] = keyboardID;
}
this.addKeyboard(kbd);
}).catch((err) => {
delete this.keyboardTable[keyboardID];
@ -189,12 +201,12 @@ export default class StubAndKeyboardCache extends EventEmitter<EventMap> {
}
getStub(keyboardID: string, languageID: string): KeyboardStub;
getStub(keyboard: Keyboard, languageID?: string): KeyboardStub;
getStub(arg0: string | Keyboard, arg1?: string): KeyboardStub {
getStub(keyboard: JSKeyboard, languageID?: string): KeyboardStub;
getStub(arg0: string | JSKeyboard, arg1?: string): KeyboardStub {
let keyboardID: string;
const languageID = arg1 || '---';
if(arg0 instanceof Keyboard) {
if(arg0 instanceof JSKeyboard) {
keyboardID = arg0.id;
} else {
keyboardID = arg0;
@ -221,12 +233,12 @@ export default class StubAndKeyboardCache extends EventEmitter<EventMap> {
/**
* Removes all metadata (stubs) associated with a specific keyboard from the cache, optionally
* removing the cached keyboard as well.
* @param keyboard Either the keyboard ID or `Keyboard` instance
* @param purge If `true`, will also purge the `Keyboard` instance itself from the cache.
* @param keyboard Either the keyboard ID or `JSKeyboard` instance
* @param purge If `true`, will also purge the `JSKeyboard` instance itself from the cache.
* If `false`, only forgets the metadata (stubs).
*/
forgetKeyboard(keyboard: string | Keyboard, purge: boolean = false) {
const id: string = (keyboard instanceof Keyboard) ? keyboard.id : prefixed(keyboard);
forgetKeyboard(keyboard: string | JSKeyboard, purge: boolean = false) {
const id: string = (keyboard instanceof JSKeyboard) ? keyboard.id : prefixed(keyboard);
if(this.stubSetTable[id]) {
delete this.stubSetTable[id];

View file

@ -23,6 +23,7 @@ builder_describe \
"@/web/src/tools/testing/recorder-core test" \
"@/web/src/tools/es-bundling" \
"@/web/src/engine/common/web-utils" \
"@/web/src/engine/core-processor" \
configure \
clean \
build \
@ -44,7 +45,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 +74,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

@ -6,7 +6,7 @@
import { ModifierKeyConstants, USVirtualKeyCodes } from '@keymanapp/common-types';
const Codes = {
export const Codes = {
modifierCodes: {
// Debug-mode keyboards compiled before Keyman 18.0 referenced the `ModifierKeyConstants`
// constants via the names established below. We must continue to support them, as they're
@ -143,5 +143,3 @@ const Codes = {
return modifier;
}
}
export default Codes;

View file

@ -5,9 +5,9 @@
*/
import { ModifierKeyConstants } from '@keymanapp/common-types';
import Codes from './codes.js';
import type KeyEvent from './keyEvent.js';
import { type OutputTarget } from './outputTarget.interface.js';
import { Codes } from './codes.js';
import { type KeyEvent } from './keyEvent.js';
import { type OutputTargetInterface } from './outputTargetInterface.js';
export enum EmulationKeystrokes {
Enter = '\n',
@ -77,13 +77,13 @@ export default class DefaultRules {
}
/**
* Used when a RuleBehavior represents a non-text "command" within the Engine. This will generally
* Used when a ProcessorAction represents a non-text "command" within the Engine. This will generally
* trigger events that require context reset - often by moving the caret or by moving what OutputTarget
* the caret is in. However, we let those events perform the actual context reset.
*
* Note: is extended by DOM-aware KeymanWeb code.
*/
public applyCommand(Lkc: KeyEvent, outputTarget: OutputTarget): void {
public applyCommand(Lkc: KeyEvent, outputTarget: OutputTargetInterface): void {
// Notes for potential default-handling extensions:
//
// switch(code) {

View file

@ -1,14 +1,11 @@
export * from "./keyboards/activeLayout.js";
export * from "./keyboards/defaultLayouts.js";
export { default as Keyboard } from "./keyboards/keyboard.js";
export * from "./keyboards/keyboard.js";
export { ActiveKeyBase, ActiveKey, ActiveSubKey, ActiveRow, ActiveLayer, ActiveLayout } from "./keyboards/activeLayout.js";
export { ButtonClass, ButtonClasses, LayoutLayer, LayoutFormFactor, LayoutRow, LayoutKey, LayoutSubKey, Layouts } from "./keyboards/defaultLayouts.js";
export { JSKeyboard, LayoutState } from "./keyboards/jsKeyboard.js";
export { KeyboardMinimalInterface } from './keyboards/keyboardMinimalInterface.js';
export { KMXKeyboard } from './keyboards/kmxKeyboard.js';
export { KeyboardHarness, KeyboardKeymanGlobal, MinimalCodesInterface, MinimalKeymanGlobal } from "./keyboards/keyboardHarness.js";
export {
default as KeyboardLoaderBase,
KeyboardLoadErrorBuilder,
KeyboardMissingError,
KeyboardScriptError
} from "./keyboards/keyboardLoaderBase.js";
export { Keyboard, KeyboardLoaderBase } from "./keyboards/keyboardLoaderBase.js";
export { KeyboardLoadErrorBuilder, KeyboardMissingError, KeyboardScriptError, KeyboardDownloadError, InvalidKeyboardError } from './keyboards/keyboardLoadError.js'
export {
CloudKeyboardFont,
internalizeFont,
@ -21,17 +18,20 @@ export {
MetadataObj as RawKeyboardMetadata,
LanguageAPIPropertySpec
} from "./keyboards/keyboardProperties.js";
export { ProcessorAction as ProcessorAction } from "./keyboards/processorAction.js";
export { default as SpacebarText } from "./keyboards/spacebarText.js";
export { default as StateKeyMap } from "./keyboards/stateKeyMap.js";
export { StateKeyMap } from "./keyboards/stateKeyMap.js";
export { type Alternate, TextTransform } from "./keyboards/textTransform.js";
export { Transcription } from "./keyboards/transcription.js";
export { default as Codes } from "./codes.js";
export * from "./codes.js";
export { Codes } from "./codes.js";
export { default as DefaultRules } from "./defaultRules.js";
export * from "./defaultRules.js";
export { default as KeyEvent } from "./keyEvent.js";
export * from "./keyEvent.js";
export { type KeyDistribution, KeyEventSpec, KeyEvent } from "./keyEvent.js";
export { default as KeyMapping } from "./keyMapping.js";
export { OutputTarget } from "./outputTarget.interface.js";
export { OutputTargetInterface } from "./outputTargetInterface.js";
export { type SystemStoreMutationHandler, MutableSystemStore, SystemStore, SystemStoreIDs, type SystemStoreDictionary } from "./systemStore.js";
export { type VariableStore, VariableStoreSerializer, VariableStoreDictionary } from "./variableStore.js";
export * from "@keymanapp/web-utils";

View file

@ -6,10 +6,10 @@
// a key event. The most straightforward way to integrate Web OSK events on other platforms is to have
// other platforms recognize and utilize this type.
import type Keyboard from "./keyboards/keyboard.js";
import { type JSKeyboard } from "./keyboards/jsKeyboard.js";
import { type DeviceSpec } from "@keymanapp/web-utils";
import Codes from './codes.js';
import { Codes } from './codes.js';
import DefaultRules from "./defaultRules.js";
import { ActiveKeyBase } from './keyboards/activeLayout.js';
@ -44,7 +44,7 @@ export interface KeyEventSpec {
* guarantee that the keyboard instance known to the handler has not changed during JS execution
* since the user's interaction that raised the event.
*/
srcKeyboard?: Keyboard;
srcKeyboard?: JSKeyboard;
// Holds a generated fat-finger distribution (when appropriate)
keyDistribution?: KeyDistribution;
@ -64,7 +64,7 @@ export interface KeyEventSpec {
* This class is defined within its own file so that it can be loaded by code outside of KMW without
* having to actually load the entirety of KMW.
*/
export default class KeyEvent implements KeyEventSpec {
export class KeyEvent implements KeyEventSpec {
Lcode: number;
Lstates: number;
LmodifierChange?: boolean;
@ -85,7 +85,7 @@ export default class KeyEvent implements KeyEventSpec {
* guarantee that the keyboard instance known to the handler has not changed during JS execution
* since the user's interaction that raised the event.
*/
srcKeyboard?: Keyboard;
srcKeyboard?: JSKeyboard;
// Holds relevant event properties leading to construction of this KeyEvent.
source?: any; // Technically, KeyEvent|MouseEvent|Touch - but those are DOM types that must be kept out of headless mode.

View file

@ -3,7 +3,7 @@
Copyright 2019 SIL International
***/
import type KeyEvent from "./keyEvent.js";
import { KeyEvent } from "./keyEvent.js";
import { KeyEventSpec } from "./keyEvent.js";
class KeyMap {

View file

@ -1,9 +1,9 @@
import Codes from "../codes.js";
import KeyEvent, { KeyEventSpec } from "../keyEvent.js";
import { Codes } from "../codes.js";
import { KeyEvent, KeyEventSpec } from "../keyEvent.js";
import KeyMapping from "../keyMapping.js";
import { ButtonClasses, Layouts } from "./defaultLayouts.js";
import type { LayoutKey, LayoutSubKey, LayoutRow, LayoutLayer, LayoutFormFactor, ButtonClass } from "./defaultLayouts.js";
import type Keyboard from "./keyboard.js";
import { type JSKeyboard } from "./jsKeyboard.js";
import { TouchLayout } from "@keymanapp/common-types";
import TouchLayoutDefaultHint = TouchLayout.TouchLayoutDefaultHint;
@ -774,7 +774,7 @@ export class ActiveLayout implements LayoutFormFactor{
font: string;
keyLabels: boolean;
isDefault?: boolean;
keyboard: Keyboard;
keyboard: JSKeyboard;
formFactor: DeviceSpec.FormFactor;
defaultHint: TouchLayoutDefaultHint;
displayUnderlying?: boolean;
@ -849,7 +849,7 @@ export class ActiveLayout implements LayoutFormFactor{
* @param layout
* @param formFactor
*/
static polyfill(layout: TouchLayoutSpec, keyboard: Keyboard, formFactor: DeviceSpec.FormFactor): ActiveLayout {
static polyfill(layout: TouchLayoutSpec, keyboard: JSKeyboard, formFactor: DeviceSpec.FormFactor): ActiveLayout {
/* c8 ignore start */
if(layout == null) {
throw new Error("Cannot build an ActiveLayout for a null specification.");

View file

@ -19,8 +19,8 @@ import ButtonClasses = TouchLayout.TouchLayoutKeySp;
export { ButtonClasses };
import Codes from "../codes.js";
import type Keyboard from "./keyboard.js";
import { Codes } from "../codes.js";
import { type JSKeyboard } from "./jsKeyboard.js";
// The following types provide type definitions for the full JSON format we use for visual keyboard definitions.
export type ButtonClass = 0 | 1 | 2 | 3 | 4 | /*5 | 6 | 7 |*/ 8 | 9 | 10;
@ -87,11 +87,11 @@ export class Layouts {
* Build a default layout for keyboards with no explicit layout
*
* @param {Object} PVK raw specifications
* @param {Keyboard} keyboard keyboard object (as loaded)
* @param {JSKeyboard} keyboard keyboard object (as loaded)
* @param {string} formFactor (really utils.FormFactor)
* @return {LayoutFormFactor}
*/
static buildDefaultLayout(PVK: EncodedVisualKeyboard, keyboard: Keyboard, formFactor: string): LayoutFormFactor {
static buildDefaultLayout(PVK: EncodedVisualKeyboard, keyboard: JSKeyboard, formFactor: string): LayoutFormFactor {
// Build a layout using the default for the device
let layoutType = formFactor as keyof TouchLayout.TouchLayoutFile;

View file

@ -1,9 +1,10 @@
import Codes from "../codes.js";
import { Codes } from "../codes.js";
import { Layouts } from "./defaultLayouts.js";
import { ActiveKey, ActiveLayout, ActiveSubKey } from "./activeLayout.js";
import KeyEvent from "../keyEvent.js";
import { type OutputTarget } from "../outputTarget.interface.js";
import { KeyEvent } from "../keyEvent.js";
import { type OutputTargetInterface } from "../outputTargetInterface.js";
import { KeymanWebKeyboard, ModifierKeyConstants, TouchLayout } from "@keymanapp/common-types";
import { VariableStoreDictionary } from "../variableStore.js";
import ComplexKeyboardStore = KeymanWebKeyboard.ComplexKeyboardStore;
import KeyboardObject = KeymanWebKeyboard.KeyboardObject;
@ -12,7 +13,7 @@ import LayoutSpec = KeymanWebKeyboard.LayoutSpec;
type TouchLayoutSpec = TouchLayout.TouchLayoutPlatform & { isDefault?: boolean};
import { Version, DeviceSpec } from "@keymanapp/web-utils";
import StateKeyMap from "./stateKeyMap.js";
import { StateKeyMap } from "./stateKeyMap.js";
/**
* Stores preprocessed properties of a keyboard for quick retrieval later.
@ -30,10 +31,6 @@ export enum LayoutState {
POLYFILLED = 1
}
export interface VariableStoreDictionary {
[name: string]: string;
};
type KmwKeyboardObject = KeyboardObject & {
/**
* Used internally by Keyman Engine for Web to hold preprocessed stores.
@ -51,9 +48,9 @@ type KmwKeyboardObject = KeyboardObject & {
* and keyboard-centered functionality in an object-oriented way without modifying the
* wrapped keyboard itself.
*/
export default class Keyboard {
export class JSKeyboard {
public static DEFAULT_SCRIPT_OBJECT: KmwKeyboardObject = {
'gs': function(outputTarget: OutputTarget, keystroke: KeyEvent) { return false; }, // no matching rules; rely on defaultRuleOutput entirely
'gs': function(outputTarget: OutputTargetInterface, keystroke: KeyEvent) { return false; }, // no matching rules; rely on defaultRuleOutput entirely
'KI': '', // The currently-existing default keyboard ID; we already have checks that focus against this.
'KN': '',
'KV': Layouts.DEFAULT_RAW_SPEC,
@ -73,7 +70,7 @@ export default class Keyboard {
if(keyboardScript) {
this.scriptObject = keyboardScript;
} else {
this.scriptObject = Keyboard.DEFAULT_SCRIPT_OBJECT;
this.scriptObject = JSKeyboard.DEFAULT_SCRIPT_OBJECT;
}
this.layoutStates = {};
}
@ -81,26 +78,26 @@ export default class Keyboard {
/**
* Calls the keyboard's `gs` function, which represents the keyboard source's begin Unicode group.
*/
process(outputTarget: OutputTarget, keystroke: KeyEvent): boolean {
process(outputTarget: OutputTargetInterface, keystroke: KeyEvent): boolean {
return this.scriptObject['gs'](outputTarget, keystroke);
}
/**
* Calls the keyboard's `gn` function, which represents the keyboard source's begin newContext group.
*/
processNewContextEvent(outputTarget: OutputTarget, keystroke: KeyEvent): boolean {
processNewContextEvent(outputTarget: OutputTargetInterface, keystroke: KeyEvent): boolean {
return this.scriptObject['gn'] ? this.scriptObject['gn'](outputTarget, keystroke) : false;
}
/**
* Calls the keyboard's `gpk` function, which represents the keyboard source's begin postKeystroke group.
*/
processPostKeystroke(outputTarget: OutputTarget, keystroke: KeyEvent): boolean {
processPostKeystroke(outputTarget: OutputTargetInterface, keystroke: KeyEvent): boolean {
return this.scriptObject['gpk'] ? this.scriptObject['gpk'](outputTarget, keystroke) : false;
}
get isHollow(): boolean {
return this.scriptObject == Keyboard.DEFAULT_SCRIPT_OBJECT;
return this.scriptObject == JSKeyboard.DEFAULT_SCRIPT_OBJECT;
}
get id(): string {
@ -362,7 +359,7 @@ export default class Keyboard {
* @param {number} _PData 1 or 0
* Notifies keyboard of keystroke or other event
*/
notify(_PCommand: number, _PTarget: OutputTarget, _PData: number) { // I2187
notify(_PCommand: number, _PTarget: OutputTargetInterface, _PData: number) { // I2187
// Good example use case - the Japanese CJK-picker keyboard
if(typeof(this.scriptObject['KNS']) == 'function') {
this.scriptObject['KNS'](_PCommand, _PTarget, _PData);

View file

@ -1,5 +1,5 @@
import Keyboard from "./keyboard.js";
import Codes from "../codes.js";
import { JSKeyboard } from "./jsKeyboard.js";
import { Codes } from "../codes.js";
import { DeviceSpec } from '@keymanapp/web-utils';
/**
@ -64,7 +64,7 @@ export class KeyboardHarness {
/**
* This field serves as the receptacle for a successfully-loaded Keyboard.
*/
public loadedKeyboard: Keyboard = null;
public loadedKeyboard: JSKeyboard = null;
/**
* Keyman keyboards register themselves into the Keyman Engine for Web by directly
@ -84,7 +84,7 @@ export class KeyboardHarness {
if(this.loadedKeyboard) {
throw new Error("Unexpected state: the most-recently loaded keyboard field was not properly reset.");
}
this.loadedKeyboard = new Keyboard(scriptObject);
this.loadedKeyboard = new JSKeyboard(scriptObject);
}
// Is evaluated on script-load for some keyboards using variable stores.

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,14 @@
import Keyboard from "./keyboard.js";
import { KM_Core, KM_CORE_STATUS } from 'keyman/engine/core-processor';
import { JSKeyboard } from "./jsKeyboard.js";
import { KMXKeyboard } from './kmxKeyboard.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 type Keyboard = JSKeyboard | KMXKeyboard;
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 +19,58 @@ 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 isKMXKeyboard(byteArray: Uint8Array): boolean {
// Check if the first 4 bytes are 'KXTS' (0x4b, 0x58, 0x54, 0x53)
return byteArray.length >= 4 && byteArray[0] === 0x4b &&
byteArray[1] === 0x58 && byteArray[2] === 0x54 && byteArray[3] === 0x53;
}
private async loadKeyboardInternal(uri: string, errorBuilder: KeyboardLoadErrorBuilder): Promise<Keyboard> {
const byteArray = await this.loadKeyboardBlob(uri, errorBuilder);
if (this.isKMXKeyboard(byteArray)) {
// KMX or LDML (KMX+) keyboard
const result = KM_Core.instance.keyboard_load_from_blob(uri, byteArray);
if (result.status == KM_CORE_STATUS.OK) {
return new KMXKeyboard(result.object);
}
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<JSKeyboard>;
}

View file

@ -0,0 +1,5 @@
import { Keyboard } from './keyboardLoaderBase.js';
export interface KeyboardMinimalInterface {
activeKeyboard: Keyboard;
}

View file

@ -0,0 +1,39 @@
import { KM_Core, km_core_keyboard } from 'keyman/engine/core-processor';
/**
* Acts as a wrapper class for KMX(+) Keyman keyboards
*/
export class KMXKeyboard {
constructor(private _keyboard: km_core_keyboard) {
}
shutdown() {
if (this._keyboard) {
KM_Core.instance.keyboard_dispose(this._keyboard);
this._keyboard = null;
}
}
get isMnemonic(): boolean {
return false;
}
get version(): string {
const attrs = KM_Core.instance.keyboard_get_attrs(this._keyboard);
const version = attrs.object.version_string;
attrs.delete();
return version;
}
get id(): string {
const attrs = KM_Core.instance.keyboard_get_attrs(this._keyboard);
const id = attrs.object.id;
attrs.delete();
return id;
}
get keyboard(): km_core_keyboard {
return this._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 { JSKeyboard } from '../jsKeyboard.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<JSKeyboard> {
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 { JSKeyboard } from '../jsKeyboard.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<JSKeyboard> {
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

@ -0,0 +1,68 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
import { LexicalModelTypes } from '@keymanapp/common-types';
import { Transcription } from './transcription.js';
import { VariableStore, VariableStoreDictionary } from '../variableStore.js';
import { SystemStoreDictionary } from '../systemStore.js';
/**
* Represents the commands and state changes that result from a matched keyboard rule.
*/
export class ProcessorAction {
/**
* The before-and-after Transform from matching a keyboard rule. May be `null`
* if no keyboard rules were matched for the keystroke.
*/
transcription: Transcription = null;
/**
* Indicates whether or not a BEEP command was issued by the matched keyboard rule.
*/
beep?: boolean;
/**
* A set of changed store values triggered by the matched keyboard rule.
*/
setStore: SystemStoreDictionary = {};
/**
* A set of variable stores with save requests triggered by the matched keyboard rule
*/
saveStore: {[name: string]: VariableStore} = {};
/**
* A set of variable stores with possible changes to be applied during finalization.
*/
variableStores: VariableStoreDictionary = {};
/**
* Denotes a non-output default behavior; this should be evaluated later, against the true keystroke.
*/
triggersDefaultCommand: boolean = false;
/**
* Denotes error log messages generated when attempting to generate this behavior.
*/
errorLog?: string;
/**
* Denotes warning log messages generated when attempting to generate this behavior.
*/
warningLog?: string;
/**
* If predictive text is active, contains a Promise returning predictive Suggestions.
*/
predictionPromise?: Promise<LexicalModelTypes.Suggestion[]>;
/**
* In reference to https://github.com/keymanapp/keyman/pull/4350#issuecomment-768753852:
*
* If the final group processed is a context and keystroke group (using keys),
* and there is no nomatch rule, and the keystroke is not matched in the group,
* the keystroke's default behavior should trigger, regardless of whether or not any
* rules in prior groups matched.
*/
triggerKeyDefault?: boolean;
}

View file

@ -2,8 +2,8 @@
* Provided by keyman/engine/keyboard's `KeyboardProcessor` class and utilized by the OSK
* to provide state feedback on any corresponding keys visible in the OSK.
*/
export default interface StateKeyMap {
export interface StateKeyMap {
K_CAPS: boolean,
K_NUMLOCK: boolean,
K_SCROLL: boolean
}
}

View file

@ -0,0 +1,23 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
import { LexicalModelTypes } from '@keymanapp/common-types';
export type Alternate = LexicalModelTypes.ProbabilityMass<LexicalModelTypes.Transform>;
export class TextTransform implements LexicalModelTypes.Transform {
readonly insert: string;
readonly deleteLeft: number;
readonly deleteRight: number;
readonly erasedSelection: boolean;
id: number;
constructor(insert: string, deleteLeft: number, deleteRight: number, erasedSelection: boolean) {
this.insert = insert;
this.deleteLeft = deleteLeft;
this.deleteRight = deleteRight;
this.erasedSelection = erasedSelection;
}
public static readonly nil = new TextTransform('', 0, 0, false);
}

View file

@ -0,0 +1,35 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
import { OutputTargetInterface } from '../outputTargetInterface.js';
import { KeyEvent } from '../keyEvent.js';
import { Alternate, TextTransform } from './textTransform.js';
export class Transcription {
readonly token: number;
readonly keystroke: KeyEvent;
readonly transform: TextTransform;
alternates: Alternate[]; // constructed after the rest of the transcription.
readonly preInput: OutputTargetInterface;
private static tokenSeed: number = 0;
constructor(keystroke: KeyEvent, transform: TextTransform, preInput: OutputTargetInterface, alternates?: Alternate[]) {
const token = this.token = Transcription.tokenSeed++;
this.keystroke = keystroke;
this.transform = transform;
this.alternates = alternates;
this.preInput = preInput;
this.transform.id = this.token;
// Assign the ID to each alternate, as well.
if (alternates) {
alternates.forEach(function (alt) {
alt.sample.id = token;
});
}
}
}

View file

@ -1,10 +1,11 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
export interface OutputTarget {
export interface OutputTargetInterface {
/**
* Signifies that this OutputTarget has no default key processing behaviors. This should be false
* for OutputTargets backed by web elements like HTMLInputElement or HTMLTextAreaElement.
* Signifies that this OutputTargetInterface has no default key processing behaviors.
* This should be false for OutputTargets backed by web elements like HTMLInputElement
* or HTMLTextAreaElement.
*/
get isSynthetic(): boolean;

View file

@ -0,0 +1,71 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Declaration of a 'system store' (see kmn reference)
*/
export enum SystemStoreIDs {
TSS_LAYER = 33,
TSS_PLATFORM = 31,
TSS_NEWLAYER = 42,
TSS_OLDLAYER = 43
}
export type SystemStoreDictionary = { [id: number]: string };
/**
* Defines common behaviors associated with system stores.
*/
export abstract class SystemStore {
public readonly id: number;
constructor(id: number) {
this.id = id;
}
abstract matches(value: string): boolean;
public set(_value: string): void {
throw new Error("System store with ID " + this.id + " may not be directly set.");
}
}
/**
* A handler designed to receive feedback whenever a system store's value is changed.
* @param source The system store being mutated, before the value change occurs.
* @param newValue The new value being set
* @returns `false` / `undefined` to allow the change, `true` to block the change.
*/
export type SystemStoreMutationHandler = (source: MutableSystemStore, newValue: string) => boolean;
export class MutableSystemStore extends SystemStore {
private _value: string;
public handler?: SystemStoreMutationHandler = null;
constructor(id: number, defaultValue: string) {
super(id);
this._value = defaultValue;
}
public get value() {
return this._value;
}
public matches(value: string) {
return this._value == value;
}
public set(value: string) {
// Even if things stay the same, we should still signal this.
// It's important for tracking if a rule directly set the layer
// versus if it passively remained.
if(this.handler) {
if(this.handler(this, value)) {
return;
}
}
this._value = value;
}
}

View file

@ -0,0 +1,16 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Definitions for variable stores (see kmn reference)
*/
export interface VariableStoreDictionary {
[name: string]: string;
};
export type VariableStore = { [name: string]: string; };
export interface VariableStoreSerializer {
loadStore(keyboardID: string, storeName: string): VariableStore;
saveStore(keyboardID: string, storeName: string, storeMap: VariableStore): void;
}

Some files were not shown because too many files have changed in this diff Show more