mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-05 00:15:32 +00:00
chore: Merge remote-tracking branch 'origin/epic/web-core' into chore/merge-master-into-web-core
This commit is contained in:
commit
e6cf6186c2
49 changed files with 1020 additions and 220 deletions
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
|
||||
|
|
@ -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,16 @@ mock_files = files(
|
|||
'mock/mock_processor.cpp',
|
||||
)
|
||||
|
||||
if cpp_compiler.get_id() == 'emscripten'
|
||||
host_links = ['--whole-archive', '-sALLOW_MEMORY_GROWTH=1', '-sMODULARIZE=1',
|
||||
'-sEXPORT_ES6', '-sENVIRONMENT=web,webview',
|
||||
'--emit-tsd', 'km-core-interface.d.ts', '-sERROR_ON_UNDEFINED_SYMBOLS=0']
|
||||
|
||||
links += ['-sEXPORTED_RUNTIME_METHODS=[\'UTF8ToString\',\'stringToNewUTF8\',\'wasmExports\']',
|
||||
# Forcing inclusion of debug symbols
|
||||
'-g', '-Wlimited-postlink-optimizations', '--bind']
|
||||
endif
|
||||
|
||||
lib = library('keymancore',
|
||||
api_files,
|
||||
core_files,
|
||||
|
|
@ -164,3 +175,24 @@ pkg.generate(
|
|||
description: 'Keyman processor for KMN keyboards.',
|
||||
subdirs: headerdirs,
|
||||
libraries: lib)
|
||||
|
||||
if cpp_compiler.get_id() == 'emscripten'
|
||||
# Build an executable
|
||||
host = executable('km-core',
|
||||
cpp_args: defns,
|
||||
include_directories: inc,
|
||||
link_args: links + host_links,
|
||||
objects: lib.extract_all_objects(recursive: false))
|
||||
|
||||
if get_option('buildtype') == 'release'
|
||||
# TODO: #12888
|
||||
# Split debug symbols into separate wasm file for release builds only
|
||||
# as the release symbols will be uploaded to sentry
|
||||
# custom_target('core.wasm',
|
||||
# depends: host,
|
||||
# input: host,
|
||||
# output: 'core.wasm',
|
||||
# command: ['wasm-split', '@OUTDIR@/core.wasm', '-o', '@OUTPUT@', '--strip', '--debug-out=@OUTDIR@/core.debug.wasm'],
|
||||
# build_by_default: true)
|
||||
endif
|
||||
endif
|
||||
|
|
|
|||
44
core/src/wasm.cpp
Normal file
44
core/src/wasm.cpp
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
#ifdef __EMSCRIPTEN__
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten/emscripten.h>
|
||||
#include <emscripten/bind.h>
|
||||
|
||||
#else
|
||||
#define EMSCRIPTEN_KEEPALIVE
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
#define EXTERN extern "C" EMSCRIPTEN_KEEPALIVE
|
||||
#else
|
||||
#define EXTERN EMSCRIPTEN_KEEPALIVE
|
||||
#endif
|
||||
|
||||
#include <keyman_core.h>
|
||||
|
||||
constexpr km_core_attr const engine_attrs = {
|
||||
256,
|
||||
KM_CORE_LIB_CURRENT,
|
||||
KM_CORE_LIB_AGE,
|
||||
KM_CORE_LIB_REVISION,
|
||||
KM_CORE_TECH_KMX,
|
||||
"SIL International"
|
||||
};
|
||||
|
||||
EMSCRIPTEN_KEEPALIVE km_core_attr const & tmp_wasm_attributes() {
|
||||
return engine_attrs;
|
||||
}
|
||||
|
||||
EMSCRIPTEN_BINDINGS(core_interface) {
|
||||
|
||||
emscripten::value_object<km_core_attr>("km_core_attr")
|
||||
.field("max_context", &km_core_attr::max_context)
|
||||
.field("current", &km_core_attr::current)
|
||||
.field("revision", &km_core_attr::revision)
|
||||
.field("age", &km_core_attr::age)
|
||||
.field("technology", &km_core_attr::technology)
|
||||
//.field("vendor", &km_core_attr::vendor, emscripten::allow_raw_pointers())
|
||||
;
|
||||
|
||||
emscripten::function("tmp_wasm_attributes", &tmp_wasm_attributes);
|
||||
}
|
||||
#endif
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -8763,6 +8763,7 @@
|
|||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz",
|
||||
"integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
|
|
@ -15649,6 +15650,7 @@
|
|||
"@sentry/cli": "^2.31.0",
|
||||
"@zip.js/zip.js": "^2.7.32",
|
||||
"c8": "^7.12.0",
|
||||
"express": "^4.19.2",
|
||||
"jsdom": "^23.0.1",
|
||||
"mocha": "^10.0.0",
|
||||
"tsx": "^4.19.0"
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ KEYMAN_MIN_TARGET_VERSION_WEB_SAFARI=13.0 # iOS 13.0, macOS 10.13.6+
|
|||
# Dependency versions
|
||||
KEYMAN_MIN_VERSION_NODE_MAJOR=20 # node version source of truth is /package.json:/engines/node; use KEYMAN_USE_NVM to automatically update
|
||||
KEYMAN_MIN_VERSION_NPM=10.5.1 # 10.5.0 has bug, discussed in #10350
|
||||
KEYMAN_MIN_VERSION_EMSCRIPTEN=3.1.58 # Use KEYMAN_USE_EMSDK to automatically update to this version
|
||||
KEYMAN_MIN_VERSION_EMSCRIPTEN=3.1.64 # Use KEYMAN_USE_EMSDK to automatically update to this version
|
||||
KEYMAN_MIN_VERSION_VISUAL_STUDIO=2019
|
||||
KEYMAN_MIN_VERSION_MESON=1.0.0
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -20,12 +20,14 @@ builder_describe "Builds engine modules for Keyman Engine for Web (KMW)." \
|
|||
"clean" \
|
||||
"configure" \
|
||||
"build" \
|
||||
"start Starts the test server" \
|
||||
"test" \
|
||||
"coverage Create an HTML page with code coverage" \
|
||||
":app/browser The form of Keyman Engine for Web for use on websites" \
|
||||
":app/webview A puppetable version of KMW designed for use in a host app's WebView" \
|
||||
":app/ui Builds KMW's desktop form-factor keyboard-selection UI modules" \
|
||||
":engine/attachment Subset used for detecting valid page contexts for use in text editing " \
|
||||
":engine/core-processor Keyman Core WASM integration" \
|
||||
":engine/dom-utils A common subset of function used for DOM calculations, layout, etc" \
|
||||
":engine/events Specialized classes utilized to support KMW API events" \
|
||||
":engine/element-wrappers Subset used to integrate with website elements" \
|
||||
|
|
@ -60,6 +62,7 @@ builder_describe_outputs \
|
|||
build:app/webview "/web/build/app/webview/${config}/keymanweb-webview.js" \
|
||||
build:app/ui "/web/build/app/ui/${config}/kmwuitoggle.js" \
|
||||
build:engine/attachment "/web/build/engine/attachment/lib/index.mjs" \
|
||||
build:engine/core-processor "/web/build/engine/core-processor/lib/index.mjs" \
|
||||
build:engine/dom-utils "/web/build/engine/dom-utils/obj/index.js" \
|
||||
build:engine/events "/web/build/engine/events/lib/index.mjs" \
|
||||
build:engine/element-wrappers "/web/build/engine/element-wrappers/lib/index.mjs" \
|
||||
|
|
@ -166,6 +169,8 @@ builder_run_child_actions build:engine/attachment
|
|||
# Uses engine/interfaces (due to resource-path config interface)
|
||||
builder_run_child_actions build:engine/keyboard-storage
|
||||
|
||||
builder_run_child_actions build:engine/core-processor
|
||||
|
||||
# Uses engine/interfaces, engine/keyboard-storage, & engine/osk
|
||||
builder_run_child_actions build:engine/main
|
||||
|
||||
|
|
@ -201,3 +206,6 @@ builder_run_action test:help do_test_help
|
|||
|
||||
# Create coverage report
|
||||
builder_run_action coverage:_all coverage_action
|
||||
|
||||
# Start the test server
|
||||
builder_run_action start node src/tools/testing/test-server/index.cjs
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@
|
|||
"types": "./build/engine/attachment/obj/index.d.ts",
|
||||
"import": "./build/engine/attachment/obj/index.js"
|
||||
},
|
||||
"./engine/interfaces": {
|
||||
"es6-bundling": "./src/engine/interfaces/src/index.ts",
|
||||
"types": "./build/engine/interfaces/obj/index.d.ts",
|
||||
"import": "./build/engine/interfaces/obj/index.js"
|
||||
"./engine/core-processor": {
|
||||
"es6-bundling": "./src/engine/core-processor/src/index.ts",
|
||||
"types": "./build/engine/core-processor/obj/index.d.ts",
|
||||
"import": "./build/engine/core-processor/obj/index.js"
|
||||
},
|
||||
"./engine/dom-utils": {
|
||||
"es6-bundling": "./src/engine/dom-utils/src/index.ts",
|
||||
|
|
@ -32,6 +32,11 @@
|
|||
"types": "./build/engine/events/obj/index.d.ts",
|
||||
"import": "./build/engine/events/obj/index.js"
|
||||
},
|
||||
"./engine/interfaces": {
|
||||
"es6-bundling": "./src/engine/interfaces/src/index.ts",
|
||||
"types": "./build/engine/interfaces/obj/index.d.ts",
|
||||
"import": "./build/engine/interfaces/obj/index.js"
|
||||
},
|
||||
"./engine/js-processor": {
|
||||
"es6-bundling": "./src/engine/js-processor/src/index.ts",
|
||||
"types": "./build/engine/js-processor/obj/index.d.ts",
|
||||
|
|
@ -81,6 +86,10 @@
|
|||
"./engine/osk/internals": {
|
||||
"types": "./build/engine/osk/obj/test-index.d.ts",
|
||||
"import": "./build/engine/osk/obj/test-index.js"
|
||||
},
|
||||
"./tools/testing/test-utils": {
|
||||
"types": "./build/tools/testing/test-utils/obj/index.d.ts",
|
||||
"import": "./build/tools/testing/test-utils/obj/index.js"
|
||||
}
|
||||
},
|
||||
"imports": {
|
||||
|
|
@ -108,6 +117,7 @@
|
|||
"@sentry/cli": "^2.31.0",
|
||||
"@zip.js/zip.js": "^2.7.32",
|
||||
"c8": "^7.12.0",
|
||||
"express": "^4.19.2",
|
||||
"jsdom": "^23.0.1",
|
||||
"mocha": "^10.0.0",
|
||||
"tsx": "^4.19.0"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
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.inc.sh"
|
||||
## END STANDARD BUILD SCRIPT INCLUDE
|
||||
|
||||
SUBPROJECT_NAME=engine/core-processor
|
||||
|
||||
. "${KEYMAN_ROOT}/web/common.inc.sh"
|
||||
. "${KEYMAN_ROOT}/resources/shellHelperFunctions.sh"
|
||||
|
||||
# ################################ Main script ################################
|
||||
|
||||
builder_describe "Keyman Core WASM integration" \
|
||||
"@/core:wasm" \
|
||||
"@/web/src/engine/common/web-utils" \
|
||||
"clean" \
|
||||
"configure" \
|
||||
"build" \
|
||||
"test" \
|
||||
"--ci+ Set to utilize CI-based test configurations & reporting."
|
||||
|
||||
builder_describe_outputs \
|
||||
configure "/web/src/engine/core-processor/src/import/core/km-core-interface.d.ts" \
|
||||
build "/web/build/${SUBPROJECT_NAME}/lib/index.mjs"
|
||||
|
||||
builder_parse "$@"
|
||||
|
||||
#### Build action definitions ####
|
||||
|
||||
do_clean() {
|
||||
rm -rf "${KEYMAN_ROOT}/web/build/${SUBPROJECT_NAME}"
|
||||
rm -rf "src/import/"
|
||||
}
|
||||
|
||||
do_configure() {
|
||||
verify_npm_setup
|
||||
|
||||
mkdir -p "src/import/core/"
|
||||
# we don't need this file for release builds, but it's nice to have
|
||||
# for reference and auto-completion
|
||||
cp "${KEYMAN_ROOT}/core/build/wasm/${BUILDER_CONFIGURATION}/src/km-core-interface.d.ts" "src/import/core/"
|
||||
}
|
||||
|
||||
copy_deps() {
|
||||
mkdir -p "${KEYMAN_ROOT}/web/build/${SUBPROJECT_NAME}/obj/import/core/"
|
||||
cp "${KEYMAN_ROOT}/core/build/wasm/${BUILDER_CONFIGURATION}/src/"km-core-interface.d.ts "${KEYMAN_ROOT}/web/build/${SUBPROJECT_NAME}/obj/import/core/"
|
||||
cp "${KEYMAN_ROOT}/core/build/wasm/${BUILDER_CONFIGURATION}/src/"km-core{.js,.wasm} "${KEYMAN_ROOT}/web/build/${SUBPROJECT_NAME}/obj/import/core/"
|
||||
}
|
||||
|
||||
do_build () {
|
||||
copy_deps
|
||||
compile "${SUBPROJECT_NAME}"
|
||||
|
||||
${BUNDLE_CMD} "${KEYMAN_ROOT}/web/build/${SUBPROJECT_NAME}/obj/index.js" \
|
||||
--out "${KEYMAN_ROOT}/web/build/${SUBPROJECT_NAME}/lib/index.mjs" \
|
||||
--format esm
|
||||
}
|
||||
|
||||
builder_run_action clean do_clean
|
||||
builder_run_action configure do_configure
|
||||
builder_run_action build do_build
|
||||
30
web/src/engine/core-processor/src/core-processor.ts
Normal file
30
web/src/engine/core-processor/src/core-processor.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
type km_core_attr = import('./import/core/km-core-interface.js').km_core_attr;
|
||||
|
||||
export class CoreProcessor {
|
||||
private instance: any;
|
||||
|
||||
/**
|
||||
* Initialize Core Processor
|
||||
* @param baseurl - The url where km-core.js is located
|
||||
*/
|
||||
public async init(baseurl: string): Promise<boolean> {
|
||||
|
||||
if (!this.instance) {
|
||||
try {
|
||||
const module = await import(baseurl + '/km-core.js');
|
||||
this.instance = await module.default({
|
||||
locateFile: function (path: string, scriptDirectory: string) {
|
||||
return baseurl + '/' + path;
|
||||
}
|
||||
});
|
||||
} catch (e: any) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return !!this.instance;
|
||||
};
|
||||
|
||||
public tmp_wasm_attributes(): km_core_attr {
|
||||
return this.instance.tmp_wasm_attributes();
|
||||
}
|
||||
}
|
||||
1
web/src/engine/core-processor/src/index.ts
Normal file
1
web/src/engine/core-processor/src/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './core-processor.js';
|
||||
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" ],
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,12 @@ do_build () {
|
|||
--format esm
|
||||
}
|
||||
|
||||
do_test() {
|
||||
test-headless "${SUBPROJECT_NAME}" ""
|
||||
test-headless-typescript "${SUBPROJECT_NAME}"
|
||||
}
|
||||
|
||||
builder_run_action configure verify_npm_setup
|
||||
builder_run_action clean rm -rf "${KEYMAN_ROOT}/web/build/${SUBPROJECT_NAME}"
|
||||
builder_run_action build do_build
|
||||
builder_run_action test test-headless "${SUBPROJECT_NAME}" ""
|
||||
builder_run_action test do_test
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ function do_configure() {
|
|||
|
||||
BUILD_DIR="${KEYMAN_ROOT}/web/build/${SUBPROJECT_NAME}"
|
||||
|
||||
function do_build() {
|
||||
do_build() {
|
||||
tsc --build "${THIS_SCRIPT_PATH}/tsconfig.all.json"
|
||||
|
||||
# Base product - the main keyboard processor
|
||||
|
|
@ -73,7 +73,12 @@ function do_build() {
|
|||
tsc --emitDeclarationOnly --outFile "${BUILD_DIR}/lib/node-keyboard-loader.d.ts" -p src/keyboards/loaders/tsconfig.node.json
|
||||
}
|
||||
|
||||
do_test() {
|
||||
test-headless "${SUBPROJECT_NAME}" ""
|
||||
test-headless-typescript "${SUBPROJECT_NAME}"
|
||||
}
|
||||
|
||||
builder_run_action configure do_configure
|
||||
builder_run_action clean rm -rf "${BUILD_DIR}"
|
||||
builder_run_action build do_build
|
||||
builder_run_action test test-headless "${SUBPROJECT_NAME}" ""
|
||||
builder_run_action test do_test
|
||||
|
|
|
|||
|
|
@ -3,12 +3,8 @@ export * from "./keyboards/defaultLayouts.js";
|
|||
export { default as Keyboard } from "./keyboards/keyboard.js";
|
||||
export * from "./keyboards/keyboard.js";
|
||||
export { KeyboardHarness, KeyboardKeymanGlobal, MinimalCodesInterface, MinimalKeymanGlobal } from "./keyboards/keyboardHarness.js";
|
||||
export {
|
||||
default as KeyboardLoaderBase,
|
||||
KeyboardLoadErrorBuilder,
|
||||
KeyboardMissingError,
|
||||
KeyboardScriptError
|
||||
} from "./keyboards/keyboardLoaderBase.js";
|
||||
export { KeyboardLoaderBase } from "./keyboards/keyboardLoaderBase.js";
|
||||
export { KeyboardLoadErrorBuilder, KeyboardMissingError, KeyboardScriptError, KeyboardDownloadError, InvalidKeyboardError } from './keyboards/keyboardLoadError.js'
|
||||
export {
|
||||
CloudKeyboardFont,
|
||||
internalizeFont,
|
||||
|
|
|
|||
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,11 @@
|
|||
import Keyboard from "./keyboard.js";
|
||||
import { KeyboardHarness } from "./keyboardHarness.js";
|
||||
import KeyboardProperties from "./keyboardProperties.js";
|
||||
import { KeyboardLoadErrorBuilder, StubBasedErrorBuilder, UriBasedErrorBuilder } from './keyboardLoadError.js';
|
||||
|
||||
type KeyboardStub = KeyboardProperties & { filename: string };
|
||||
export type KeyboardStub = KeyboardProperties & { filename: string };
|
||||
|
||||
export interface KeyboardLoadErrorBuilder {
|
||||
scriptError(err?: Error): void;
|
||||
missingError(err: Error): void;
|
||||
}
|
||||
|
||||
export class KeyboardScriptError extends Error {
|
||||
public readonly cause;
|
||||
|
||||
constructor(msg: string, cause?: Error) {
|
||||
super(msg);
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
|
||||
export class KeyboardMissingError extends Error {
|
||||
public readonly cause;
|
||||
|
||||
constructor(msg: string, cause?: Error) {
|
||||
super(msg);
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
|
||||
class UriBasedErrorBuilder implements KeyboardLoadErrorBuilder {
|
||||
readonly uri: string;
|
||||
|
||||
constructor(uri: string) {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
missingError(err: Error) {
|
||||
const msg = `Cannot find the keyboard at ${this.uri}.`;
|
||||
return new KeyboardMissingError(msg, err);
|
||||
}
|
||||
|
||||
scriptError(err: Error) {
|
||||
const msg = `Error registering the keyboard script at ${this.uri}; it may contain an error.`;
|
||||
return new KeyboardScriptError(msg, err);
|
||||
}
|
||||
}
|
||||
|
||||
class StubBasedErrorBuilder implements KeyboardLoadErrorBuilder {
|
||||
readonly stub: KeyboardStub;
|
||||
|
||||
constructor(stub: KeyboardStub) {
|
||||
this.stub = stub;
|
||||
}
|
||||
|
||||
missingError(err: Error) {
|
||||
const stub = this.stub;
|
||||
const msg = `Cannot find the ${stub.name} keyboard for ${stub.langName} at ${stub.filename}.`;
|
||||
return new KeyboardMissingError(msg, err);
|
||||
}
|
||||
|
||||
scriptError(err: Error) {
|
||||
const stub = this.stub;
|
||||
const msg = `Error registering the ${stub.name} keyboard for ${stub.langName}; keyboard script at ${stub.filename} may contain an error.`;
|
||||
return new KeyboardScriptError(msg, err);
|
||||
}
|
||||
}
|
||||
|
||||
export default abstract class KeyboardLoaderBase {
|
||||
export abstract class KeyboardLoaderBase {
|
||||
private _harness: KeyboardHarness;
|
||||
|
||||
public get harness(): KeyboardHarness {
|
||||
|
|
@ -76,23 +16,49 @@ export default abstract class KeyboardLoaderBase {
|
|||
this._harness = harness;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a keyboard from a remote or local URI.
|
||||
*
|
||||
* @param uri The URI of the keyboard to load.
|
||||
* @returns A Promise that resolves to the loaded keyboard.
|
||||
*/
|
||||
public loadKeyboardFromPath(uri: string): Promise<Keyboard> {
|
||||
this.harness.install();
|
||||
const promise = this.loadKeyboardInternal(uri, new UriBasedErrorBuilder(uri));
|
||||
|
||||
return promise;
|
||||
return this.loadKeyboardInternal(uri, new UriBasedErrorBuilder(uri));
|
||||
}
|
||||
|
||||
public loadKeyboardFromStub(stub: KeyboardStub) {
|
||||
/**
|
||||
* Load a keyboard from keyboard stub.
|
||||
*
|
||||
* @param stub The stub of the keyboard to load.
|
||||
* @returns A Promise that resolves to the loaded keyboard.
|
||||
*/
|
||||
public async loadKeyboardFromStub(stub: KeyboardStub): Promise<Keyboard> {
|
||||
this.harness.install();
|
||||
let promise = this.loadKeyboardInternal(stub.filename, new StubBasedErrorBuilder(stub), stub.id);
|
||||
|
||||
return promise;
|
||||
return this.loadKeyboardInternal(stub.filename, new StubBasedErrorBuilder(stub));
|
||||
}
|
||||
|
||||
protected abstract loadKeyboardInternal(
|
||||
uri: string,
|
||||
errorBuilder: KeyboardLoadErrorBuilder,
|
||||
id?: string
|
||||
): Promise<Keyboard>;
|
||||
private async loadKeyboardInternal(uri: string, errorBuilder: KeyboardLoadErrorBuilder): Promise<Keyboard> {
|
||||
const byteArray = await this.loadKeyboardBlob(uri, errorBuilder);
|
||||
|
||||
if (byteArray.slice(0, 4) == Uint8Array.from([0x4b, 0x58, 0x54, 0x53])) { // 'KXTS'
|
||||
// KMX or LDML (KMX+) keyboard
|
||||
console.error("KMX keyboard loading is not yet implemented!");
|
||||
return null;
|
||||
}
|
||||
|
||||
let script: string;
|
||||
try {
|
||||
script = new TextDecoder('utf-8', { fatal: true }).decode(byteArray);
|
||||
} catch (e) {
|
||||
throw errorBuilder.invalidKeyboard(e);
|
||||
}
|
||||
|
||||
// .js keyboard
|
||||
return await this.loadKeyboardFromScript(script, errorBuilder);
|
||||
}
|
||||
|
||||
protected abstract loadKeyboardBlob(uri: string, errorBuilder: KeyboardLoadErrorBuilder): Promise<Uint8Array>;
|
||||
|
||||
protected abstract loadKeyboardFromScript(scriptSrc: string, errorBuilder: KeyboardLoadErrorBuilder): Promise<Keyboard>;
|
||||
}
|
||||
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
///<reference lib="dom" />
|
||||
|
||||
import { Keyboard, KeyboardHarness, KeyboardLoaderBase, KeyboardLoadErrorBuilder, MinimalKeymanGlobal } from 'keyman/engine/keyboard';
|
||||
|
||||
import { ManagedPromise } from '@keymanapp/web-utils';
|
||||
import { default as Keyboard } from '../keyboard.js';
|
||||
import { KeyboardHarness, MinimalKeymanGlobal } from '../keyboardHarness.js';
|
||||
import { KeyboardLoaderBase } from '../keyboardLoaderBase.js';
|
||||
import { KeyboardLoadErrorBuilder } from '../keyboardLoadError.js';
|
||||
|
||||
export class DOMKeyboardLoader extends KeyboardLoaderBase {
|
||||
public readonly element: HTMLIFrameElement;
|
||||
|
|
@ -28,54 +29,44 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase {
|
|||
this.performCacheBusting = cacheBust || false;
|
||||
}
|
||||
|
||||
protected loadKeyboardInternal(
|
||||
uri: string,
|
||||
errorBuilder: KeyboardLoadErrorBuilder,
|
||||
id?: string
|
||||
): Promise<Keyboard> {
|
||||
const promise = new ManagedPromise<Keyboard>();
|
||||
|
||||
if(this.performCacheBusting) {
|
||||
protected async loadKeyboardBlob(uri: string, errorBuilder: KeyboardLoadErrorBuilder): Promise<Uint8Array> {
|
||||
if (this.performCacheBusting) {
|
||||
uri = this.cacheBust(uri);
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
const document = this.harness._jsGlobal.document;
|
||||
const script = document.createElement('script');
|
||||
if(id) {
|
||||
script.id = id;
|
||||
}
|
||||
document.head.appendChild(script);
|
||||
script.onerror = (err: any) => {
|
||||
promise.reject(errorBuilder.missingError(err));
|
||||
}
|
||||
script.onload = () => {
|
||||
if(this.harness.loadedKeyboard) {
|
||||
const keyboard = this.harness.loadedKeyboard;
|
||||
this.harness.loadedKeyboard = null;
|
||||
promise.resolve(keyboard);
|
||||
} else {
|
||||
promise.reject(errorBuilder.scriptError());
|
||||
}
|
||||
}
|
||||
|
||||
// On the oldest mobile devices we support, Promise.finally may not actually exist.
|
||||
// Fortunately... it's not that hard of an issue to work around.
|
||||
// Note: es6-shim doesn't polyfill Promise.finally!
|
||||
promise.then(() => {
|
||||
// It is safe to remove the script once it has been run (https://stackoverflow.com/a/37393041)
|
||||
script.remove();
|
||||
}).catch(() => {
|
||||
script.remove();
|
||||
});
|
||||
|
||||
// Now that EVERYTHING ELSE is ready, establish the link to the keyboard's script.
|
||||
script.src = uri;
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
response = await fetch(uri);
|
||||
} catch (e) {
|
||||
throw errorBuilder.keyboardDownloadError(e);
|
||||
}
|
||||
|
||||
return promise.corePromise;
|
||||
if (!response.ok) {
|
||||
throw errorBuilder.keyboardDownloadError(new Error(`HTTP ${response.status} ${response.statusText}`));
|
||||
}
|
||||
|
||||
let buffer: ArrayBuffer;
|
||||
try {
|
||||
buffer = await response.arrayBuffer();
|
||||
} catch (e) {
|
||||
throw errorBuilder.invalidKeyboard(e);
|
||||
}
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
protected async loadKeyboardFromScript(script: string, errorBuilder: KeyboardLoadErrorBuilder): Promise<Keyboard> {
|
||||
try {
|
||||
this.evalScriptInContext(script, this.harness._jsGlobal);
|
||||
} catch (e) {
|
||||
throw errorBuilder.scriptError(e);
|
||||
}
|
||||
const keyboard = this.harness.loadedKeyboard;
|
||||
if (!keyboard) {
|
||||
throw errorBuilder.scriptError();
|
||||
}
|
||||
|
||||
this.harness.loadedKeyboard = null;
|
||||
return keyboard;
|
||||
}
|
||||
|
||||
private cacheBust(uri: string) {
|
||||
|
|
@ -84,4 +75,14 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase {
|
|||
// being ignored.
|
||||
return uri + "?v=" + (new Date()).getTime(); /*cache buster*/
|
||||
}
|
||||
|
||||
private evalScriptInContext(script: string, context: any) {
|
||||
const f = function (s: string) {
|
||||
// use indirect eval (eval?.() notation doesn't work because of esbuild bundling)
|
||||
const evalFunc = eval;
|
||||
return evalFunc(s);
|
||||
}
|
||||
f.call(context, script);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
import { Keyboard, KeyboardHarness, KeyboardLoaderBase, KeyboardLoadErrorBuilder, MinimalKeymanGlobal } from 'keyman/engine/keyboard';
|
||||
import vm from 'node:vm';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
import vm from 'vm';
|
||||
import fs from 'fs';
|
||||
import { globalObject } from '@keymanapp/web-utils';
|
||||
|
||||
import { default as Keyboard } from '../keyboard.js';
|
||||
import { KeyboardHarness, MinimalKeymanGlobal } from '../keyboardHarness.js';
|
||||
import { KeyboardLoaderBase } from '../keyboardLoaderBase.js';
|
||||
import { KeyboardLoadErrorBuilder } from '../keyboardLoadError.js';
|
||||
|
||||
export class NodeKeyboardLoader extends KeyboardLoaderBase {
|
||||
constructor()
|
||||
constructor(harness: KeyboardHarness);
|
||||
|
|
@ -22,22 +26,32 @@ export class NodeKeyboardLoader extends KeyboardLoaderBase {
|
|||
}
|
||||
}
|
||||
|
||||
protected loadKeyboardInternal(uri: string, errorBuilder: KeyboardLoadErrorBuilder): Promise<Keyboard> {
|
||||
protected async loadKeyboardBlob(uri: string, errorBuilder: KeyboardLoadErrorBuilder): Promise<Uint8Array> {
|
||||
// `fs` does not like 'file:///'; it IS "File System" oriented, after all, and wants a path, not a URI.
|
||||
if(uri.indexOf('file:///') == 0) {
|
||||
if (uri.indexOf('file:///') == 0) {
|
||||
uri = uri.substring('file:///'.length);
|
||||
}
|
||||
|
||||
let buffer: Buffer;
|
||||
try {
|
||||
buffer = await readFile(uri);
|
||||
} catch (err) {
|
||||
throw errorBuilder.keyboardDownloadError(err);
|
||||
}
|
||||
return Uint8Array.from(buffer);
|
||||
}
|
||||
|
||||
protected async loadKeyboardFromScript(scriptSrc: string, errorBuilder: KeyboardLoadErrorBuilder): Promise<Keyboard> {
|
||||
let script;
|
||||
try {
|
||||
script = new vm.Script(fs.readFileSync(uri).toString());
|
||||
script = new vm.Script(scriptSrc);
|
||||
} catch (err) {
|
||||
return Promise.reject(errorBuilder.missingError(err));
|
||||
throw errorBuilder.invalidKeyboard(err);
|
||||
}
|
||||
try {
|
||||
script.runInContext(this.harness._jsGlobal);
|
||||
} catch (err) {
|
||||
return Promise.reject(errorBuilder.scriptError(err));
|
||||
throw errorBuilder.scriptError(err);
|
||||
}
|
||||
|
||||
const keyboard = this.harness.loadedKeyboard;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ SUBPROJECT_NAME=engine/main
|
|||
|
||||
builder_describe "Builds the Keyman Engine for Web's common top-level base classes." \
|
||||
"@/common/web/keyman-version" \
|
||||
"@/web/src/engine/core-processor" \
|
||||
"@/web/src/engine/keyboard" \
|
||||
"@/web/src/engine/interfaces build" \
|
||||
"@/web/src/engine/js-processor build" \
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
## engine/main
|
||||
# engine/main
|
||||
|
||||
This subproject holds modularized code converted from the old, namespaced version of KMW.
|
||||
This subproject holds modularized code converted from the old, namespaced version of KMW.
|
||||
Previously it was called `input-processor`.
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
import ContextWindow from "./contextWindow.js";
|
||||
import { LanguageProcessor } from "./languageProcessor.js";
|
||||
import type { ModelSpec } from "keyman/engine/interfaces";
|
||||
import type { ModelSpec, PathConfiguration } from "keyman/engine/interfaces";
|
||||
import { globalObject, DeviceSpec } from "@keymanapp/web-utils";
|
||||
|
||||
import { CoreProcessor } from "keyman/engine/core-processor";
|
||||
import { Codes, type Keyboard, type KeyEvent } from "keyman/engine/keyboard";
|
||||
import {
|
||||
type Alternate,
|
||||
|
|
@ -34,6 +35,7 @@ export class InputProcessor {
|
|||
private contextDevice: DeviceSpec;
|
||||
private kbdProcessor: KeyboardProcessor;
|
||||
private lngProcessor: LanguageProcessor;
|
||||
private coreProcessor: CoreProcessor;
|
||||
|
||||
private readonly contextCache = new TranscriptionCache();
|
||||
|
||||
|
|
@ -49,6 +51,11 @@ export class InputProcessor {
|
|||
this.contextDevice = device;
|
||||
this.kbdProcessor = new KeyboardProcessor(device, options);
|
||||
this.lngProcessor = new LanguageProcessor(predictiveTextWorker, this.contextCache);
|
||||
this.coreProcessor = new CoreProcessor();
|
||||
}
|
||||
|
||||
public async init(paths: PathConfiguration) {
|
||||
this.coreProcessor.init(paths.basePath);
|
||||
}
|
||||
|
||||
public get languageProcessor(): LanguageProcessor {
|
||||
|
|
|
|||
|
|
@ -238,6 +238,8 @@ export default class KeymanEngine<
|
|||
// Initialize supplementary plane string extensions
|
||||
String.kmwEnableSupplementaryPlane(true);
|
||||
|
||||
await this.core.init(config.paths);
|
||||
|
||||
// Since we're not sandboxing keyboard loads yet, we just use `window` as the jsGlobal object.
|
||||
// All components initialized below require a properly-configured `config.paths` or similar.
|
||||
const keyboardLoader = new KeyboardLoader(this.interface, config.applyCacheBusting);
|
||||
|
|
|
|||
21
web/src/test/auto/dom/cases/core-processor/basic.spec.ts
Normal file
21
web/src/test/auto/dom/cases/core-processor/basic.spec.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { assert } from 'chai';
|
||||
import { CoreProcessor } from 'keyman/engine/core-processor';
|
||||
|
||||
const coreurl = '/web/build/engine/core-processor/obj/import/core';
|
||||
|
||||
// Test the CoreProcessor interface.
|
||||
describe('CoreProcessor', function () {
|
||||
it('can initialize without errors', async function () {
|
||||
const kp = new CoreProcessor();
|
||||
assert.isTrue(await kp.init(coreurl));
|
||||
});
|
||||
|
||||
it('can call temp function', async function () {
|
||||
const kp = new CoreProcessor();
|
||||
await kp.init(coreurl);
|
||||
const a = kp.tmp_wasm_attributes();
|
||||
assert.isNotNull(a);
|
||||
assert.isNumber(a.max_context);
|
||||
console.dir(a);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import { assert } from 'chai';
|
||||
|
||||
import { DOMKeyboardLoader } from 'keyman/engine/keyboard/dom-keyboard-loader';
|
||||
import { extendString, KeyboardHarness, Keyboard, MinimalKeymanGlobal, DeviceSpec, KeyboardKeymanGlobal } from 'keyman/engine/keyboard';
|
||||
import { extendString, KeyboardHarness, Keyboard, MinimalKeymanGlobal, DeviceSpec, KeyboardKeymanGlobal, KeyboardDownloadError, KeyboardScriptError } from 'keyman/engine/keyboard';
|
||||
import { KeyboardInterface, Mock } from 'keyman/engine/js-processor';
|
||||
import { assertThrowsAsync } from 'keyman/tools/testing/test-utils';
|
||||
|
||||
declare let window: typeof globalThis;
|
||||
// KeymanEngine from the web/ folder... when available.
|
||||
|
|
@ -27,6 +28,24 @@ describe('Keyboard loading in DOM', function() {
|
|||
}
|
||||
})
|
||||
|
||||
it('throws error when keyboard does not exist', async () => {
|
||||
const harness = new KeyboardInterface(window, MinimalKeymanGlobal);
|
||||
const keyboardLoader = new DOMKeyboardLoader(harness);
|
||||
const nonExisting = '/does/not/exist.js';
|
||||
|
||||
await assertThrowsAsync(async () => await keyboardLoader.loadKeyboardFromPath(nonExisting),
|
||||
KeyboardDownloadError, `Unable to download keyboard at ${nonExisting}`);
|
||||
});
|
||||
|
||||
it('throws error when keyboard is invalid', async () => {
|
||||
const harness = new KeyboardInterface(window, MinimalKeymanGlobal);
|
||||
const keyboardLoader = new DOMKeyboardLoader(harness);
|
||||
const nonKeyboardPath = '/common/test/resources/index.mjs';
|
||||
|
||||
await assertThrowsAsync(async () => await keyboardLoader.loadKeyboardFromPath(nonKeyboardPath),
|
||||
KeyboardScriptError, `Error registering the keyboard script at ${nonKeyboardPath}; it may contain an error.`);
|
||||
});
|
||||
|
||||
it('`window`, disabled rule processing', async () => {
|
||||
const harness = new KeyboardHarness(window, MinimalKeymanGlobal);
|
||||
let keyboardLoader = new DOMKeyboardLoader(harness);
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export default {
|
|||
nodeResolve: true,
|
||||
// Top-level, implicit 'default' group
|
||||
files: [
|
||||
'src/test/auto/dom/init_check.tests.ts',
|
||||
'web/src/test/auto/dom/init_check.tests.ts',
|
||||
// '**/*.tests.html'
|
||||
],
|
||||
groups: [
|
||||
|
|
@ -41,45 +41,50 @@ export default {
|
|||
name: 'engine/attachment',
|
||||
// Relative, from the containing package.json
|
||||
files: [
|
||||
'build/test/dom/cases/attachment/**/*.tests.html',
|
||||
'build/test/dom/cases/attachment/**/*.tests.mjs'
|
||||
'web/build/test/dom/cases/attachment/**/*.tests.html',
|
||||
'web/build/test/dom/cases/attachment/**/*.tests.mjs'
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'app/browser',
|
||||
// Relative, from the containing package.json
|
||||
files: ['build/test/dom/cases/browser/**/*.tests.mjs']
|
||||
files: ['web/build/test/dom/cases/browser/**/*.tests.mjs']
|
||||
},
|
||||
{
|
||||
name: 'engine/core-processor',
|
||||
// Relative, from the containing package.json
|
||||
files: ['web/src/test/auto/dom/cases/core-processor/*.tests.ts']
|
||||
},
|
||||
{
|
||||
name: 'engine/dom-utils',
|
||||
// Relative, from the containing package.json
|
||||
files: ['build/test/dom/cases/dom-utils/**/*.tests.mjs']
|
||||
files: ['web/build/test/dom/cases/dom-utils/**/*.tests.mjs']
|
||||
},
|
||||
{
|
||||
name: 'engine/element-wrappers',
|
||||
// Relative, from the containing package.json
|
||||
files: ['build/test/dom/cases/element-wrappers/**/*.tests.mjs']
|
||||
files: ['web/build/test/dom/cases/element-wrappers/**/*.tests.mjs']
|
||||
},
|
||||
{
|
||||
name: 'engine/gesture-processor',
|
||||
// Relative, from the containing package.json
|
||||
// Note: here we use the .tests.html file in the src directory!
|
||||
files: ['src/test/auto/dom/cases/gesture-processor/**/*.tests.html']
|
||||
files: ['web/src/test/auto/dom/cases/gesture-processor/**/*.tests.html']
|
||||
},
|
||||
{
|
||||
name: 'engine/keyboard',
|
||||
// Relative, from the containing package.json
|
||||
files: ['build/test/dom/cases/keyboard/**/*.tests.mjs']
|
||||
files: ['web/build/test/dom/cases/keyboard/**/*.tests.mjs']
|
||||
},
|
||||
{
|
||||
name: 'engine/keyboard-storage',
|
||||
// Relative, from the containing package.json
|
||||
files: ['build/test/dom/cases/keyboard-storage/**/*.tests.mjs']
|
||||
files: ['web/build/test/dom/cases/keyboard-storage/**/*.tests.mjs']
|
||||
},
|
||||
{
|
||||
name: 'engine/osk',
|
||||
// Relative, from the containing package.json
|
||||
files: ['build/test/dom/cases/osk/**/*.tests.mjs']
|
||||
files: ['web/build/test/dom/cases/osk/**/*.tests.mjs']
|
||||
}
|
||||
],
|
||||
middleware: [
|
||||
|
|
@ -89,6 +94,12 @@ export default {
|
|||
context.url = '/web/src/test/auto' + context.url;
|
||||
}
|
||||
|
||||
return next();
|
||||
},
|
||||
function rewriteWasmContentType(context, next) {
|
||||
if (context.url.endsWith('.wasm')) {
|
||||
context.headers['content-type'] = 'application/wasm';
|
||||
}
|
||||
return next();
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { assert } from 'chai';
|
|||
import { createRequire } from 'module';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
import { MinimalKeymanGlobal } from 'keyman/engine/keyboard';
|
||||
import { DeviceSpec, MinimalKeymanGlobal } from 'keyman/engine/keyboard';
|
||||
import { KeyboardInterface, Mock } from 'keyman/engine/js-processor';
|
||||
import { NodeKeyboardLoader } from 'keyman/engine/keyboard/node-keyboard-loader';
|
||||
|
||||
|
|
@ -11,21 +11,22 @@ describe('Headless keyboard loading', function () {
|
|||
const laoPath = require.resolve('@keymanapp/common-test-resources/keyboards/lao_2008_basic.js');
|
||||
const khmerPath = require.resolve('@keymanapp/common-test-resources/keyboards/khmer_angkor.js');
|
||||
const nonKeyboardPath = require.resolve('@keymanapp/common-test-resources/index.mjs');
|
||||
const ipaPath = require.resolve('@keymanapp/common-test-resources/keyboards/sil_ipa.js');
|
||||
// const ipaPath = require.resolve('@keymanapp/common-test-resources/keyboards/sil_ipa.js');
|
||||
// Common test suite setup.
|
||||
|
||||
let device = {
|
||||
formFactor: 'desktop',
|
||||
OS: 'windows',
|
||||
browser: 'native'
|
||||
const device = {
|
||||
formFactor: DeviceSpec.FormFactor.Desktop,
|
||||
OS: DeviceSpec.OperatingSystem.Windows,
|
||||
browser: DeviceSpec.Browser.Native,
|
||||
touchable: false
|
||||
}
|
||||
|
||||
describe('Full harness loading', () => {
|
||||
it('successfully loads', async function () {
|
||||
// -- START: Standard Recorder-based unit test loading boilerplate --
|
||||
let harness = new KeyboardInterface({}, MinimalKeymanGlobal);
|
||||
let keyboardLoader = new NodeKeyboardLoader(harness);
|
||||
let keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath);
|
||||
const harness = new KeyboardInterface({}, MinimalKeymanGlobal);
|
||||
const keyboardLoader = new NodeKeyboardLoader(harness);
|
||||
const keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath);
|
||||
harness.activeKeyboard = keyboard;
|
||||
// -- END: Standard Recorder-based unit test loading boilerplate --
|
||||
|
||||
|
|
@ -35,9 +36,9 @@ describe('Headless keyboard loading', function () {
|
|||
|
||||
it('can evaluate rules', async function () {
|
||||
// -- START: Standard Recorder-based unit test loading boilerplate --
|
||||
let harness = new KeyboardInterface({}, MinimalKeymanGlobal);
|
||||
let keyboardLoader = new NodeKeyboardLoader(harness);
|
||||
let keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath);
|
||||
const harness = new KeyboardInterface({}, MinimalKeymanGlobal);
|
||||
const keyboardLoader = new NodeKeyboardLoader(harness);
|
||||
const keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath);
|
||||
harness.activeKeyboard = keyboard;
|
||||
// -- END: Standard Recorder-based unit test loading boilerplate --
|
||||
|
||||
|
|
@ -46,8 +47,8 @@ describe('Headless keyboard loading', function () {
|
|||
});
|
||||
|
||||
it('does not change the active kehboard', async function () {
|
||||
let harness = new KeyboardInterface({}, MinimalKeymanGlobal);
|
||||
let keyboardLoader = new NodeKeyboardLoader(harness);
|
||||
const harness = new KeyboardInterface({}, MinimalKeymanGlobal);
|
||||
const keyboardLoader = new NodeKeyboardLoader(harness);
|
||||
const lao_keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath);
|
||||
assert.isNotOk(harness.activeKeyboard);
|
||||
assert.isOk(lao_keyboard);
|
||||
|
|
@ -66,8 +67,8 @@ describe('Headless keyboard loading', function () {
|
|||
it('throws distinct errors', async function () {
|
||||
const invalidPath = 'totally_invalid_path.js';
|
||||
|
||||
let harness = new KeyboardInterface({}, MinimalKeymanGlobal);
|
||||
let keyboardLoader = new NodeKeyboardLoader(harness);
|
||||
const harness = new KeyboardInterface({}, MinimalKeymanGlobal);
|
||||
const keyboardLoader = new NodeKeyboardLoader(harness);
|
||||
let missingError;
|
||||
try {
|
||||
await keyboardLoader.loadKeyboardFromPath(invalidPath);
|
||||
34
web/src/test/auto/headless/engine/keyboard/keyboard.tests.ts
Normal file
34
web/src/test/auto/headless/engine/keyboard/keyboard.tests.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { assert } from 'chai';
|
||||
|
||||
import { createRequire } from 'module';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
import { KeyboardHarness, MinimalKeymanGlobal, DeviceSpec } from 'keyman/engine/keyboard';
|
||||
import { NodeKeyboardLoader } from 'keyman/engine/keyboard/node-keyboard-loader';
|
||||
|
||||
|
||||
describe('Keyboard tests', function () {
|
||||
const khmerPath = require.resolve('@keymanapp/common-test-resources/keyboards/khmer_angkor.js');
|
||||
|
||||
it('accurately determines layout properties', async () => {
|
||||
// -- START: Standard Recorder-based unit test loading boilerplate --
|
||||
const harness = new KeyboardHarness({}, MinimalKeymanGlobal);
|
||||
const keyboardLoader = new NodeKeyboardLoader(harness);
|
||||
const km_keyboard = await keyboardLoader.loadKeyboardFromPath(khmerPath);
|
||||
// -- END: Standard Recorder-based unit test loading boilerplate --
|
||||
|
||||
// `khmer_angkor` - supports longpresses, but not flicks or multitaps.
|
||||
|
||||
// Phone supports longpress if the keyboard supports it.
|
||||
const mobileLayout = km_keyboard.layout(DeviceSpec.FormFactor.Phone);
|
||||
assert.isTrue(mobileLayout.hasLongpresses);
|
||||
assert.isFalse(mobileLayout.hasFlicks);
|
||||
assert.isFalse(mobileLayout.hasMultitaps);
|
||||
|
||||
// Desktop doesn't support longpress even if the keyboard supports it.
|
||||
const desktopLayout = km_keyboard.layout(DeviceSpec.FormFactor.Desktop);
|
||||
assert.isFalse(desktopLayout.hasLongpresses);
|
||||
assert.isFalse(desktopLayout.hasFlicks);
|
||||
assert.isFalse(desktopLayout.hasMultitaps);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import { assert } from 'chai';
|
||||
|
||||
import { createRequire } from 'module';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
import { KeyboardHarness, MinimalKeymanGlobal, KeyboardDownloadError, DeviceSpec, InvalidKeyboardError } from 'keyman/engine/keyboard';
|
||||
import { KeyboardInterface, Mock } from 'keyman/engine/js-processor';
|
||||
import { NodeKeyboardLoader } from 'keyman/engine/keyboard/node-keyboard-loader';
|
||||
import { assertThrowsAsync, assertThrows } from 'keyman/tools/testing/test-utils';
|
||||
|
||||
describe('Headless keyboard loading', function() {
|
||||
const laoPath = require.resolve('@keymanapp/common-test-resources/keyboards/lao_2008_basic.js');
|
||||
const nonKeyboardPath = require.resolve('@keymanapp/common-test-resources/index.mjs');
|
||||
const ipaPath = require.resolve('@keymanapp/common-test-resources/keyboards/sil_ipa.js');
|
||||
const nonExisting = '/does/not/exist.js';
|
||||
// Common test suite setup.
|
||||
|
||||
const device = {
|
||||
formFactor: DeviceSpec.FormFactor.Desktop,
|
||||
OS: DeviceSpec.OperatingSystem.Windows,
|
||||
browser: DeviceSpec.Browser.Native,
|
||||
touchable: false
|
||||
}
|
||||
|
||||
describe('Minimal harness loading', () => {
|
||||
it('successfully loads a single keyboard from filesystem', async () => {
|
||||
// -- START: Standard Recorder-based unit test loading boilerplate --
|
||||
const harness = new KeyboardInterface({}, MinimalKeymanGlobal);
|
||||
const keyboardLoader = new NodeKeyboardLoader(harness);
|
||||
const keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath);
|
||||
// -- END: Standard Recorder-based unit test loading boilerplate --
|
||||
|
||||
// Asserts that the harness's loading field is cleared once the load is complete.
|
||||
assert.isNotOk(harness.loadedKeyboard);
|
||||
|
||||
// Asserts that the `activeKeyboard` field was not set by the operation.
|
||||
assert.isNotOk(harness.activeKeyboard);
|
||||
|
||||
// This part provides assurance that the keyboard properly loaded.
|
||||
assert.equal(keyboard.id, "Keyboard_lao_2008_basic");
|
||||
});
|
||||
|
||||
it('throws error when keyboard does not exist', async () => {
|
||||
const harness = new KeyboardInterface({}, MinimalKeymanGlobal);
|
||||
const keyboardLoader = new NodeKeyboardLoader(harness);
|
||||
|
||||
await assertThrowsAsync(async () => await keyboardLoader.loadKeyboardFromPath(nonExisting),
|
||||
KeyboardDownloadError, `Unable to download keyboard at ${nonExisting}`);
|
||||
});
|
||||
|
||||
it('throws error when keyboard is invalid', async () => {
|
||||
const harness = new KeyboardInterface({}, MinimalKeymanGlobal);
|
||||
const keyboardLoader = new NodeKeyboardLoader(harness);
|
||||
|
||||
await assertThrowsAsync(async () => await keyboardLoader.loadKeyboardFromPath(nonKeyboardPath),
|
||||
InvalidKeyboardError, `${nonKeyboardPath} is not a valid keyboard file`);
|
||||
});
|
||||
|
||||
it('successfully loads (has variable stores)', async () => {
|
||||
// -- START: Standard Recorder-based unit test loading boilerplate --
|
||||
const harness = new KeyboardInterface({}, MinimalKeymanGlobal);
|
||||
const keyboardLoader = new NodeKeyboardLoader(harness);
|
||||
const keyboard = await keyboardLoader.loadKeyboardFromPath(ipaPath);
|
||||
// -- END: Standard Recorder-based unit test loading boilerplate --
|
||||
|
||||
// This part provides extra assurance that the keyboard properly loaded.
|
||||
assert.equal(keyboard.id, "Keyboard_sil_ipa");
|
||||
});
|
||||
|
||||
// TODO-WEB-CORE: figure out what the purpose of this test is
|
||||
// TODO-WEB-CORE: move to kbdInterface.tests.ts
|
||||
it('cannot evaluate rules', async function() {
|
||||
// -- START: Standard Recorder-based unit test loading boilerplate --
|
||||
const harness = new KeyboardHarness({}, MinimalKeymanGlobal);
|
||||
const keyboardLoader = new NodeKeyboardLoader(harness);
|
||||
const keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath);
|
||||
// -- END: Standard Recorder-based unit test loading boilerplate --
|
||||
|
||||
// Runs a blank KeyEvent through the keyboard's rule processing...
|
||||
// but via separate harness configured with a different captured global.
|
||||
// This shows an important detail: the 'global' object is effectively
|
||||
// closure-captured. (Similar constraints may occur when experimenting with
|
||||
// 'sandboxed' keyboard loading in the DOM!)
|
||||
const ruleHarness = new KeyboardInterface({}, MinimalKeymanGlobal);
|
||||
ruleHarness.activeKeyboard = keyboard;
|
||||
assertThrows(() => ruleHarness.processKeystroke(new Mock(), keyboard.constructNullKeyEvent(device)), 'k.KKM is not a function');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -24,7 +24,7 @@ export default {
|
|||
concurrency: 10,
|
||||
nodeResolve: true,
|
||||
files: [
|
||||
'build/test/integrated//**/*.tests.mjs',
|
||||
'web/build/test/integrated//**/*.tests.mjs',
|
||||
// '**/*.tests.html'
|
||||
],
|
||||
middleware: [
|
||||
|
|
|
|||
10
web/src/test/manual/README.md
Normal file
10
web/src/test/manual/README.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Manual tests
|
||||
|
||||
To run the the manual tests, start the test web server with:
|
||||
|
||||
```bash
|
||||
cd "$KEYMAN_ROOT"
|
||||
web/build.sh start
|
||||
```
|
||||
|
||||
Then open <http://localhost:3000> in your browser.
|
||||
|
|
@ -57,4 +57,4 @@ function do_copy() {
|
|||
}
|
||||
|
||||
builder_run_action clean rm -rf "$KEYMAN_ROOT/$DEST"
|
||||
builder_run_action build do_copy
|
||||
builder_run_action build do_copy
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
// JavaScript Document samplehdr.js: Keyboard management for KeymanWeb demonstration pages
|
||||
|
||||
/*
|
||||
/*
|
||||
This script is designed to test KeymanWeb error message handling.
|
||||
*/
|
||||
|
||||
function loadKeyboards()
|
||||
{
|
||||
function loadKeyboards()
|
||||
{
|
||||
var kmw=keyman;
|
||||
|
||||
|
||||
// We start by adding a keyboard correctly. It's best to include a 'control' in our experiment.
|
||||
kmw.addKeyboards({id:'us',name:'English',languages:{id:'en',name:'English'},
|
||||
filename:'../us-1.0.js'});
|
||||
|
||||
|
||||
// Insert a keyboard that cannot be found.
|
||||
kmw.addKeyboards({id:'lao_2008_basic',name:'wrong-filename',
|
||||
languages:{
|
||||
|
|
@ -19,9 +17,9 @@
|
|||
font:{family:'LaoWeb',source:['../font/saysettha_web.ttf','../font/saysettha_web.woff','../font/saysettha_web.eot']}
|
||||
},
|
||||
filename:'./missing_file.js' // Intentional error - the file doesn't exist, so the <script> tag will raise an error event.
|
||||
});
|
||||
|
||||
// Insert a keyboard that will generate a timing error.
|
||||
});
|
||||
|
||||
// Insert a keyboard that will generate a timing error.
|
||||
kmw.addKeyboards({id:'unparsable',name:'non-parsable',
|
||||
languages:{
|
||||
id:'lo',name:'debugging',region:'Asia',
|
||||
|
|
@ -29,14 +27,16 @@
|
|||
},
|
||||
filename:'./unparsable.js' // Intentional error - the file has no parsable keyboard, so while the <script> tag will load,
|
||||
// registration will fail.
|
||||
});
|
||||
});
|
||||
|
||||
// Insert a keyboard that will generate a timing error.
|
||||
// Insert a keyboard that will generate a timing error. `timeout.js` doesn't
|
||||
// exist, but the test server (web/src/tools/testing/test-server/index.cjs)
|
||||
// has special handling for that URL and times out after 10 seconds.
|
||||
kmw.addKeyboards({id:'timeout',name:'timeout',
|
||||
languages:{
|
||||
id:'lo',name:'debugging',region:'Asia',
|
||||
font:{family:'LaoWeb',source:['../font/saysettha_web.ttf','../font/saysettha_web.woff','../font/saysettha_web.eot']}
|
||||
},
|
||||
filename:'./timeout.js' // Intentional (simulated) error - the file never loads, simulating a server timeout.
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,9 +65,32 @@
|
|||
<h3>or in this input field:</h3>
|
||||
<input class='test' value='' placeholder='or here'/>
|
||||
|
||||
<h2>Expected error messages:</h2>
|
||||
<ul>
|
||||
<li><i>wrong-filename</i> keyboard: "Unable to download wrong-filename keyboard for debugging"</li>
|
||||
<li><i>non-parsable</i> keyboard: "Error registering the non-parsable keyboard for debugging; keyboard script at ./unparsable.js may contain an error."</li>
|
||||
<li><i>timeout</i> keyboard: "Sorry, the timeout keyboard for debugging is not currently available."</li>
|
||||
</ul>
|
||||
|
||||
<h3><a href="../index.html">Return to testing home page</a></h3>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
if (!window.location.href.startsWith('http://localhost:3000')) {
|
||||
const body = document.getElementsByTagName('body')[0];
|
||||
body.innerHTML = `<h1>KeymanWeb Sample Page - Error Testing page</h1>
|
||||
<h2>Unable to load this test page!</h2>
|
||||
<p>This page has to be loaded through the local test server.</p>
|
||||
<p>Do the following:</p>
|
||||
<ol>
|
||||
<li>Open a terminal and navigate to the Keyman source root directory</li>
|
||||
<li>Start the test server with <code>web/build.sh start</code></li>
|
||||
<li>Open <a href="http://localhost:3000/src/test/manual/web/keyboard-errors/index.html">http://localhost:3000/src/test/manual/web/keyboard-errors/index.html</a> in a browser</li>
|
||||
</ol>
|
||||
`;
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
<!--
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
(function() {
|
||||
var me = document.currentScript;
|
||||
console.log(me);
|
||||
|
||||
var onload = me.onload;
|
||||
if(onload) {
|
||||
me.onload = null;
|
||||
}
|
||||
|
||||
document.body.addEventListener('load', function(e) {
|
||||
// Prevent the element's onload event from firing
|
||||
if(e.srcElement == me || e.target == me) {
|
||||
e.cancelBubble = true;
|
||||
}
|
||||
}, {capture: true});
|
||||
|
||||
window.setTimeout(function () {
|
||||
// Restores the function after a slight delay.
|
||||
me.onload = onload;
|
||||
}, 1);
|
||||
})();
|
||||
|
|
@ -22,7 +22,8 @@ builder_describe "Builds the Keyman Engine for Web's development & unit-testing
|
|||
"--ci Does nothing for this script" \
|
||||
":bulk_rendering=testing/bulk_rendering Builds the bulk-rendering tool used to validate changes to OSK display code" \
|
||||
":recorder=testing/recorder Builds the KMW recorder tool used for development of unit-test resources" \
|
||||
":sourcemap-root=building/sourcemap-root Builds the sourcemap-cleaning tool used during minification of app/ builds"
|
||||
":sourcemap-root=building/sourcemap-root Builds the sourcemap-cleaning tool used during minification of app/ builds" \
|
||||
":test-utils=testing/test-utils Builds the test-utils module"
|
||||
|
||||
builder_parse "$@"
|
||||
|
||||
|
|
|
|||
25
web/src/tools/testing/test-server/index.cjs
Normal file
25
web/src/tools/testing/test-server/index.cjs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
const express = require('express')
|
||||
const path = require('path')
|
||||
const app = express()
|
||||
const port = 3000
|
||||
|
||||
app.use(express.static(path.join(__dirname, '../../../../')))
|
||||
|
||||
// for testing timeout error in web/src/test/manual/web/keyboard-errors
|
||||
const router = express.Router()
|
||||
router.get('/src/test/manual/web/keyboard-errors/timeout.js', async (req, res, next) => {
|
||||
console.log('timeout.js requested')
|
||||
return new Promise(() => {
|
||||
setTimeout(() => {
|
||||
res.set('Content-Type', 'application/json');
|
||||
res.status(200);
|
||||
|
||||
next();
|
||||
}, 10500); // > ContextManagerBase.TIMEOUT_THRESHOLD (10 seconds)
|
||||
});
|
||||
})
|
||||
app.use(router)
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Keyman test app listening on port ${port}`)
|
||||
})
|
||||
34
web/src/tools/testing/test-utils/build.sh
Executable file
34
web/src/tools/testing/test-utils/build.sh
Executable file
|
|
@ -0,0 +1,34 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# Compile KeymanWeb's automated js/ts test utilities
|
||||
|
||||
## START STANDARD BUILD SCRIPT INCLUDE
|
||||
# adjust relative paths as necessary
|
||||
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
|
||||
. "${THIS_SCRIPT%/*}/../../../../../resources/build/builder.inc.sh"
|
||||
## END STANDARD BUILD SCRIPT INCLUDE
|
||||
|
||||
SUBPROJECT_NAME=tools/testing/test-utils
|
||||
. "${KEYMAN_ROOT}/web/common.inc.sh"
|
||||
. "${KEYMAN_ROOT}/resources/shellHelperFunctions.sh"
|
||||
|
||||
################################ Main script ################################
|
||||
|
||||
builder_describe "Automated js/ts test utilities for KeymanWeb" \
|
||||
"clean" \
|
||||
"configure" \
|
||||
"build"
|
||||
|
||||
builder_describe_outputs \
|
||||
configure /node_modules \
|
||||
build "/web/build/${SUBPROJECT_NAME}/obj/index.js"
|
||||
|
||||
builder_parse "$@"
|
||||
|
||||
do_build ( ) {
|
||||
compile "${SUBPROJECT_NAME}"
|
||||
}
|
||||
|
||||
builder_run_action configure verify_npm_setup
|
||||
builder_run_action clean rm -rf "../../../../build/${SUBPROJECT_NAME}/"
|
||||
builder_run_action build do_build
|
||||
31
web/src/tools/testing/test-utils/index.ts
Normal file
31
web/src/tools/testing/test-utils/index.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { assert } from 'chai';
|
||||
|
||||
// export async function assertThrowsAsync(fn: () => Promise<any>, message?: string): Promise<void>;
|
||||
export async function assertThrowsAsync(fn: () => Promise<any>, type?: any, message?: string): Promise<void> {
|
||||
assert(!!type || !!message, 'at least one of type or message must be specified');
|
||||
if (typeof(type) === 'string') {
|
||||
message = type;
|
||||
type = undefined;
|
||||
}
|
||||
try {
|
||||
await fn();
|
||||
assert.fail('Expected function to throw an error, but it did not.');
|
||||
} catch (err) {
|
||||
if (type) {
|
||||
assert.isTrue(err instanceof type, `Expected error to be of type ${type.name}, but got ${err.constructor.name}`);
|
||||
}
|
||||
if (message) {
|
||||
assert.equal((err as Error).message, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function assertThrows(fn: () => any, message?: string): void;
|
||||
export function assertThrows(fn: () => any, type?: any, message?: string): void {
|
||||
assert(!!type || !!message, 'at least one of type or message must be specified');
|
||||
if (typeof(type) === 'string') {
|
||||
message = type;
|
||||
type = undefined;
|
||||
}
|
||||
assert.throws(fn, type, message);
|
||||
}
|
||||
11
web/src/tools/testing/test-utils/tsconfig.json
Normal file
11
web/src/tools/testing/test-utils/tsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.dom.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "../../../../build/tools/testing/test-utils/obj/",
|
||||
"rootDir": ".",
|
||||
"tsBuildInfoFile": "../../../../build/tools/testing/test-utils/obj/tsconfig.tsbuildinfo"
|
||||
},
|
||||
|
||||
"include": [ "*.ts" ],
|
||||
}
|
||||
|
|
@ -41,6 +41,8 @@ fi
|
|||
|
||||
# End common configs.
|
||||
|
||||
builder_run_action test:dom web-test-runner --config "src/test/auto/dom/web-test-runner${WTR_CONFIG}.config.mjs" ${WTR_DEBUG}
|
||||
cd "${KEYMAN_ROOT}"
|
||||
|
||||
builder_run_action test:integrated web-test-runner --config "src/test/auto/integrated/web-test-runner${WTR_CONFIG}.config.mjs" ${WTR_DEBUG}
|
||||
builder_run_action test:dom web-test-runner --config "web/src/test/auto/dom/web-test-runner${WTR_CONFIG}.config.mjs" ${WTR_DEBUG}
|
||||
|
||||
builder_run_action test:integrated web-test-runner --config "web/src/test/auto/integrated/web-test-runner${WTR_CONFIG}.config.mjs" ${WTR_DEBUG}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue