mirror of
https://github.com/keymanapp/keyman.git
synced 2026-10-15 18:37:32 +00:00
feat(web): refactor loading of .js keyboards
- split keyboard loading into loading into blob and then loading the script - look at first four bytes to see if it's a .js or a .kmx keyboard - for domKeyboardLoader, use fetch to get the blob, then use indirect eval to load the script, instead of injecting a script element. This does not yet implement the loading of .kmx keyboards.
This commit is contained in:
parent
68566af3f9
commit
f7f26e87f8
5 changed files with 125 additions and 130 deletions
|
|
@ -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 { default as KeyboardLoaderBase, } from "./keyboards/keyboardLoaderBase.js";
|
||||
export { KeyboardLoadErrorBuilder, KeyboardMissingError, KeyboardScriptError } from './keyboards/keyboardLoadError.js'
|
||||
export {
|
||||
CloudKeyboardFont,
|
||||
internalizeFont,
|
||||
|
|
|
|||
63
web/src/engine/keyboard/src/keyboards/keyboardLoadError.ts
Normal file
63
web/src/engine/keyboard/src/keyboards/keyboardLoadError.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { type KeyboardStub } from './keyboardLoaderBase.js';
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,69 +1,9 @@
|
|||
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 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 type KeyboardStub = KeyboardProperties & { filename: string };
|
||||
|
||||
export default abstract class KeyboardLoaderBase {
|
||||
private _harness: KeyboardHarness;
|
||||
|
|
@ -78,21 +18,29 @@ export default abstract class KeyboardLoaderBase {
|
|||
|
||||
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) {
|
||||
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 blob = await this.loadKeyboardBlob(uri);
|
||||
|
||||
const script = await blob.text();
|
||||
if (script.startsWith('KXTS', 0)) {
|
||||
// KMX or LDML (KMX+) keyboard
|
||||
console.error("KMX keyboard loading is not yet implemented!");
|
||||
return null;
|
||||
}
|
||||
|
||||
// .js keyboard
|
||||
return await this.loadKeyboardFromScript(script, errorBuilder);
|
||||
}
|
||||
|
||||
protected abstract loadKeyboardBlob(uri: string): Promise<Blob>;
|
||||
|
||||
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 { default as KeyboardLoaderBase } from '../keyboardLoaderBase.js';
|
||||
import { KeyboardLoadErrorBuilder, KeyboardMissingError } from '../keyboardLoadError.js';
|
||||
|
||||
export class DOMKeyboardLoader extends KeyboardLoaderBase {
|
||||
public readonly element: HTMLIFrameElement;
|
||||
|
|
@ -28,54 +29,23 @@ 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): Promise<Blob> {
|
||||
if (this.performCacheBusting) {
|
||||
uri = this.cacheBust(uri);
|
||||
}
|
||||
|
||||
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);
|
||||
const response = await fetch(uri);
|
||||
if (!response.ok) {
|
||||
throw new KeyboardMissingError(`Cannot find the keyboard at ${uri}.`, new Error(`HTTP ${response.status} ${response.statusText}`));
|
||||
}
|
||||
return response.blob();
|
||||
}
|
||||
|
||||
return promise.corePromise;
|
||||
protected async loadKeyboardFromScript(script: string, errorBuilder: KeyboardLoadErrorBuilder): Promise<Keyboard> {
|
||||
this.evalScriptInContext(script, this.harness._jsGlobal);
|
||||
const keyboard = this.harness.loadedKeyboard;
|
||||
this.harness.loadedKeyboard = null;
|
||||
return keyboard;
|
||||
}
|
||||
|
||||
private cacheBust(uri: string) {
|
||||
|
|
@ -84,4 +54,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,7 +1,10 @@
|
|||
import { Keyboard, KeyboardHarness, KeyboardLoaderBase, KeyboardLoadErrorBuilder, MinimalKeymanGlobal } from 'keyman/engine/keyboard';
|
||||
import { default as Keyboard } from '../keyboard.js';
|
||||
import { KeyboardHarness, MinimalKeymanGlobal } from '../keyboardHarness.js';
|
||||
import { default as KeyboardLoaderBase } from '../keyboardLoaderBase.js';
|
||||
import { KeyboardLoadErrorBuilder } from '../keyboardLoadError.js';
|
||||
|
||||
import vm from 'vm';
|
||||
import fs from 'fs';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { globalObject } from '@keymanapp/web-utils';
|
||||
|
||||
export class NodeKeyboardLoader extends KeyboardLoaderBase {
|
||||
|
|
@ -22,15 +25,20 @@ export class NodeKeyboardLoader extends KeyboardLoaderBase {
|
|||
}
|
||||
}
|
||||
|
||||
protected loadKeyboardInternal(uri: string, errorBuilder: KeyboardLoadErrorBuilder): Promise<Keyboard> {
|
||||
protected async loadKeyboardBlob(uri: string): Promise<Blob> {
|
||||
// `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);
|
||||
}
|
||||
|
||||
const buffer = await readFile(uri);
|
||||
return new Blob([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));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue