Skip to content

Commit

Permalink
Merge pull request #8401 from keymanapp/change/web/keyboard-requisition
Browse files Browse the repository at this point in the history
change(web): engine-level keyboard/stub fetching and caching 🧩
  • Loading branch information
jahorton authored Mar 17, 2023
2 parents 88cb304 + 56f3345 commit f48ecf0
Show file tree
Hide file tree
Showing 42 changed files with 10,225 additions and 1,279 deletions.
3 changes: 2 additions & 1 deletion common/web/input-processor/tests/cases/inputProcessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,9 @@ describe('InputProcessor', function() {

// Load the keyboard.
let keyboardLoader = new NodeKeyboardLoader(new KeyboardInterface({}, MinimalKeymanGlobal));
await keyboardLoader.loadKeyboardFromPath(require.resolve('@keymanapp/common-test-resources/keyboards/test_chirality.js'));
const keyboard = await keyboardLoader.loadKeyboardFromPath(require.resolve('@keymanapp/common-test-resources/keyboards/test_chirality.js'));
keyboardWithHarness = keyboardLoader.harness;
keyboardWithHarness.activeKeyboard = keyboard;
});

describe('without fat-fingering', function() {
Expand Down
1 change: 1 addition & 0 deletions common/web/keyboard-processor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export {
KeyboardInternalPropertySpec,
default as KeyboardProperties,
KeyboardFont,
MetadataObj as RawKeyboardMetadata,
LanguageAPIPropertySpec
} from "./keyboards/keyboardProperties.js";
export { default as SpacebarText } from "./keyboards/spacebarText.js";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export class KeyboardHarness {
/**
* This field serves as the receptacle for a successfully-loaded Keyboard.
*/
public activeKeyboard: Keyboard;
public loadedKeyboard: Keyboard = null;

/**
* Keyman keyboards register themselves into the Keyman Engine for Web by directly
Expand All @@ -79,7 +79,10 @@ export class KeyboardHarness {
* @param scriptObject
*/
public KR(scriptObject: any) {
this.activeKeyboard = new Keyboard(scriptObject);
if(this.loadedKeyboard) {
throw new Error("Unexpected state: the most-recently loaded keyboard field was not properly reset.");
}
this.loadedKeyboard = new Keyboard(scriptObject);
}

/**
Expand Down
52 changes: 48 additions & 4 deletions common/web/keyboard-processor/src/keyboards/keyboardProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ export type LanguageAPIPropertySpec = {
export type KeyboardAPIPropertySpec = {
id: string,
name: string,

/**
* @deprecated Replaced with `languages`.
*/
language?: LanguageAPIPropertySpec;
languages: LanguageAPIPropertySpec;
}

Expand All @@ -94,10 +99,15 @@ export type KeyboardAPIPropertySpec = {
export type KeyboardAPIPropertyMultilangSpec = {
id: string,
name: string,

/**
* @deprecated Replaced with `languages`.
*/
language?: LanguageAPIPropertySpec[];
languages: LanguageAPIPropertySpec[];
}

type MetadataObj = KeyboardInternalPropertySpec | KeyboardAPIPropertySpec | KeyboardAPIPropertyMultilangSpec;
export type MetadataObj = KeyboardInternalPropertySpec | KeyboardAPIPropertySpec | KeyboardAPIPropertyMultilangSpec;

export default class KeyboardProperties implements KeyboardInternalPropertySpec {
KI: string;
Expand All @@ -112,19 +122,22 @@ export default class KeyboardProperties implements KeyboardInternalPropertySpec

public constructor(metadataObj: MetadataObj, fontPath?: string);
public constructor(keyboardId: string, languageCode: string);
public constructor(arg1: MetadataObj | string, arg2?: string | SpacebarText, arg3?: string, arg4?: KeyboardFont, arg5?: KeyboardFont) {
public constructor(arg1: MetadataObj | string, arg2?: string | SpacebarText) {
if(!(typeof arg1 == 'string')) {
if(arg1['KI'] || arg1['KLC'] || arg1['KFont'] || arg1['KOskFont']) {
if(arg1['KI'] || arg1['KL'] || arg1['KLC'] || arg1['KFont'] || arg1['KOskFont']) {
const other = arg1 as KeyboardInternalPropertySpec;
this.KI = other.KI;
this.KN = other.KN;
this.KL = other.KL;
this.KLC = other.KLC;
this.KFont = other.KFont;
this.KOskFont = other.KOskFont;
this._displayName = other.displayName;
this._displayName = (other instanceof KeyboardProperties) ? other._displayName : other.displayName;
} else {
let apiStub = arg1 as KeyboardAPIPropertySpec; // TODO: could be an array, as currently specified. :(

apiStub.languages ||= apiStub.language;

this.KI = apiStub.id,
this.KN = apiStub.name,
this.KL = apiStub.languages.name,
Expand All @@ -141,6 +154,8 @@ export default class KeyboardProperties implements KeyboardInternalPropertySpec
public static fromMultilanguageAPIStub(apiStub: KeyboardAPIPropertyMultilangSpec, spacebarTextMode?: SpacebarText): KeyboardProperties[] {
let stubs: KeyboardProperties[] = [];

apiStub.languages ||= apiStub.language;

for(let langSpec of apiStub.languages) {
let stub: KeyboardAPIPropertySpec = {
id: apiStub.id,
Expand Down Expand Up @@ -204,4 +219,33 @@ export default class KeyboardProperties implements KeyboardInternalPropertySpec
public get oskFont() {
return this.KOskFont;
}

/**
* Generates an error for objects with specification levels insufficient for use in the on-screen-keyboard
* module, complete with a message about one or more details in need of correction.
* @returns A preconstructed `Error` instance that may be thrown by the caller.
*/
public validateForOSK(): Error {
if(!this.KLC) {
if(this.KI || this.KN) {
return new Error(`No language code was specified for use with the ${this.KI || this.KN} keyboard`);
} else {
return new Error("No language code was specified for use with the corresponding keyboard")
}
}

if(this.displayName === undefined || (this.spacebarTextMode != SpacebarText.BLANK && !this.displayName)) {
return new Error("A display name is missing for this keyboard and cannot be generated.")
}

return null;
}

public validateForCustomKeyboard(): Error {
if(!this.KI || !this.KN || !this.KL || !this.KLC) {
return new Error("To use a custom keyboard, you must specify keyboard id, keyboard name, language and language code.");
} else {
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ export class DOMKeyboardLoader extends KeyboardLoaderBase {
document.head.appendChild(script);
script.onerror = promise.reject;
script.onload = () => {
if(this.harness.activeKeyboard) {
promise.resolve(this.harness.activeKeyboard);
if(this.harness.loadedKeyboard) {
const keyboard = this.harness.loadedKeyboard;
this.harness.loadedKeyboard = null;
promise.resolve(keyboard);
} else {
promise.reject();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,18 @@ export class NodeKeyboardLoader extends KeyboardLoaderBase {

protected loadKeyboardInternal(uri: string): Promise<Keyboard> {
try {
// `fs` does not like 'file:///'; it IS "File System" oriented, after all, and wants a path, not a URI.
if(uri.indexOf('file:///') == 0) {
uri = uri.substring('file:///'.length);
}
const script = new vm.Script(fs.readFileSync(uri).toString());
script.runInContext(this.harness._jsGlobal);
} catch (err) {
return Promise.reject(err);
}

return Promise.resolve(this.harness.activeKeyboard);
const keyboard = this.harness.loadedKeyboard;
this.harness.loadedKeyboard = null;
return Promise.resolve(keyboard);
}
}
40 changes: 0 additions & 40 deletions common/web/keyboard-processor/src/keyboards/nodeKeyboardLoader.ts

This file was deleted.

12 changes: 4 additions & 8 deletions common/web/keyboard-processor/src/text/kbdInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,15 +258,9 @@ export default class KeyboardInterface extends KeyboardHarness {
// NOTE: This implementation is web-core specific and is intentionally replaced, whole-sale,
// by DOM-aware code.
let keyboard = new Keyboard(Pk);
this.activeKeyboard = keyboard;
this.loadedKeyboard = keyboard;
}

/**
* Used by DOM-aware KeymanWeb to add keyboard stubs, used by the `KeyboardManager` type
* to optimize resource use.
*/
registerStub?: (Pstub) => number;

/**
* Get *cached or uncached* keyboard context for a specified range, relative to caret
*
Expand Down Expand Up @@ -1145,7 +1139,9 @@ export default class KeyboardInterface extends KeyboardHarness {
let prototype = this.prototype;

var exportKBCallback = function(miniName: string, longName: string) {
prototype[miniName] = prototype[longName];
if(prototype[longName]) {
prototype[miniName] = prototype[longName];
}
}

exportKBCallback('KSF', 'saveFocus');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ describe('Keyboard loading in DOM', function() {
})

it('`window`, disabled rule processing', async () => {
let keyboardLoader = new DOMKeyboardLoader(new KeyboardHarness(window, MinimalKeymanGlobal));
const harness = new KeyboardHarness(window, MinimalKeymanGlobal);
let keyboardLoader = new DOMKeyboardLoader(harness);
let keyboard = await keyboardLoader.loadKeyboardFromPath('/resources/keyboards/khmer_angkor.js');

assert.isOk(keyboard);
Expand All @@ -30,12 +31,19 @@ describe('Keyboard loading in DOM', function() {
assert.isFalse(keyboard.isCJK);
assert.isOk(window.KeymanWeb);
assert.isOk(window.keyman);

// Should be not be modified by the keyboard load; it is not activated by default.
assert.isNotOk(harness.activeKeyboard);

// Should be cleared post-keyboard-load.
assert.isNotOk(harness.loadedKeyboard);
});

it('`window`, enabled rule processing', async () => {
const harness = new KeyboardInterface(window, MinimalKeymanGlobal);
const keyboardLoader = new DOMKeyboardLoader(harness);
const keyboard = await keyboardLoader.loadKeyboardFromPath('/resources/keyboards/khmer_angkor.js');
harness.activeKeyboard = keyboard;

assert.isOk(keyboard);
assert.equal(keyboard.id, 'Keyboard_khmer_angkor');
Expand All @@ -52,5 +60,42 @@ describe('Keyboard loading in DOM', function() {
assert.isOk(result);
assert.isOk(window.KeymanWeb);
assert.isOk(window.keyman);

// Should be cleared post-keyboard-load.
assert.isNotOk(harness.loadedKeyboard);
});

it('load keyboards successfully in parallel without side effects', async () => {
let harness = new KeyboardInterface(window, MinimalKeymanGlobal);
let keyboardLoader = new DOMKeyboardLoader(harness);

// Preload a keyboard and make it active.
const test_kbd = await keyboardLoader.loadKeyboardFromPath('/resources/keyboards/test_917.js');
harness.activeKeyboard = test_kbd;
assert.isNotOk(harness.loadedKeyboard);

// With an active keyboard, load three keyboards but activate none of them.
const lao_keyboard_promise = keyboardLoader.loadKeyboardFromPath('/resources/keyboards/lao_2008_basic.js');
const khmer_keyboard_promise = keyboardLoader.loadKeyboardFromPath('/resources/keyboards/khmer_angkor.js');
const chiral_keyboard_promise = keyboardLoader.loadKeyboardFromPath('/resources/keyboards/test_chirality.js');

// Sure, why not `await` out of order?
const chiral_keyboard = await chiral_keyboard_promise;
const lao_keyboard = await lao_keyboard_promise;
const khmer_keyboard = await khmer_keyboard_promise;

assert.strictEqual(test_kbd, harness.activeKeyboard);
assert.isNotOk(harness.loadedKeyboard);

assert.isOk(lao_keyboard);
assert.isOk(chiral_keyboard);
assert.isOk(khmer_keyboard);

// This part provides assurance that the keyboard properly loaded.
assert.equal(lao_keyboard.id, "Keyboard_lao_2008_basic");
assert.equal(khmer_keyboard.id, "Keyboard_khmer_angkor");
assert.equal(chiral_keyboard.id, "Keyboard_test_chirality");

harness.activeKeyboard = lao_keyboard;
});
});
1 change: 1 addition & 0 deletions common/web/keyboard-processor/tests/node/basic-engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('Engine - Basic Simulation', function() {
let keyboardLoader = new NodeKeyboardLoader(new KeyboardInterface({}, MinimalKeymanGlobal));
let keyboard = await keyboardLoader.loadKeyboardFromPath('../../test/' + testSuite.keyboard.filename);
keyboardWithHarness = keyboardLoader.harness;
keyboardWithHarness.activeKeyboard = keyboard;

assert.equal(keyboard.id, "Keyboard_" + testSuite.keyboard.id);
// -- END: Standard Recorder-based unit test loading boilerplate --
Expand Down
1 change: 1 addition & 0 deletions common/web/keyboard-processor/tests/node/chirality.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('Engine - Chirality', function() {
let keyboardLoader = new NodeKeyboardLoader(new KeyboardInterface({}, MinimalKeymanGlobal));
let keyboard = await keyboardLoader.loadKeyboardFromPath('../../test/' + testSuite.keyboard.filename);
keyboardWithHarness = keyboardLoader.harness;
keyboardWithHarness.activeKeyboard = keyboard;

assert.equal(keyboard.id, "Keyboard_" + testSuite.keyboard.id);
// -- END: Standard Recorder-based unit test loading boilerplate --
Expand Down
1 change: 1 addition & 0 deletions common/web/keyboard-processor/tests/node/deadkeys.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('Engine - Deadkeys', function() {
let keyboardLoader = new NodeKeyboardLoader(new KeyboardInterface({}, MinimalKeymanGlobal));
let keyboard = await keyboardLoader.loadKeyboardFromPath('../../test/' + testSuite.keyboard.filename);
keyboardWithHarness = keyboardLoader.harness;
keyboardWithHarness.activeKeyboard = keyboard;

assert.equal(keyboard.id, "Keyboard_" + testSuite.keyboard.id);
// -- END: Standard Recorder-based unit test loading boilerplate --
Expand Down
3 changes: 2 additions & 1 deletion common/web/keyboard-processor/tests/node/engine/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -986,8 +986,9 @@ var NOTANY_NUL_RULE_SET = [ NOTANY_NUL_TEST_1, NOTANY_NUL_TEST_2, NOTANY_NUL_TES
describe('Engine - Context Matching', function() {
before(async function() {
let keyboardLoader = new NodeKeyboardLoader(new KeyboardInterface({}, MinimalKeymanGlobal));
await keyboardLoader.loadKeyboardFromPath(require.resolve('@keymanapp/common-test-resources/keyboards/test_simple_deadkeys.js'));
const keyboard = await keyboardLoader.loadKeyboardFromPath(require.resolve('@keymanapp/common-test-resources/keyboards/test_simple_deadkeys.js'));
keyboardWithHarness = keyboardLoader.harness;
keyboardWithHarness.activeKeyboard = keyboard;
});

// Tests "stage 1" of fullContextMatch - ensuring that a proper context index map is built.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ function runStringRuleSet(input, output) {
describe('Engine - notany() and context()', function() {
before(async function() {
let keyboardLoader = new NodeKeyboardLoader(new KeyboardInterface({}, MinimalKeymanGlobal));
await keyboardLoader.loadKeyboardFromPath(require.resolve('@keymanapp/common-test-resources/keyboards/test_917.js'));
const keyboard = await keyboardLoader.loadKeyboardFromPath(require.resolve('@keymanapp/common-test-resources/keyboards/test_917.js'));
keyboardWithHarness = keyboardLoader.harness;
keyboardWithHarness.activeKeyboard = keyboard;
});

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ describe('Engine - Unmatched Final Groups', function() {
before(async function() {
// -- START: Standard Recorder-based unit test loading boilerplate --
let keyboardLoader = new NodeKeyboardLoader(new KeyboardInterface({}, MinimalKeymanGlobal));
let keyboard = await keyboardLoader.loadKeyboardFromPath('../../test/' + testSuite.keyboard.filename);
const keyboard = await keyboardLoader.loadKeyboardFromPath('../../test/' + testSuite.keyboard.filename);
keyboardWithHarness = keyboardLoader.harness;
keyboardWithHarness.activeKeyboard = keyboard;

assert.equal(keyboard.id, "Keyboard_" + testSuite.keyboard.id);
// -- END: Standard Recorder-based unit test loading boilerplate --
Expand Down
Loading

0 comments on commit f48ecf0

Please sign in to comment.