Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(web): load .kmx keyboard from blob 🎼 #12823

Merged
merged 3 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion core/include/keyman/keyman_core_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 COMs `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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// (see https://github.com/emscripten-core/emscripten/issues/18585)
<!--
```c */
enum km_core_status_codes {
KM_CORE_STATUS_OK = 0,
Expand Down
3 changes: 3 additions & 0 deletions web/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,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/" \
Expand Down
4 changes: 2 additions & 2 deletions web/src/app/browser/src/keymanEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,11 +191,11 @@ export default class KeymanEngine extends KeymanEngineBase<BrowserConfiguration,
// globe key - are accessible.
//
// The `super` call above initializes `keyboardRequisitioner`, as needed here.
this.keyboardRequisitioner.cloudQueryEngine.once('unboundregister', () => {
this.keyboardRequisitioner.cloudQueryEngine.once('unboundregister', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EventEmitter, which is what will trigger this function, does not do await / async stuff. If you add code later that motivates it, okay... but as is, you're gaining nothing by the async / await change here.

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('', '');
}
});

Expand Down
4 changes: 4 additions & 0 deletions web/src/app/ui/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
6 changes: 3 additions & 3 deletions web/src/app/ui/kmwuibutton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ if(!keyman?.ui?.name) {

/**
* Do not enclose in an anonymous function, as the compiler may create
* global scope variables to replace true, false, null, whcih can then collide
* global scope variables to replace true, false, null, which can then collide
* with other variables.
* Instead, use the --output-wrapper command during optimization, which will
* add the anonymous function to enclose all code, including those optimized
Expand Down Expand Up @@ -96,7 +96,7 @@ if(!keyman?.ui?.name) {
* @param {Event} _id keyboard selection event
* @return {boolean}
*/
readonly _SelectKeyboard = (_id: Event) => {
readonly _SelectKeyboard = async (_id: Event): Promise<boolean> => {
let id: string = '';
if(typeof(_id) == 'object') {
let t: HTMLElement = null;
Expand Down Expand Up @@ -124,7 +124,7 @@ if(!keyman?.ui?.name) {
_k.className='selected';
}
this._KMWSel = _k;
keymanweb.setActiveKeyboard(_name,_lgc);
await keymanweb.setActiveKeyboard(_name,_lgc);
} else {
_name=null;
}
Expand Down
6 changes: 3 additions & 3 deletions web/src/app/ui/kmwuifloat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,15 +442,15 @@ if(!keyman?.ui?.name) {
* @param {Object} e event
* Description Change active keyboard in response to user selection event
*/
readonly SelectKeyboardChange = (e: Event) => {
readonly SelectKeyboardChange = async (e: Event) => {
keymanweb.activatingUI(true);

if(this.KeyboardSelector.value != '-') {
var i=this.KeyboardSelector.selectedIndex;
var t=this.KeyboardSelector.options[i].value.split(':');
keymanweb.setActiveKeyboard(t[0],t[1]);
await keymanweb.setActiveKeyboard(t[0],t[1]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the review for me learning. Just did some very quick reading of promises. So as I understand setActiveKeyboard is a promise function which will either resolve or reject. However this code doesn't take into account whether it resolved or rejected, is it just simply waiting to it has run (async) before continuing... regardless of resolved or rejected.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However this code doesn't take into account whether it resolved or rejected, is it just simply waiting to it has run (async) before continuing... regardless of resolved or rejected.

If rejected, an exception is thrown. Generally setActiveKeyboard should never reject.

} else {
keymanweb.setActiveKeyboard('');
await keymanweb.setActiveKeyboard('');
}

//if(osk['show']) osk['show'](osk['isEnabled']()); handled by keyboard change event???
Expand Down
18 changes: 9 additions & 9 deletions web/src/app/ui/kmwuitoggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ if(!keyman?.ui?.name) {
/**
* Toggle a single keyboard on or off - KMW button control event
**/
readonly switchSingleKbd = () => {
readonly switchSingleKbd = async () => {
const _v = keymanweb.getActiveKeyboard() == '';
let nLastKbd=0, kbdName='', lgCode='';

Expand All @@ -195,10 +195,10 @@ if(!keyman?.ui?.name) {

kbdName = this.keyboards[nLastKbd]._InternalName;
lgCode = this.keyboards[nLastKbd]._LanguageCode;
keymanweb.setActiveKeyboard(kbdName,lgCode);
await keymanweb.setActiveKeyboard(kbdName,lgCode);
this.lastActiveKeyboard = nLastKbd;
} else {
keymanweb.setActiveKeyboard('');
await keymanweb.setActiveKeyboard('');
}

if(this.kbdButton) {
Expand All @@ -209,7 +209,7 @@ if(!keyman?.ui?.name) {
/**
* Switch to the next keyboard in the list - KMW button control event
**/
readonly switchNextKbd = () => {
readonly switchNextKbd = async () => {
let _v = (keymanweb.getActiveKeyboard() == '');
let kbdName='', lgCode='';

Expand All @@ -220,16 +220,16 @@ if(!keyman?.ui?.name) {

kbdName = this.keyboards[0]._InternalName;
lgCode = this.keyboards[0]._LanguageCode;
keymanweb.setActiveKeyboard(kbdName,lgCode);
await keymanweb.setActiveKeyboard(kbdName,lgCode);
this.lastActiveKeyboard = 0;
} else {
if(this.lastActiveKeyboard == this.keyboards.length-1) {
keymanweb.setActiveKeyboard('');
await keymanweb.setActiveKeyboard('');
_v = false;
} else {
kbdName = this.keyboards[++this.lastActiveKeyboard]._InternalName;
lgCode = this.keyboards[this.lastActiveKeyboard]._LanguageCode;
keymanweb.setActiveKeyboard(kbdName,lgCode);
await keymanweb.setActiveKeyboard(kbdName,lgCode);
_v = true;
}
}
Expand Down Expand Up @@ -551,7 +551,7 @@ if(!keyman?.ui?.name) {
* @param {number} _kbd
* Description Select a keyboard from the drop down menu
**/
selectKbd(_kbd: number) {
async selectKbd(_kbd: number): Promise<boolean> {
let _name,_lgCode;
if(_kbd < 0) {
_name = '';
Expand All @@ -561,7 +561,7 @@ if(!keyman?.ui?.name) {
_lgCode = this.keyboards[_kbd]._LanguageCode;
}

keymanweb.setActiveKeyboard(_name,_lgCode);
await keymanweb.setActiveKeyboard(_name,_lgCode);
keymanweb.focusLastActiveElement();
this.kbdButton._setSelected(_name != '');
if(_kbd >= 0) {
Expand Down
8 changes: 4 additions & 4 deletions web/src/app/ui/kmwuitoolbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -813,7 +813,7 @@ if(!keyman?.ui?.name) {
* @param {boolean} updateKeyman
* @return {boolean}
**/
selectKeyboard(event: Event, lang: LanguageEntry, kbd: KeyboardDetail, updateKeyman: boolean) {
async selectKeyboard(event: Event, lang: LanguageEntry, kbd: KeyboardDetail, updateKeyman: boolean) {
keymanweb.activatingUI(true);

if(this.selectedLanguage) {
Expand All @@ -834,7 +834,7 @@ if(!keyman?.ui?.name) {
// Return focus to input area and activate the selected keyboard
this.addKeyboardToList(lang, kbd);
if(updateKeyman) {
keymanweb.setActiveKeyboard(kbd.InternalName, kbd.LanguageCode).then(() => {
await keymanweb.setActiveKeyboard(kbd.InternalName, kbd.LanguageCode).then(() => {
// Restore focus _after_ the keyboard finishes loading.
this.setLastFocus();
});
Expand Down Expand Up @@ -936,7 +936,7 @@ if(!keyman?.ui?.name) {
* @return {boolean}
* Description Update the UI when all keyboards disabled by user
**/
readonly offButtonClickEvent = (event: Event) => {
readonly offButtonClickEvent = async (event: Event) => {
if(this.toolbarNode.className != 'kmw_controls_disabled') {
this.hideKeyboardsForLanguage(null);
if(this.selectedLanguage) {
Expand All @@ -952,7 +952,7 @@ if(!keyman?.ui?.name) {

// Return the focus to the input area and set the active keyboard to nothing
this.setLastFocus();
keymanweb.setActiveKeyboard('','');
await keymanweb.setActiveKeyboard('','');

//Save current state when deselecting a keyboard (may not be needed)
this.saveCookie();
Expand Down
33 changes: 33 additions & 0 deletions web/src/engine/core-processor/src/core-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Keyman is copyright (C) SIL International. MIT License.

import { type MainModule } 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 CoreFactory {
public static async createCoreProcessor(baseurl: string): Promise<MainModule> {
const module = await import(baseurl + '/km-core.js');
const createCoreProcessor = module.default;
return await createCoreProcessor({
locateFile: function (path: string, scriptDirectory: string) {
return baseurl + '/' + path;
}
});
}
}
30 changes: 0 additions & 30 deletions web/src/engine/core-processor/src/core-processor.ts

This file was deleted.

4 changes: 3 additions & 1 deletion web/src/engine/core-processor/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './core-processor.js';
export { CoreFactory, KM_CORE_STATUS } from './core-factory.js';
import { type MainModule, type km_core_keyboard, type CoreKeyboardReturn } from './import/core/keymancore.js';
export { MainModule, km_core_keyboard, CoreKeyboardReturn };
14 changes: 9 additions & 5 deletions web/src/engine/main/src/headless/inputProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { LanguageProcessor } from "./languageProcessor.js";
import type { ModelSpec, PathConfiguration } from "keyman/engine/interfaces";
import { globalObject, DeviceSpec } from "@keymanapp/web-utils";

import { CoreProcessor } from "keyman/engine/core-processor";
import { CoreFactory, MainModule as KmCoreModule } from 'keyman/engine/core-processor';

import { Codes, type Keyboard, type KeyEvent } from "keyman/engine/keyboard";
import {
type Alternate,
Expand Down Expand Up @@ -35,7 +36,7 @@ export class InputProcessor {
private contextDevice: DeviceSpec;
private kbdProcessor: KeyboardProcessor;
private lngProcessor: LanguageProcessor;
private coreProcessor: CoreProcessor;
private km_core: Promise<KmCoreModule>;

private readonly contextCache = new TranscriptionCache();

Expand All @@ -51,11 +52,10 @@ 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 init(paths: PathConfiguration) {
this.km_core = CoreFactory.createCoreProcessor(paths.basePath);
}

public get languageProcessor(): LanguageProcessor {
Expand All @@ -70,6 +70,10 @@ export class InputProcessor {
return this.keyboardProcessor.keyboardInterface;
}

public get keymanCore(): Promise<KmCoreModule> {
return this.km_core;
}

public get activeKeyboard(): Keyboard {
return this.keyboardInterface.activeKeyboard;
}
Expand Down
2 changes: 1 addition & 1 deletion web/src/engine/main/src/keymanEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ export default class KeymanEngine<
// Initialize supplementary plane string extensions
String.kmwEnableSupplementaryPlane(true);

await this.core.init(config.paths);
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.
Expand Down
31 changes: 24 additions & 7 deletions web/src/test/auto/dom/cases/core-processor/basic.tests.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
import { assert } from 'chai';
import { CoreProcessor } from 'keyman/engine/core-processor';
import { CoreFactory, KM_CORE_STATUS } from 'keyman/engine/core-processor';

const coreurl = '/web/build/engine/core-processor/obj/import/core';
const coreurl = '/build/engine/core-processor/obj/import/core';

// Test the CoreProcessor interface.
describe('CoreProcessor', function () {
async function loadKeyboardBlob(uri: string) {
const response = await fetch(uri);
if (!response.ok) {
throw new Error(`HTTP ${response.status} ${response.statusText}`);
}

const buffer = await response.arrayBuffer();
return new Uint8Array(buffer);
}

it('can initialize without errors', async function () {
const kp = new CoreProcessor();
assert.isTrue(await kp.init(coreurl));
assert.isNotNull(await CoreFactory.createCoreProcessor(coreurl));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here for example would fail if there's an err.

});

it('can call temp function', async function () {
const kp = new CoreProcessor();
await kp.init(coreurl);
const a = kp.tmp_wasm_attributes();
const km_core = await CoreFactory.createCoreProcessor(coreurl);
const a = km_core.tmp_wasm_attributes();
assert.isNotNull(a);
assert.isNumber(a.max_context);
console.dir(a);
});

it('can load a keyboard from blob', async function () {
const km_core = await CoreFactory.createCoreProcessor(coreurl);
const blob = await loadKeyboardBlob('/common/test/resources/keyboards/test_8568_deadkeys.kmx')
const result = km_core.keyboard_load_from_blob('test', blob);
assert.equal(result.status, KM_CORE_STATUS.OK);
assert.isNotNull(result.object);
result.delete();
});
});
2 changes: 2 additions & 0 deletions web/src/test/auto/dom/web-test-runner.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ export default {
function rewriteResourcePath(context, next) {
if(context.url.startsWith('/resources/')) {
context.url = '/web/src/test/auto' + context.url;
} else if (context.url.startsWith('/build/')) {
context.url = '/web' + context.url;
}

return next();
Expand Down
Loading
Loading