mirror of
https://github.com/keymanapp/keyman.git
synced 2026-10-31 02:07:49 +00:00
Merge branch 'epic/web-core' into chore/A19S12-merge-master-into-web-core
This commit is contained in:
commit
a56110950f
172 changed files with 4444 additions and 1367 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
185
core/src/layout.hpp
Normal 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
|
||||
|
|
@ -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
288
core/src/wasm.cpp
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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] )
|
||||
|
|
|
|||
|
|
@ -53,3 +53,4 @@ subdir('utftest')
|
|||
subdir('kmnkbd')
|
||||
subdir('kmx')
|
||||
subdir('ldml')
|
||||
subdir('wasm')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
5
core/tests/unit/wasm/meson.build
Normal file
5
core/tests/unit/wasm/meson.build
Normal 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
1425
core/tests/unit/wasm/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
16
core/tests/unit/wasm/package.json
Normal file
16
core/tests/unit/wasm/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
149
core/tests/unit/wasm/wasm.tests.js
Normal file
149
core/tests/unit/wasm/wasm.tests.js
Normal 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 ]');
|
||||
});
|
||||
});
|
||||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
1
package-lock.json
generated
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
12
web/build.sh
12
web/build.sh
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 != '-') {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
1
web/src/engine/core-processor/.gitignore
vendored
Normal file
1
web/src/engine/core-processor/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
src/import/
|
||||
64
web/src/engine/core-processor/build.sh
Executable file
64
web/src/engine/core-processor/build.sh
Executable 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
|
||||
43
web/src/engine/core-processor/src/KM_Core.ts
Normal file
43
web/src/engine/core-processor/src/KM_Core.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
3
web/src/engine/core-processor/src/index.ts
Normal file
3
web/src/engine/core-processor/src/index.ts
Normal 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 };
|
||||
13
web/src/engine/core-processor/tsconfig.json
Normal file
13
web/src/engine/core-processor/tsconfig.json
Normal 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" ],
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
||||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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")) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}());
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
84
web/src/engine/js-processor/src/platformSystemStore.ts
Normal file
84
web/src/engine/js-processor/src/platformSystemStore.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
20
web/src/engine/js-processor/src/stores.ts
Normal file
20
web/src/engine/js-processor/src/stores.ts
Normal 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;
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,5 +9,8 @@
|
|||
"rootDir": "./src"
|
||||
},
|
||||
|
||||
"references": [
|
||||
{ "path": "../keyboard"},
|
||||
],
|
||||
"include": [ "**/*.ts" ],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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[] = [];
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
104
web/src/engine/keyboard/src/keyboards/keyboardLoadError.ts
Normal file
104
web/src/engine/keyboard/src/keyboards/keyboardLoadError.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { Keyboard } from './keyboardLoaderBase.js';
|
||||
|
||||
export interface KeyboardMinimalInterface {
|
||||
activeKeyboard: Keyboard;
|
||||
}
|
||||
39
web/src/engine/keyboard/src/keyboards/kmxKeyboard.ts
Normal file
39
web/src/engine/keyboard/src/keyboards/kmxKeyboard.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
68
web/src/engine/keyboard/src/keyboards/processorAction.ts
Normal file
68
web/src/engine/keyboard/src/keyboards/processorAction.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
23
web/src/engine/keyboard/src/keyboards/textTransform.ts
Normal file
23
web/src/engine/keyboard/src/keyboards/textTransform.ts
Normal 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);
|
||||
}
|
||||
35
web/src/engine/keyboard/src/keyboards/transcription.ts
Normal file
35
web/src/engine/keyboard/src/keyboards/transcription.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
|
||||
71
web/src/engine/keyboard/src/systemStore.ts
Normal file
71
web/src/engine/keyboard/src/systemStore.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
16
web/src/engine/keyboard/src/variableStore.ts
Normal file
16
web/src/engine/keyboard/src/variableStore.ts
Normal 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
Loading…
Add table
Reference in a new issue