Merge pull request #8270 from keymanapp/refactor/web/common-configuration

refactor(web): common config for both types of Keyman Engine for Web 🧩
This commit is contained in:
Joshua Horton 2023-02-24 08:15:32 +07:00 committed by GitHub
commit ba328a7cb1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 310 additions and 16 deletions

View file

@ -12,8 +12,7 @@ import { spawn } from 'child_process';
const commonConfig = {
bundle: true,
sourcemap: true,
tsconfig: 'tsconfig.json',
target: 'es5',
format: "esm",
// Sets 'common/web' as a root folder for module resolution;
// this allows the keyman-version and utils imports to resolve.
//

View file

@ -2,6 +2,10 @@
"name": "keyman",
"description": "Facilitates text input in any language.",
"exports": {
"./engine/configuration": {
"types": "./build/engine/configuration/obj/index.d.ts",
"import": "./build/engine/configuration/obj/index.js"
},
"./engine/device-detect": {
"types": "./build/engine/device-detect/obj/kmwdevice.d.ts",
"import": "./build/engine/device-detect/obj/kmwdevice.js"

View file

@ -24,6 +24,7 @@ cd "$THIS_SCRIPT_PATH"
# Definition of global compile constants
MAIN=engine/main # Covers all engine code, including submodules like those listed below.
CONFIGURATION=engine/configuration
DEVICEDETECT=engine/device-detect
DOMUTILS=engine/dom-utils
KEYBOARDCACHE=engine/keyboard-cache
@ -90,6 +91,7 @@ builder_describe "Builds engine modules for Keyman Engine for Web (KMW)." \
"clean" \
"configure" \
"build" \
":configuration Subset used to configure KMW" \
":device-detect Subset used for device-detection " \
":dom-utils A common subset of function used for DOM calculations, layout, etc" \
":element-wrappers Subset used to integrate with website elements" \
@ -102,13 +104,15 @@ builder_describe "Builds engine modules for Keyman Engine for Web (KMW)." \
builder_describe_outputs \
configure ../../../node_modules \
configure:configuration ../../../node_modules \
configure:device-detect ../../../node_modules \
configure:dom-utils ../../../node_modules \
configure:element-wrappers ../../../node_modules \
configure:keyboard-cache ../../../node_modules \
configure:main ../../../node_modules \
configure:osk ../../../node_modules \
build:device-detect $(output_path $DEVICEDETECT $OUTPUT_DIR)/index.js \
build:configuration $(output_path $CONFIGURATION $OUTPUT_DIR)/index.js \
build:device-detect $(output_path $DEVICEDETECT $OUTPUT_DIR)/kmwdevice.js \
build:dom-utils $(output_path $DOMUTILS $OUTPUT_DIR)/index.js \
build:element-wrappers $(output_path $ELEMENTWRAPPERS $OUTPUT_DIR)/index.js \
build:keyboard-cache $(output_path $KEYBOARDCACHE $OUTPUT_DIR)/index.js \
@ -193,6 +197,12 @@ if builder_start_action build:device-detect; then
builder_finish_action success build:device-detect
fi
if builder_start_action build:configuration; then
compile $CONFIGURATION
builder_finish_action success build:configuration
fi
if builder_start_action build:dom-utils; then
compile $DOMUTILS
@ -217,8 +227,8 @@ if builder_start_action build:osk; then
builder_finish_action success build:osk
fi
if builder_start_action build:main; then
compile $MAIN
# if builder_start_action build:main; then
# compile $MAIN
builder_finish_action success build:main
fi
# builder_finish_action success build:main
# fi

View file

@ -0,0 +1,42 @@
import { physicalKeyDeviceAlias } from "keyman/engine/device-detect";
import { DeviceSpec, type SpacebarText } from "@keymanapp/keyboard-processor";
import { OptionSpec } from "./optionSpec.interface.js";
import PathConfiguration from "./pathConfiguration.js";
export default class Configuration {
readonly paths: PathConfiguration;
readonly activateFirstKeyboard: boolean;
readonly defaultSpacebarText: SpacebarText;
readonly hostDevice: DeviceSpec;
readonly embeddingApp: string;
// sourcePath: see `var sPath =` in kmwbase.ts. It is not obtainable headlessly.
constructor(options: Required<OptionSpec>, device: DeviceSpec, sourcePath: string) {
this.paths = new PathConfiguration(options, sourcePath);
if(typeof options.setActiveOnRegister == 'boolean') {
this.activateFirstKeyboard = options.setActiveOnRegister;
} else if (typeof options.setActiveOnRegister == 'string') {
let str = options.setActiveOnRegister.toLowerCase();
this.activateFirstKeyboard = str === 'true';
} else {
this.activateFirstKeyboard = true;
}
this.defaultSpacebarText = options.spacebarText;
this.hostDevice = device;
if(options.embeddingApp) {
this.embeddingApp = options.embeddingApp;
}
}
get softDevice(): DeviceSpec {
return this.hostDevice;
}
get hardDevice(): DeviceSpec {
return physicalKeyDeviceAlias(this.hostDevice);
}
}

View file

@ -0,0 +1,3 @@
export { default as Configuration } from './configuration.js';
export { default as PathConfiguration } from './pathConfiguration.js';
export { OptionDefaults, OptionSpec, PathOptionSpec } from './optionSpec.interface.js';

View file

@ -0,0 +1,57 @@
import { SpacebarText } from "@keymanapp/keyboard-processor";
export interface PathOptionSpec {
/**
* If defined, specifies the root path of the default location hosting KMW resources.
* Is typically just the protocol + domain name.
*/
root?: string;
/**
* The base path to prepend on relative paths for other types of resources.
*/
resources?: string;
/**
* The base path to prepend on relative paths when loading keyboards.
*/
keyboards?: string;
/**
* The base path to prepend on relative paths when loading fonts.
*/
fonts?: string;
}
export interface OptionSpec extends PathOptionSpec {
/**
* May be used to denote the name of the embedding application
*/
embeddingApp?: string | undefined;
// ui?: string;
/**
* If set to true || "true" or if left undefined, the engine will automatically select the first available
* keyboard for activation.
*
* Note that keyboards specified locally are synchronously loaded while cloud keyboards are async; as a
* result, a locally-specified keyboard will generally be available "sooner", even if added "later".
*/
setActiveOnRegister?: string | boolean; // TODO: Convert to boolean. Option loader needs to be able to receive this as a string or boolean
/**
* Determines the default text shown on the spacebar. If undefined, uses `LANGUAGE_KEYBOARD`
*/
spacebarText?: SpacebarText;
}
export const OptionDefaults: Required<OptionSpec> = {
embeddingApp: undefined,
root: '',
resources: '',
keyboards: '',
fonts: '',
setActiveOnRegister: true,
spacebarText: SpacebarText.LANGUAGE_KEYBOARD,
}

View file

@ -0,0 +1,71 @@
import { PathOptionSpec } from "./optionSpec.interface.js";
export default class PathConfiguration {
readonly root: string;
readonly resources: string;
readonly keyboards: string;
readonly fonts: string;
readonly protocol: string;
constructor(pathSpec: Required<PathOptionSpec>, sourcePath: string) {
const addDelimiter = (p: string) => {
// Add delimiter if missing
if(p.substring(p.length-1, p.length) != '/') {
return p + '/';
} else {
return p;
}
}
sourcePath = addDelimiter(sourcePath);
const _rootPath = sourcePath.replace(/(https?:\/\/)([^\/]*)(.*)/,'$1$2/');
this.protocol = sourcePath.replace(/(.{3,5}:)(.*)/,'$1');
// Local function to convert relative to absolute URLs
// with respect to the source path, server root and protocol
const fixPath = (p: string) => {
if(p.length == 0) {
return p;
}
p = addDelimiter(p);
// Absolute
if((p.replace(/^(http)s?:.*/,'$1') == 'http') || (p.replace(/^(file):.*/,'$1') == 'file')) {
return p;
}
// Absolute (except for protocol)
if(p.substring(0,2) == '//') {
return this.protocol + p;
}
// Relative to server root
if(p.substring(0,1) == '/') {
return this.root + p.substring(1);
}
// Otherwise, assume relative to source path
return sourcePath + p;
}
// Get default paths and device options
this.root = _rootPath;
if(pathSpec.root != '') {
this.root = fixPath(pathSpec.root);
} else {
this.root = fixPath(_rootPath);
}
// Resources are located with respect to the engine by default
let resources = pathSpec.resources; // avoid mutating the parameter!
if(resources == '') {
resources = sourcePath;
}
// Convert resource, keyboard and font paths to absolute URLs
this.resources = fixPath(resources);
this.keyboards = fixPath(pathSpec.keyboards);
this.fonts = fixPath(pathSpec.fonts);
}
}

View file

@ -0,0 +1,24 @@
{
"extends": "../../../../tsconfig-base.json",
"compilerOptions": {
"allowJs": false,
"inlineSources": true,
"allowSyntheticDefaultImports": true,
"module": "es6",
"moduleResolution": "Node16",
"sourceMap": true,
"target": "es5",
"baseUrl": "./",
"outDir": "../../../build/engine/configuration/obj/",
"tsBuildInfoFile": "../../../build/engine/configuration/obj/tsconfig.tsbuildinfo",
"rootDir": "./src"
},
"include": [ "**/*.ts" ],
"references": [
{ "path": "../../../../common/web/keyman-version" },
{ "path": "../../../../common/web/utils" }
]
}

View file

@ -4,29 +4,34 @@ import { DeviceSpec, Version } from "@keymanapp/web-utils";
// The Device object definition -------------------------------------------------
export class Device {
// These correspond directly to the properties & parameters for `DeviceSpec`.
touchable: boolean;
OS: string;
formFactor: string;
dyPortrait: number;
dyLandscape: number;
version: string;
orientation: string|number;
browser: string;
colorScheme: 'light' | 'dark';
// These components aren't needed for key events. All but `version` could be a sort
// of `DeviceStyle`.
dyPortrait: number; // Its value is only referenced by an unused method.
dyLandscape: number; // Its value is only referenced by an unused method.
orientation: string|number; // Appears to be unused as well?
colorScheme: 'light' | 'dark'; // Also unused?
version: string; // As in, device version; only really persisted for Android.
// No real sign of actual use, though.
private detected: boolean = false;
private _styles: StyleConstants;
// Generates a default Device value.
constructor() {
this.touchable = !!('ontouchstart' in window);
this.OS = '';
this.formFactor='desktop';
this.browser='';
this.dyPortrait=0;
this.dyLandscape=0;
this.version='0';
this.orientation=window.orientation;
this.browser='';
}
/**
@ -200,4 +205,8 @@ export class Device {
}
}
export default Device;
export default Device;
export function physicalKeyDeviceAlias(device: DeviceSpec) {
return new DeviceSpec(device.browser, DeviceSpec.FormFactor.Desktop, device.OS, false);
}

View file

@ -5,6 +5,7 @@
"allowJs": false,
"declaration": true,
"inlineSources": true,
"allowSyntheticDefaultImports": true,
"module": "es6",
"moduleResolution": "Node16",
"sourceMap": true,
@ -12,7 +13,7 @@
"baseUrl": "./",
"outDir": "../../../build/engine/device-detect/obj/",
"tsBuildInfoFile": "../../../build/engine/device-detect/obj/tsconfig.tsbuildinfo",
"rootDir": "."
"rootDir": "./src"
},
"include": [ "**/*.ts" ],

View file

@ -39,6 +39,8 @@ namespace com.keyman {
resources?: string;
keyboards?: string;
fonts?: string;
// attachType and ui are 100% ignored for embedded (app-WebView hosted) contexts.
// They should only be expected for website-based KMW use.
attachType?: 'auto' | 'manual' | ''; // If blank or undefined, attachType will be assigned to "auto" or "manual"
ui?: string;
setActiveOnRegister?: string; // TODO: Convert to boolean. Option loader needs to be able to receive this as a string or boolean
@ -47,6 +49,7 @@ namespace com.keyman {
spacebarText?: SpacebarText;
// Determines whether or not KeymanWeb should display its own alert messages
// Only relevant for website-based KMW use.
useAlerts?: boolean;
}

View file

@ -0,0 +1,71 @@
import { assert } from 'chai';
import sinon from 'sinon';
import { OptionDefaults, PathConfiguration } from 'keyman/engine/configuration';
// Tests the activation-state logic abstraction & implementations used to model and control OSK visibility.
describe("Path Configuration", () => {
it('https://test.site.com/folder/, default options', () => {
const paths = new PathConfiguration(OptionDefaults, 'https://test.site.com/folder/');
assert.equal(paths.protocol, 'https:');
assert.equal(paths.root, 'https://test.site.com/');
assert.equal(paths.resources, 'https://test.site.com/folder/');
assert.equal(paths.keyboards, '');
assert.equal(paths.fonts, '');
});
it('https://test.site.com/folder, non-standard option values', () => {
const paths = new PathConfiguration({...OptionDefaults,
keyboards: 'https://s.keyman.com/keyboard',
fonts: '/fonts',
resources: 'resources',
root: '//something.or.other'
}, 'https://test.site.com/folder/');
assert.equal(paths.protocol, 'https:');
assert.equal(paths.root, 'https://something.or.other/');
assert.equal(paths.resources, 'https://test.site.com/folder/resources/');
assert.equal(paths.keyboards, 'https://s.keyman.com/keyboard/');
assert.equal(paths.fonts, 'https://something.or.other/fonts/');
});
it('http://test.site.com/folder, non-standard option values', () => {
const paths = new PathConfiguration({...OptionDefaults,
keyboards: 'https://s.keyman.com/keyboard',
fonts: '/fonts',
resources: 'resources',
root: '//something.or.other'
}, 'http://test.site.com/folder');
assert.equal(paths.protocol, 'http:');
assert.equal(paths.root, 'http://something.or.other/');
assert.equal(paths.resources, 'http://test.site.com/folder/resources/');
assert.equal(paths.keyboards, 'https://s.keyman.com/keyboard/');
assert.equal(paths.fonts, 'http://something.or.other/fonts/');
});
it('http://localhost/keymanweb/src/test/manual/web', () => {
const paths = new PathConfiguration(OptionDefaults, 'http://localhost/keymanweb/src/test/manual/web');
assert.equal(paths.protocol, 'http:');
assert.equal(paths.root, 'http://localhost/');
assert.equal(paths.resources, 'http://localhost/keymanweb/src/test/manual/web/');
assert.equal(paths.keyboards, '');
assert.equal(paths.fonts, '');
});
it('file:///C:/keymanapp/keyman/web/src/test/manual/web', () => {
const paths = new PathConfiguration({...OptionDefaults,
resources: '../../../../build/resources'
}, 'file:///C:/keymanapp/keyman/web/src/test/manual/web');
assert.equal(paths.protocol, 'file:');
// Is this proper? We just treat 'root' differently (by default) if being served via file:/// ?
assert.equal(paths.root, 'file:///C:/keymanapp/keyman/web/src/test/manual/web/');
assert.equal(paths.resources, 'file:///C:/keymanapp/keyman/web/src/test/manual/web/../../../../build/resources/');
assert.equal(paths.keyboards, '');
assert.equal(paths.fonts, '');
});
});