Merge pull request #7809 from keymanapp/change/common/web/keyboard-processor-modularization

change(common/web): keyboard processor package modularization 🧩
This commit is contained in:
Joshua Horton 2023-02-02 14:07:32 +07:00 committed by GitHub
commit caa4165aaf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 6197 additions and 6115 deletions

View file

@ -2804,7 +2804,7 @@ function Keyboard_khmer_angkor()
k.KO(-1,t,"»");
}
if(m) {
k.KDC(-1,t);
r=this.g_normalise(t,e);
}

View file

@ -325,7 +325,7 @@ function Keyboard_test_deadkeys()
k.KDO(-1,t,17);
}
if(m) {
k.KDC(-1,t);
r=this.g_dead_reorder(t,e);
}

View file

@ -16,7 +16,7 @@
"references": [
{ "path": "../../keyman-version" },
{ "path": "../../utils" },
{ "path": "../../keyboard-processor/src" },
{ "path": "../../keyboard-processor" },
{ "path": "../../../predictive-text/browser.tsconfig.json" },
],
"include": ["./**/*.ts"],

View file

@ -0,0 +1,43 @@
/*
* Note: while this file is not meant to exist long-term, it provides a nice
* low-level proof-of-concept for esbuild bundling of the various Web submodules.
*
* Add some extra code at the end of src/index.ts and run it to verify successful bundling!
*/
import esbuild from 'esbuild';
import { spawn } from 'child_process';
// Bundled ES module version
esbuild.buildSync({
entryPoints: ['build/obj/index.js'],
bundle: true,
sourcemap: true,
format: "esm",
// Sets 'common/web' as a root folder for module resolution;
// this allows the keyman-version and utils imports to resolve.
//
// We also need to point it at the nested build output folder to resolve in-project
// imports when compiled - esbuild doesn't seem to pick up on the shifted base.
nodePaths: ['..', "build/obj"],
outfile: "build/lib/index.mjs",
tsconfig: 'tsconfig.json',
target: "es5"
});
// Bundled CommonJS (classic Node) module version
esbuild.buildSync({
entryPoints: ['build/obj/index.js'],
bundle: true,
sourcemap: true,
format: "cjs",
// Sets 'common/web' as a root folder for module resolution;
// this allows the keyman-version and utils imports to resolve.
//
// We also need to point it at the nested build output folder to resolve in-project
// imports when compiled - esbuild doesn't seem to pick up on the shifted base.
nodePaths: ['..', "build/obj"],
outfile: "build/lib/index.cjs",
tsconfig: 'tsconfig.json',
target: "es5"
});

View file

@ -49,13 +49,16 @@ if builder_start_action clean; then
fi
if builder_start_action build; then
npm run tsc -- --build "$THIS_SCRIPT_PATH/src/tsconfig.json"
npm run tsc -- --build "$THIS_SCRIPT_PATH/tsconfig.json"
node ./build-bundler.js
# Declaration bundling.
npm run tsc -- --emitDeclarationOnly --outFile ./build/lib/index.d.ts
builder_finish_action success build
fi
if builder_start_action test; then
npm run tsc -- --build "$THIS_SCRIPT_PATH/src/tsconfig.bundled.json"
echo_heading "Running Keyboard Processor test suite"
FLAGS=

View file

@ -35,5 +35,6 @@
"@keymanapp/keyman-version": "*",
"@keymanapp/web-utils": "*",
"@types/node": "^11.9.4"
}
},
"type": "module"
}

View file

@ -0,0 +1,29 @@
export * from "./keyboards/activeLayout.js";
export * from "./keyboards/defaultLayouts.js";
export { default as Keyboard } from "./keyboards/keyboard.js";
export * from "./keyboards/keyboard.js";
export { default as Codes } from "./text/codes.js";
export * from "./text/codes.js";
export * from "./text/deadkeys.js";
export { default as DefaultOutput } from "./text/defaultOutput.js";
export * from "./text/defaultOutput.js";
export { default as KeyboardInterface } from "./text/kbdInterface.js";
export * from "./text/kbdInterface.js";
export { default as KeyboardProcessor } from "./text/keyboardProcessor.js";
export * from "./text/keyboardProcessor.js";
export { default as KeyEvent } from "./text/keyEvent.js";
export * from "./text/keyEvent.js";
export { default as KeyMapping } from "./text/keyMapping.js";
export { default as OutputTarget } from "./text/outputTarget.js";
export * from "./text/outputTarget.js";
export { default as RuleBehavior } from "./text/ruleBehavior.js";
export * from "./text/systemStores.js";
export * from "@keymanapp/web-utils/build/obj/index.js";
// At the top level, there should be no default export.
// Without the line below... OutputTarget would likely be aliased there, as it's
// the last `export { default as _ }` => `export * from` pairing seen above.
export default undefined;

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,448 +1,452 @@
/// <reference path="defaultLayouts.ts" />
/// <reference path="activeLayout.ts" />
/// <reference path="../text/kbdInterface.ts" />
import Codes from "../text/codes.js";
import { Layouts, type LayoutFormFactor } from "./defaultLayouts.js";
import { ActiveLayout } from "./activeLayout.js";
import type KeyEvent from "../text/keyEvent.js";
import type OutputTarget from "../text/outputTarget.js";
namespace com.keyman.keyboards {
/**
* Stores preprocessed properties of a keyboard for quick retrieval later.
*/
class CacheTag {
stores: {[storeName: string]: text.ComplexKeyboardStore};
import type { ComplexKeyboardStore } from "../text/kbdInterface.js";
constructor() {
this.stores = {};
}
import { Version, DeviceSpec } from "@keymanapp/web-utils/build/obj/index.js";
/**
* Stores preprocessed properties of a keyboard for quick retrieval later.
*/
class CacheTag {
stores: {[storeName: string]: ComplexKeyboardStore};
constructor() {
this.stores = {};
}
}
export enum LayoutState {
NOT_LOADED = undefined,
POLYFILLED = 1,
CALIBRATED = 2
}
export interface VariableStoreDictionary {
[name: string]: string;
};
/**
* Acts as a wrapper class for Keyman keyboards compiled to JS, providing type information
* and keyboard-centered functionality in an object-oriented way without modifying the
* wrapped keyboard itself.
*/
export default class Keyboard {
public static DEFAULT_SCRIPT_OBJECT = {
'gs': function(outputTarget, keystroke) { return false; }, // no matching rules; rely on defaultRuleOutput entirely
'KI': '', // The currently-existing default keyboard ID; we already have checks that focus against this.
'KN': '',
'KV': Layouts.DEFAULT_RAW_SPEC,
'KM': 0 // May not be the best default, but this matches current behavior when there is no activeKeyboard.
}
export enum LayoutState {
NOT_LOADED = undefined,
POLYFILLED = 1,
CALIBRATED = 2
/**
* This is the object provided to KeyboardInterface.registerKeyboard - that is, the keyboard
* being wrapped.
*
* TODO: Make this private instead. But there are a LOT of references that must be rooted out first.
*/
public readonly scriptObject: any;
private layoutStates: {[layout: string]: LayoutState};
constructor(keyboardScript: any) {
if(keyboardScript) {
this.scriptObject = keyboardScript;
} else {
this.scriptObject = Keyboard.DEFAULT_SCRIPT_OBJECT;
}
this.layoutStates = {};
}
export interface VariableStoreDictionary {
[name: string]: string;
};
/**
* Calls the keyboard's `gs` function, which represents the keyboard source's begin Unicode group.
*/
process(outputTarget: OutputTarget, keystroke: KeyEvent): boolean {
return this.scriptObject['gs'](outputTarget, keystroke);
}
/**
* Acts as a wrapper class for Keyman keyboards compiled to JS, providing type information
* and keyboard-centered functionality in an object-oriented way without modifying the
* wrapped keyboard itself.
* Calls the keyboard's `gn` function, which represents the keyboard source's begin newContext group.
*/
export class Keyboard {
public static DEFAULT_SCRIPT_OBJECT = {
'gs': function(outputTarget, keystroke) { return false; }, // no matching rules; rely on defaultRuleOutput entirely
'KI': '', // The currently-existing default keyboard ID; we already have checks that focus against this.
'KN': '',
'KV': Layouts.DEFAULT_RAW_SPEC,
'KM': 0 // May not be the best default, but this matches current behavior when there is no activeKeyboard.
}
processNewContextEvent(outputTarget: OutputTarget, keystroke: KeyEvent): boolean {
return this.scriptObject['gn'] ? this.scriptObject['gn'](outputTarget, keystroke) : false;
}
/**
* This is the object provided to KeyboardInterface.registerKeyboard - that is, the keyboard
* being wrapped.
*
* TODO: Make this private instead. But there are a LOT of references that must be rooted out first.
*/
public readonly scriptObject: any;
private layoutStates: {[layout: string]: LayoutState};
/**
* Calls the keyboard's `gpk` function, which represents the keyboard source's begin postKeystroke group.
*/
processPostKeystroke(outputTarget: OutputTarget, keystroke: KeyEvent): boolean {
return this.scriptObject['gpk'] ? this.scriptObject['gpk'](outputTarget, keystroke) : false;
}
constructor(keyboardScript: any) {
if(keyboardScript) {
this.scriptObject = keyboardScript;
} else {
this.scriptObject = Keyboard.DEFAULT_SCRIPT_OBJECT;
get isHollow(): boolean {
return this.scriptObject == Keyboard.DEFAULT_SCRIPT_OBJECT;
}
get id(): string {
return this.scriptObject['KI'];
}
get name(): string {
return this.scriptObject['KN'];
}
/**
* Cache variable store values
*
* Primarily used for predictive text to prevent variable store
* values from being changed in 'fat finger' processing.
*
* KVS is available in keyboards compiled with Keyman Developer 15
* and later versions. See #2924.
*
* @returns an object with each property referencing a variable store
*/
get variableStores(): VariableStoreDictionary {
const storeNames = this.scriptObject['KVS'];
let values = {};
if(Array.isArray(storeNames)) {
for(let store of storeNames) {
values[store] = this.scriptObject[store];
}
this.layoutStates = {};
}
return values;
}
/**
* Calls the keyboard's `gs` function, which represents the keyboard source's begin Unicode group.
*/
process(outputTarget: text.OutputTarget, keystroke: text.KeyEvent): boolean {
return this.scriptObject['gs'](outputTarget, keystroke);
}
/**
* Calls the keyboard's `gn` function, which represents the keyboard source's begin newContext group.
*/
processNewContextEvent(outputTarget: text.OutputTarget, keystroke: text.KeyEvent): boolean {
return this.scriptObject['gn'] ? this.scriptObject['gn'](outputTarget, keystroke) : false;
}
/**
* Calls the keyboard's `gpk` function, which represents the keyboard source's begin postKeystroke group.
*/
processPostKeystroke(outputTarget: text.OutputTarget, keystroke: text.KeyEvent): boolean {
return this.scriptObject['gpk'] ? this.scriptObject['gpk'](outputTarget, keystroke) : false;
}
get isHollow(): boolean {
return this.scriptObject == Keyboard.DEFAULT_SCRIPT_OBJECT;
}
get id(): string {
return this.scriptObject['KI'];
}
get name(): string {
return this.scriptObject['KN'];
}
/**
* Cache variable store values
*
* Primarily used for predictive text to prevent variable store
* values from being changed in 'fat finger' processing.
*
* KVS is available in keyboards compiled with Keyman Developer 15
* and later versions. See #2924.
*
* @returns an object with each property referencing a variable store
*/
get variableStores(): VariableStoreDictionary {
const storeNames = this.scriptObject['KVS'];
let values = {};
if(Array.isArray(storeNames)) {
for(let store of storeNames) {
values[store] = this.scriptObject[store];
}
}
return values;
}
/**
* Restore variable store values from cache
*
* KVS is available in keyboards compiled with Keyman Developer 15
* and later versions. See #2924.
*
* @param values name-value pairs for each store value
*/
set variableStores(values: VariableStoreDictionary) {
const storeNames = this.scriptObject['KVS'];
if(Array.isArray(storeNames)) {
for(let store of storeNames) {
// If the value is not present in the cache, don't overwrite it;
// while this is not used in initial implementation, we could use
// it in future to update a single variable store value rather than
// the whole cache.
if(typeof values[store] == 'string') {
this.scriptObject[store] = values[store];
}
/**
* Restore variable store values from cache
*
* KVS is available in keyboards compiled with Keyman Developer 15
* and later versions. See #2924.
*
* @param values name-value pairs for each store value
*/
set variableStores(values: VariableStoreDictionary) {
const storeNames = this.scriptObject['KVS'];
if(Array.isArray(storeNames)) {
for(let store of storeNames) {
// If the value is not present in the cache, don't overwrite it;
// while this is not used in initial implementation, we could use
// it in future to update a single variable store value rather than
// the whole cache.
if(typeof values[store] == 'string') {
this.scriptObject[store] = values[store];
}
}
}
}
// TODO: Better typing.
private get _legacyLayoutSpec(): any {
return this.scriptObject['KV']; // used with buildDefaultLayout; layout must be constructed at runtime.
// TODO: Better typing.
private get _legacyLayoutSpec(): any {
return this.scriptObject['KV']; // used with buildDefaultLayout; layout must be constructed at runtime.
}
// May return null if no layouts exist or have been initialized.
private get _layouts(): {[formFactor: string]: LayoutFormFactor} {
return this.scriptObject['KVKL']; // This one is compiled by Developer's visual keyboard layout editor.
}
private set _layouts(value) {
this.scriptObject['KVKL'] = value;
}
get compilerVersion(): Version {
return new Version(this.scriptObject['KVER']);
}
get isMnemonic(): boolean {
return !!this.scriptObject['KM'];
}
get definesPositionalOrMnemonic(): boolean {
return typeof this.scriptObject['KM'] != 'undefined';
}
/**
* HTML help text, as specified by either the &kmw_helptext or &kmw_helpfile system stores.
*
* Reference: https://help.keyman.com/developer/language/reference/kmw_helptext,
* https://help.keyman.com/developer/language/reference/kmw_helpfile
*/
get helpText(): string {
return this.scriptObject['KH'];
}
/**
* Embedded JS script designed for use with a keyboard's HTML help text. Always defined
* within the file referenced by &kmw_embedjs in a keyboard's source, though that file
* may also contain _other_ script definitions as well. (`KHF` must be explicitly defined
* within that file.)
*/
get hasScript(): boolean {
return !!this.scriptObject['KHF'];
}
/**
* Embeds a custom script for use by the OSK, which may be interactive (like with sil_euro_latin).
* Note: this must be called AFTER any contents of `helpText` have been inserted into the DOM.
* (See sil_euro_latin's source -> sil_euro_latin_js.txt)
*
* Reference: https://help.keyman.com/developer/language/reference/kmw_embedjs
*/
embedScript(e: any) {
// e: Expects the OSKManager's _Box element. We don't add type info here b/c it would
// reference the DOM.
this.scriptObject['KHF'](e);
}
get oskStyling(): string {
return this.scriptObject['KCSS'];
}
/**
* true if this keyboard uses a (legacy) pick list (Chinese, Japanese, Korean, etc.)
*
* TODO: Make a property on keyboards (say, `isPickList` / `KPL`) to signal this when we
* get around to better, generalized picker-list support.
*/
get isCJK(): boolean { // I3363 (Build 301)
var lg: string;
if(typeof(this.scriptObject['KLC']) != 'undefined') {
lg = this.scriptObject['KLC'];
} else if(typeof(this.scriptObject['LanguageCode']) != 'undefined') {
lg = this.scriptObject['LanguageCode'];
}
// May return null if no layouts exist or have been initialized.
private get _layouts(): {[formFactor: string]: LayoutFormFactor} {
return this.scriptObject['KVKL']; // This one is compiled by Developer's visual keyboard layout editor.
// While some of these aren't proper BCP-47 language codes, the CJK keyboards predate our use of BCP-47.
// So, we preserve the old ISO 639-3 codes, as that's what the keyboards are matching against.
return ((lg == 'cmn') || (lg == 'jpn') || (lg == 'kor'));
}
get isRTL(): boolean {
return !!this.scriptObject['KRTL'];
}
/**
* Obtains the currently-active modifier bitmask for the active keyboard.
*/
get modifierBitmask(): number {
// NON_CHIRAL is the default bitmask if KMBM is not defined.
// We always need a bitmask to compare against, as seen in `isChiral`.
return this.scriptObject['KMBM'] || Codes.modifierBitmasks['NON_CHIRAL'];
}
get isChiral(): boolean {
return !!(this.modifierBitmask & Codes.modifierBitmasks['IS_CHIRAL']);
}
get desktopFont(): string {
if(this.scriptObject['KV']) {
return this.scriptObject['KV']['F'];
} else {
return null;
}
}
private get cacheTag(): CacheTag {
let tag = this.scriptObject['_kmw'];
if(!tag) {
tag = new CacheTag();
this.scriptObject['_kmw'] = tag;
}
private set _layouts(value) {
this.scriptObject['KVKL'] = value;
return tag;
}
get explodedStores(): {[storeName: string]: ComplexKeyboardStore} {
return this.cacheTag.stores;
}
/**
* Signifies whether or not a layout or OSK should include AltGr / Right-alt emulation for this keyboard.
* @param {Object=} keyLabels
* @return {boolean}
*/
get emulatesAltGr(): boolean {
let modifierCodes = Codes.modifierCodes;
// If we're not chiral, we're not emulating.
if(!this.isChiral) {
return false;
}
get compilerVersion(): utils.Version {
return new utils.Version(this.scriptObject['KVER']);
if(this._legacyLayoutSpec == null) {
return false;
}
get isMnemonic(): boolean {
return !!this.scriptObject['KM'];
// Only exists in KMW 10.0+, but before that Web had no chirality support, so... return false.
let layers = this._legacyLayoutSpec['KLS'];
if(!layers) {
return false;
}
get definesPositionalOrMnemonic(): boolean {
return typeof this.scriptObject['KM'] != 'undefined';
var emulationMask = modifierCodes['LCTRL'] | modifierCodes['LALT'];
var unshiftedEmulationLayer = layers[Layouts.getLayerId(emulationMask)];
var shiftedEmulationLayer = layers[Layouts.getLayerId(modifierCodes['SHIFT'] | emulationMask)];
// buildDefaultLayout ensures that these are aliased to the original modifier set being emulated.
// As a result, we can directly test for reference equality.
//
// This allows us to still return `true` after creating the layers for emulation; during keyboard
// construction, the two layers should be null for AltGr emulation to succeed.
if(unshiftedEmulationLayer != null &&
unshiftedEmulationLayer != layers[Layouts.getLayerId(modifierCodes['RALT'])]) {
return false;
}
/**
* HTML help text, as specified by either the &kmw_helptext or &kmw_helpfile system stores.
*
* Reference: https://help.keyman.com/developer/language/reference/kmw_helptext,
* https://help.keyman.com/developer/language/reference/kmw_helpfile
*/
get helpText(): string {
return this.scriptObject['KH'];
if(shiftedEmulationLayer != null &&
shiftedEmulationLayer != layers[Layouts.getLayerId(modifierCodes['RALT'] | modifierCodes['SHIFT'])]) {
return false;
}
/**
* Embedded JS script designed for use with a keyboard's HTML help text. Always defined
* within the file referenced by &kmw_embedjs in a keyboard's source, though that file
* may also contain _other_ script definitions as well. (`KHF` must be explicitly defined
* within that file.)
*/
get hasScript(): boolean {
return !!this.scriptObject['KHF'];
}
/**
* Embeds a custom script for use by the OSK, which may be interactive (like with sil_euro_latin).
* Note: this must be called AFTER any contents of `helpText` have been inserted into the DOM.
* (See sil_euro_latin's source -> sil_euro_latin_js.txt)
*
* Reference: https://help.keyman.com/developer/language/reference/kmw_embedjs
*/
embedScript(e: any) {
// e: Expects the OSKManager's _Box element. We don't add type info here b/c it would
// reference the DOM.
this.scriptObject['KHF'](e);
}
get oskStyling(): string {
return this.scriptObject['KCSS'];
}
/**
* true if this keyboard uses a (legacy) pick list (Chinese, Japanese, Korean, etc.)
*
* TODO: Make a property on keyboards (say, `isPickList` / `KPL`) to signal this when we
* get around to better, generalized picker-list support.
*/
get isCJK(): boolean { // I3363 (Build 301)
var lg: string;
if(typeof(this.scriptObject['KLC']) != 'undefined') {
lg = this.scriptObject['KLC'];
} else if(typeof(this.scriptObject['LanguageCode']) != 'undefined') {
lg = this.scriptObject['LanguageCode'];
}
// While some of these aren't proper BCP-47 language codes, the CJK keyboards predate our use of BCP-47.
// So, we preserve the old ISO 639-3 codes, as that's what the keyboards are matching against.
return ((lg == 'cmn') || (lg == 'jpn') || (lg == 'kor'));
}
get isRTL(): boolean {
return !!this.scriptObject['KRTL'];
}
/**
* Obtains the currently-active modifier bitmask for the active keyboard.
*/
get modifierBitmask(): number {
// NON_CHIRAL is the default bitmask if KMBM is not defined.
// We always need a bitmask to compare against, as seen in `isChiral`.
return this.scriptObject['KMBM'] || text.Codes.modifierBitmasks['NON_CHIRAL'];
}
get isChiral(): boolean {
return !!(this.modifierBitmask & text.Codes.modifierBitmasks['IS_CHIRAL']);
}
get desktopFont(): string {
if(this.scriptObject['KV']) {
return this.scriptObject['KV']['F'];
} else {
return null;
}
}
private get cacheTag(): CacheTag {
let tag = this.scriptObject['_kmw'];
if(!tag) {
tag = new CacheTag();
this.scriptObject['_kmw'] = tag;
}
return tag;
}
get explodedStores(): {[storeName: string]: text.ComplexKeyboardStore} {
return this.cacheTag.stores;
}
/**
* Signifies whether or not a layout or OSK should include AltGr / Right-alt emulation for this keyboard.
* @param {Object=} keyLabels
* @return {boolean}
*/
get emulatesAltGr(): boolean {
let modifierCodes = text.Codes.modifierCodes;
// If we're not chiral, we're not emulating.
if(!this.isChiral) {
return false;
}
if(this._legacyLayoutSpec == null) {
return false;
}
// Only exists in KMW 10.0+, but before that Web had no chirality support, so... return false.
let layers = this._legacyLayoutSpec['KLS'];
if(!layers) {
return false;
}
var emulationMask = modifierCodes['LCTRL'] | modifierCodes['LALT'];
var unshiftedEmulationLayer = layers[Layouts.getLayerId(emulationMask)];
var shiftedEmulationLayer = layers[Layouts.getLayerId(modifierCodes['SHIFT'] | emulationMask)];
// buildDefaultLayout ensures that these are aliased to the original modifier set being emulated.
// As a result, we can directly test for reference equality.
//
// This allows us to still return `true` after creating the layers for emulation; during keyboard
// construction, the two layers should be null for AltGr emulation to succeed.
if(unshiftedEmulationLayer != null &&
unshiftedEmulationLayer != layers[Layouts.getLayerId(modifierCodes['RALT'])]) {
return false;
}
if(shiftedEmulationLayer != null &&
shiftedEmulationLayer != layers[Layouts.getLayerId(modifierCodes['RALT'] | modifierCodes['SHIFT'])]) {
return false;
}
// It's technically possible for the OSK to not specify anything while allowing chiral input. A last-ditch catch:
var bitmask = this.modifierBitmask;
if((bitmask & emulationMask) != emulationMask) {
// At least one of the emulation modifiers is never used by the keyboard! We can confirm everything's safe.
return true;
}
if(unshiftedEmulationLayer == null && shiftedEmulationLayer == null) {
// We've run out of things to go on; we can't detect if chiral AltGr emulation is intended or not.
// TODO: handle this again!
// if(!osk.altGrWarning) {
// console.warn("Could not detect if AltGr emulation is safe, but defaulting to active emulation!")
// // Avoid spamming the console with warnings on every call of the method.
// osk.altGrWarning = true;
// }
return true;
}
// It's technically possible for the OSK to not specify anything while allowing chiral input. A last-ditch catch:
var bitmask = this.modifierBitmask;
if((bitmask & emulationMask) != emulationMask) {
// At least one of the emulation modifiers is never used by the keyboard! We can confirm everything's safe.
return true;
}
get usesSupplementaryPlaneChars(): boolean {
let kbd = this.scriptObject;
// I3319 - SMP extension, I3363 (Build 301)
return kbd && ((kbd['KS'] && kbd['KS'] == 1) || kbd['KN'] == 'Hieroglyphic');
if(unshiftedEmulationLayer == null && shiftedEmulationLayer == null) {
// We've run out of things to go on; we can't detect if chiral AltGr emulation is intended or not.
// TODO: handle this again!
// if(!osk.altGrWarning) {
// console.warn("Could not detect if AltGr emulation is safe, but defaulting to active emulation!")
// // Avoid spamming the console with warnings on every call of the method.
// osk.altGrWarning = true;
// }
return true;
}
return true;
}
usesDesktopLayoutOnDevice(device: utils.DeviceSpec) {
if(this.scriptObject['KVKL']) {
// A custom mobile layout is defined... but are we using it?
return device.formFactor == utils.FormFactor.Desktop;
} else {
return true;
}
}
get usesSupplementaryPlaneChars(): boolean {
let kbd = this.scriptObject;
// I3319 - SMP extension, I3363 (Build 301)
return kbd && ((kbd['KS'] && kbd['KS'] == 1) || kbd['KN'] == 'Hieroglyphic');
}
/**
* @param {number} _PCommand event code (16,17,18) or 0
* @param {Object} _PTarget target element
* @param {number} _PData 1 or 0
* Notifies keyboard of keystroke or other event
*/
notify(_PCommand: number, _PTarget: text.OutputTarget, _PData: number) { // I2187
// Good example use case - the Japanese CJK-picker keyboard
if(typeof(this.scriptObject['KNS']) == 'function') {
this.scriptObject['KNS'](_PCommand, _PTarget, _PData);
}
}
private findOrConstructLayout(formFactor: utils.FormFactor): LayoutFormFactor {
if(this._layouts) {
// Search for viable layouts. `null` is allowed for desktop form factors when help text is available,
// so we check explicitly against `undefined`.
if(this._layouts[formFactor] !== undefined) {
return this._layouts[formFactor];
} else if(formFactor == utils.FormFactor.Phone && this._layouts[utils.FormFactor.Tablet]) {
return this._layouts[utils.FormFactor.Phone] = this._layouts[utils.FormFactor.Tablet];
} else if(formFactor == utils.FormFactor.Tablet && this._layouts[utils.FormFactor.Phone]) {
return this._layouts[utils.FormFactor.Tablet] = this._layouts[utils.FormFactor.Phone];
}
}
// No pre-built layout available; time to start constructing it via defaults.
// First, if we have non-default keys specified by the ['BK'] array, we've got
// enough to work with to build a default layout.
let rawSpecifications: any = null; // TODO: better typing, same type as this._legacyLayoutSpec.
if(this._legacyLayoutSpec != null && this._legacyLayoutSpec['KLS']) { // KLS is only specified whenever there are non-default keys.
rawSpecifications = this._legacyLayoutSpec;
} else if(this._legacyLayoutSpec != null && this._legacyLayoutSpec['BK'] != null) {
var keyCaps=this._legacyLayoutSpec['BK'];
for(var i=0; i<keyCaps.length; i++) {
if(keyCaps[i].length > 0) {
rawSpecifications = this._legacyLayoutSpec;
break;
}
}
}
// If we don't have key definitions to use for a layout but also lack help text or are a touch-based layout,
// we make a default layout anyway. We have to show display something usable.
if(!rawSpecifications && (this.helpText == '' || formFactor != utils.FormFactor.Desktop)) {
rawSpecifications = {'F':'Tahoma', 'BK': Layouts.dfltText};
}
// Regardless of success, we'll want to initialize the field that backs the property;
// may as well cache the default layout we just built, or a 'null' if it shouldn't exist..
if(!this._layouts) {
this._layouts = {};
}
// Final check - do we construct a layout, or is this a case where helpText / insertHelpHTML should take over?
if(rawSpecifications) {
// Now to generate a layout from our raw specifications.
let layout = this._layouts[formFactor] = Layouts.buildDefaultLayout(rawSpecifications, this, formFactor);
layout.isDefault = true;
return layout;
} else {
// The fact that it doesn't exist will indicate that help text/HTML should be inserted instead.
this._layouts[formFactor] = null; // provides a cached value for the check at the top of this method.
return null;
}
}
/**
* Returns an ActiveLayout object representing the keyboard's layout for this form factor. May return null if a custom desktop "help" OSK is defined, as with sil_euro_latin.
*
* In such cases, please use either `helpText` or `insertHelpHTML` instead.
* @param formFactor {string} The desired form factor for the layout.
*/
public layout(formFactor: utils.FormFactor): ActiveLayout {
let rawLayout = this.findOrConstructLayout(formFactor);
if(rawLayout) {
// Prevents accidentally reprocessing layouts; it's a simple enough check.
if(this.layoutStates[formFactor] == LayoutState.NOT_LOADED) {
rawLayout = ActiveLayout.polyfill(rawLayout, this, formFactor);
this.layoutStates[formFactor] = LayoutState.POLYFILLED;
}
return rawLayout as ActiveLayout;
} else {
return null;
}
}
public refreshLayouts() {
let formFactors = [ utils.FormFactor.Desktop, utils.FormFactor.Phone, utils.FormFactor.Tablet ];
let _this = this;
formFactors.forEach(function(form) {
// Currently doesn't work if we reset it to POLYFILLED, likely due to how 'calibration'
// currently works.
_this.layoutStates[form] = LayoutState.NOT_LOADED;
});
}
public markLayoutCalibrated(formFactor: utils.FormFactor) {
if(this.layoutStates[formFactor] != LayoutState.NOT_LOADED) {
this.layoutStates[formFactor] = LayoutState.CALIBRATED;
}
}
public getLayoutState(formFactor: utils.FormFactor) {
return this.layoutStates[formFactor];
usesDesktopLayoutOnDevice(device: DeviceSpec) {
if(this.scriptObject['KVKL']) {
// A custom mobile layout is defined... but are we using it?
return device.formFactor == DeviceSpec.FormFactor.Desktop;
} else {
return true;
}
}
/**
* @param {number} _PCommand event code (16,17,18) or 0
* @param {Object} _PTarget target element
* @param {number} _PData 1 or 0
* Notifies keyboard of keystroke or other event
*/
notify(_PCommand: number, _PTarget: OutputTarget, _PData: number) { // I2187
// Good example use case - the Japanese CJK-picker keyboard
if(typeof(this.scriptObject['KNS']) == 'function') {
this.scriptObject['KNS'](_PCommand, _PTarget, _PData);
}
}
private findOrConstructLayout(formFactor: DeviceSpec.FormFactor): LayoutFormFactor {
if(this._layouts) {
// Search for viable layouts. `null` is allowed for desktop form factors when help text is available,
// so we check explicitly against `undefined`.
if(this._layouts[formFactor] !== undefined) {
return this._layouts[formFactor];
} else if(formFactor == DeviceSpec.FormFactor.Phone && this._layouts[DeviceSpec.FormFactor.Tablet]) {
return this._layouts[DeviceSpec.FormFactor.Phone] = this._layouts[DeviceSpec.FormFactor.Tablet];
} else if(formFactor == DeviceSpec.FormFactor.Tablet && this._layouts[DeviceSpec.FormFactor.Phone]) {
return this._layouts[DeviceSpec.FormFactor.Tablet] = this._layouts[DeviceSpec.FormFactor.Phone];
}
}
// No pre-built layout available; time to start constructing it via defaults.
// First, if we have non-default keys specified by the ['BK'] array, we've got
// enough to work with to build a default layout.
let rawSpecifications: any = null; // TODO: better typing, same type as this._legacyLayoutSpec.
if(this._legacyLayoutSpec != null && this._legacyLayoutSpec['KLS']) { // KLS is only specified whenever there are non-default keys.
rawSpecifications = this._legacyLayoutSpec;
} else if(this._legacyLayoutSpec != null && this._legacyLayoutSpec['BK'] != null) {
var keyCaps=this._legacyLayoutSpec['BK'];
for(var i=0; i<keyCaps.length; i++) {
if(keyCaps[i].length > 0) {
rawSpecifications = this._legacyLayoutSpec;
break;
}
}
}
// If we don't have key definitions to use for a layout but also lack help text or are a touch-based layout,
// we make a default layout anyway. We have to show display something usable.
if(!rawSpecifications && (this.helpText == '' || formFactor != DeviceSpec.FormFactor.Desktop)) {
rawSpecifications = {'F':'Tahoma', 'BK': Layouts.dfltText};
}
// Regardless of success, we'll want to initialize the field that backs the property;
// may as well cache the default layout we just built, or a 'null' if it shouldn't exist..
if(!this._layouts) {
this._layouts = {};
}
// Final check - do we construct a layout, or is this a case where helpText / insertHelpHTML should take over?
if(rawSpecifications) {
// Now to generate a layout from our raw specifications.
let layout = this._layouts[formFactor] = Layouts.buildDefaultLayout(rawSpecifications, this, formFactor);
layout.isDefault = true;
return layout;
} else {
// The fact that it doesn't exist will indicate that help text/HTML should be inserted instead.
this._layouts[formFactor] = null; // provides a cached value for the check at the top of this method.
return null;
}
}
/**
* Returns an ActiveLayout object representing the keyboard's layout for this form factor. May return null if a custom desktop "help" OSK is defined, as with sil_euro_latin.
*
* In such cases, please use either `helpText` or `insertHelpHTML` instead.
* @param formFactor {string} The desired form factor for the layout.
*/
public layout(formFactor: DeviceSpec.FormFactor): ActiveLayout {
let rawLayout = this.findOrConstructLayout(formFactor);
if(rawLayout) {
// Prevents accidentally reprocessing layouts; it's a simple enough check.
if(this.layoutStates[formFactor] == LayoutState.NOT_LOADED) {
rawLayout = ActiveLayout.polyfill(rawLayout, this, formFactor);
this.layoutStates[formFactor] = LayoutState.POLYFILLED;
}
return rawLayout as ActiveLayout;
} else {
return null;
}
}
public refreshLayouts() {
let formFactors = [ DeviceSpec.FormFactor.Desktop, DeviceSpec.FormFactor.Phone, DeviceSpec.FormFactor.Tablet ];
let _this = this;
formFactors.forEach(function(form) {
// Currently doesn't work if we reset it to POLYFILLED, likely due to how 'calibration'
// currently works.
_this.layoutStates[form] = LayoutState.NOT_LOADED;
});
}
public markLayoutCalibrated(formFactor: DeviceSpec.FormFactor) {
if(this.layoutStates[formFactor] != LayoutState.NOT_LOADED) {
this.layoutStates[formFactor] = LayoutState.CALIBRATED;
}
}
public getLayoutState(formFactor: DeviceSpec.FormFactor) {
return this.layoutStates[formFactor];
}
}

View file

@ -1,103 +1,103 @@
namespace com.keyman.text {
export var Codes = {
// Define Keyman Developer modifier bit-flags (exposed for use by other modules)
// Compare against /common/include/kmx_file.h. CTRL+F "#define LCTRLFLAG" to find the secton.
modifierCodes: {
"LCTRL":0x0001, // LCTRLFLAG
"RCTRL":0x0002, // RCTRLFLAG
"LALT":0x0004, // LALTFLAG
"RALT":0x0008, // RALTFLAG
"SHIFT":0x0010, // K_SHIFTFLAG
"CTRL":0x0020, // K_CTRLFLAG
"ALT":0x0040, // K_ALTFLAG
// TENTATIVE: Represents command keys, which some OSes use for shortcuts we don't
// want to block. No rule will ever target a modifier set with this bit set to 1.
"META":0x0080, // K_METAFLAG
"CAPS":0x0100, // CAPITALFLAG
"NO_CAPS":0x0200, // NOTCAPITALFLAG
"NUM_LOCK":0x0400, // NUMLOCKFLAG
"NO_NUM_LOCK":0x0800, // NOTNUMLOCKFLAG
"SCROLL_LOCK":0x1000, // SCROLLFLAG
"NO_SCROLL_LOCK":0x2000, // NOTSCROLLFLAG
"VIRTUAL_KEY":0x4000, // ISVIRTUALKEY
"VIRTUAL_CHAR_KEY":0x8000 // VIRTUALCHARKEY // Unused by KMW, but reserved for use by other Keyman engines.
},
const Codes = {
// Define Keyman Developer modifier bit-flags (exposed for use by other modules)
// Compare against /common/include/kmx_file.h. CTRL+F "#define LCTRLFLAG" to find the secton.
modifierCodes: {
"LCTRL":0x0001, // LCTRLFLAG
"RCTRL":0x0002, // RCTRLFLAG
"LALT":0x0004, // LALTFLAG
"RALT":0x0008, // RALTFLAG
"SHIFT":0x0010, // K_SHIFTFLAG
"CTRL":0x0020, // K_CTRLFLAG
"ALT":0x0040, // K_ALTFLAG
// TENTATIVE: Represents command keys, which some OSes use for shortcuts we don't
// want to block. No rule will ever target a modifier set with this bit set to 1.
"META":0x0080, // K_METAFLAG
"CAPS":0x0100, // CAPITALFLAG
"NO_CAPS":0x0200, // NOTCAPITALFLAG
"NUM_LOCK":0x0400, // NUMLOCKFLAG
"NO_NUM_LOCK":0x0800, // NOTNUMLOCKFLAG
"SCROLL_LOCK":0x1000, // SCROLLFLAG
"NO_SCROLL_LOCK":0x2000, // NOTSCROLLFLAG
"VIRTUAL_KEY":0x4000, // ISVIRTUALKEY
"VIRTUAL_CHAR_KEY":0x8000 // VIRTUALCHARKEY // Unused by KMW, but reserved for use by other Keyman engines.
},
modifierBitmasks: {
"ALL":0x007F,
"ALT_GR_SIM": (0x0001 | 0x0004),
"CHIRAL":0x001F, // The base bitmask for chiral keyboards. Includes SHIFT, which is non-chiral.
"IS_CHIRAL":0x000F, // Used to test if a bitmask uses a chiral modifier.
"NON_CHIRAL":0x0070 // The default bitmask, for non-chiral keyboards
},
modifierBitmasks: {
"ALL":0x007F,
"ALT_GR_SIM": (0x0001 | 0x0004),
"CHIRAL":0x001F, // The base bitmask for chiral keyboards. Includes SHIFT, which is non-chiral.
"IS_CHIRAL":0x000F, // Used to test if a bitmask uses a chiral modifier.
"NON_CHIRAL":0x0070 // The default bitmask, for non-chiral keyboards
},
stateBitmasks: {
"ALL":0x3F00,
"CAPS":0x0300,
"NUM_LOCK":0x0C00,
"SCROLL_LOCK":0x3000
},
stateBitmasks: {
"ALL":0x3F00,
"CAPS":0x0300,
"NUM_LOCK":0x0C00,
"SCROLL_LOCK":0x3000
},
// Define standard keycode numbers (exposed for use by other modules)
keyCodes: {
"K_BKSP":8,"K_TAB":9,"K_ENTER":13,
"K_SHIFT":16,"K_CONTROL":17,"K_ALT":18,"K_PAUSE":19,"K_CAPS":20,
"K_ESC":27,"K_SPACE":32,"K_PGUP":33,
"K_PGDN":34,"K_END":35,"K_HOME":36,"K_LEFT":37,"K_UP":38,
"K_RIGHT":39,"K_DOWN":40,"K_SEL":41,"K_PRINT":42,"K_EXEC":43,
"K_INS":45,"K_DEL":46,"K_HELP":47,"K_0":48,
"K_1":49,"K_2":50,"K_3":51,"K_4":52,"K_5":53,"K_6":54,"K_7":55,
"K_8":56,"K_9":57,"K_A":65,"K_B":66,"K_C":67,"K_D":68,"K_E":69,
"K_F":70,"K_G":71,"K_H":72,"K_I":73,"K_J":74,"K_K":75,"K_L":76,
"K_M":77,"K_N":78,"K_O":79,"K_P":80,"K_Q":81,"K_R":82,"K_S":83,
"K_T":84,"K_U":85,"K_V":86,"K_W":87,"K_X":88,"K_Y":89,"K_Z":90,
"K_NP0":96,"K_NP1":97,"K_NP2":98,
"K_NP3":99,"K_NP4":100,"K_NP5":101,"K_NP6":102,
"K_NP7":103,"K_NP8":104,"K_NP9":105,"K_NPSTAR":106,
"K_NPPLUS":107,"K_SEPARATOR":108,"K_NPMINUS":109,"K_NPDOT":110,
"K_NPSLASH":111,"K_F1":112,"K_F2":113,"K_F3":114,"K_F4":115,
"K_F5":116,"K_F6":117,"K_F7":118,"K_F8":119,"K_F9":120,
"K_F10":121,"K_F11":122,"K_F12":123,"K_NUMLOCK":144,"K_SCROLL":145,
"K_LSHIFT":160,"K_RSHIFT":161,"K_LCONTROL":162,"K_RCONTROL":163,
"K_LALT":164,"K_RALT":165,
"K_COLON":186,"K_EQUAL":187,"K_COMMA":188,"K_HYPHEN":189,
"K_PERIOD":190,"K_SLASH":191,"K_BKQUOTE":192,
"K_LBRKT":219,"K_BKSLASH":220,"K_RBRKT":221,
"K_QUOTE":222,"K_oE2":226,"K_OE2":226,
"K_LOPT":50001,"K_ROPT":50002,
"K_NUMERALS":50003,"K_SYMBOLS":50004,"K_CURRENCIES":50005,
"K_UPPER":50006,"K_LOWER":50007,"K_ALPHA":50008,
"K_SHIFTED":50009,"K_ALTGR":50010,
"K_TABBACK":50011,"K_TABFWD":50012
},
// Define standard keycode numbers (exposed for use by other modules)
keyCodes: {
"K_BKSP":8,"K_TAB":9,"K_ENTER":13,
"K_SHIFT":16,"K_CONTROL":17,"K_ALT":18,"K_PAUSE":19,"K_CAPS":20,
"K_ESC":27,"K_SPACE":32,"K_PGUP":33,
"K_PGDN":34,"K_END":35,"K_HOME":36,"K_LEFT":37,"K_UP":38,
"K_RIGHT":39,"K_DOWN":40,"K_SEL":41,"K_PRINT":42,"K_EXEC":43,
"K_INS":45,"K_DEL":46,"K_HELP":47,"K_0":48,
"K_1":49,"K_2":50,"K_3":51,"K_4":52,"K_5":53,"K_6":54,"K_7":55,
"K_8":56,"K_9":57,"K_A":65,"K_B":66,"K_C":67,"K_D":68,"K_E":69,
"K_F":70,"K_G":71,"K_H":72,"K_I":73,"K_J":74,"K_K":75,"K_L":76,
"K_M":77,"K_N":78,"K_O":79,"K_P":80,"K_Q":81,"K_R":82,"K_S":83,
"K_T":84,"K_U":85,"K_V":86,"K_W":87,"K_X":88,"K_Y":89,"K_Z":90,
"K_NP0":96,"K_NP1":97,"K_NP2":98,
"K_NP3":99,"K_NP4":100,"K_NP5":101,"K_NP6":102,
"K_NP7":103,"K_NP8":104,"K_NP9":105,"K_NPSTAR":106,
"K_NPPLUS":107,"K_SEPARATOR":108,"K_NPMINUS":109,"K_NPDOT":110,
"K_NPSLASH":111,"K_F1":112,"K_F2":113,"K_F3":114,"K_F4":115,
"K_F5":116,"K_F6":117,"K_F7":118,"K_F8":119,"K_F9":120,
"K_F10":121,"K_F11":122,"K_F12":123,"K_NUMLOCK":144,"K_SCROLL":145,
"K_LSHIFT":160,"K_RSHIFT":161,"K_LCONTROL":162,"K_RCONTROL":163,
"K_LALT":164,"K_RALT":165,
"K_COLON":186,"K_EQUAL":187,"K_COMMA":188,"K_HYPHEN":189,
"K_PERIOD":190,"K_SLASH":191,"K_BKQUOTE":192,
"K_LBRKT":219,"K_BKSLASH":220,"K_RBRKT":221,
"K_QUOTE":222,"K_oE2":226,"K_OE2":226,
"K_LOPT":50001,"K_ROPT":50002,
"K_NUMERALS":50003,"K_SYMBOLS":50004,"K_CURRENCIES":50005,
"K_UPPER":50006,"K_LOWER":50007,"K_ALPHA":50008,
"K_SHIFTED":50009,"K_ALTGR":50010,
"K_TABBACK":50011,"K_TABFWD":50012
},
codesUS: [
['0123456789',';=,-./`', '[\\]\''],
[')!@#$%^&*(',':+<_>?~', '{|}"']
],
codesUS: [
['0123456789',';=,-./`', '[\\]\''],
[')!@#$%^&*(',':+<_>?~', '{|}"']
],
isKnownOSKModifierKey(keyID: string): boolean {
switch(keyID) {
case 'K_SHIFT':
case 'K_LOPT':
case 'K_ROPT':
case 'K_NUMLOCK': // Often used for numeric layers.
case 'K_CAPS':
isKnownOSKModifierKey(keyID: string): boolean {
switch(keyID) {
case 'K_SHIFT':
case 'K_LOPT':
case 'K_ROPT':
case 'K_NUMLOCK': // Often used for numeric layers.
case 'K_CAPS':
return true;
default:
if(Codes.keyCodes[keyID] >= 50000) { // A few are used by `sil_euro_latin`.
return true; // is a 'K_' key defined for layer shifting or 'control' use.
}
// Refer to text/codes.ts - these are Keyman-custom "keycodes" used for
// layer shifting keys. To be safe, we currently let K_TABBACK and
// K_TABFWD through, though we might be able to drop them too.
const code = Codes[keyID];
if(code > 50000 && code < 50011) {
return true;
default:
if(Codes.keyCodes[keyID] >= 50000) { // A few are used by `sil_euro_latin`.
return true; // is a 'K_' key defined for layer shifting or 'control' use.
}
// Refer to text/codes.ts - these are Keyman-custom "keycodes" used for
// layer shifting keys. To be safe, we currently let K_TABBACK and
// K_TABFWD through, though we might be able to drop them too.
let code = com.keyman.text.Codes[keyID];
if(code > 50000 && code < 50011) {
return true;
}
}
return false;
}
}
return false;
}
}
}
export default Codes;

View file

@ -1,160 +1,157 @@
namespace com.keyman.text {
// Defines the base Deadkey-tracking object.
export class Deadkey {
p: number; // Position of deadkey
d: number; // Numerical id of the deadkey
o: number; // Ordinal value of the deadkey (resolves same-place conflicts)
matched: number;
// Defines the base Deadkey-tracking object.
export class Deadkey {
p: number; // Position of deadkey
d: number; // Numerical id of the deadkey
o: number; // Ordinal value of the deadkey (resolves same-place conflicts)
matched: number;
static ordinalSeed: number = 0;
static ordinalSeed: number = 0;
constructor(pos: number, id: number) {
this.p = pos;
this.d = id;
this.o = Deadkey.ordinalSeed++;
}
match(p: number, d: number): boolean {
var result:boolean = (this.p == p && this.d == d);
return result;
}
set(): void {
this.matched = 1;
}
reset(): void {
this.matched = 0;
}
before(other: Deadkey): boolean {
return this.o < other.o;
}
clone(): Deadkey {
let dk = new Deadkey(this.p, this.d);
dk.o = this.o;
return dk;
}
/**
* Sorts the deadkeys in reverse order.
*/
static sortFunc = function(a: Deadkey, b: Deadkey) {
// We want descending order, so we want 'later' deadkeys first.
if(a.p != b.p) {
return b.p - a.p;
} else {
return b.o - a.o;
}
};
constructor(pos: number, id: number) {
this.p = pos;
this.d = id;
this.o = Deadkey.ordinalSeed++;
}
// Object-orients deadkey management.
export class DeadkeyTracker {
dks: Deadkey[] = [];
match(p: number, d: number): boolean {
var result:boolean = (this.p == p && this.d == d);
toSortedArray(): Deadkey[] {
this.dks = this.dks.sort(Deadkey.sortFunc);
return [].concat(this.dks);
return result;
}
set(): void {
this.matched = 1;
}
reset(): void {
this.matched = 0;
}
before(other: Deadkey): boolean {
return this.o < other.o;
}
clone(): Deadkey {
let dk = new Deadkey(this.p, this.d);
dk.o = this.o;
return dk;
}
/**
* Sorts the deadkeys in reverse order.
*/
static sortFunc = function(a: Deadkey, b: Deadkey) {
// We want descending order, so we want 'later' deadkeys first.
if(a.p != b.p) {
return b.p - a.p;
} else {
return b.o - a.o;
}
};
}
// Object-orients deadkey management.
export class DeadkeyTracker {
dks: Deadkey[] = [];
toSortedArray(): Deadkey[] {
this.dks = this.dks.sort(Deadkey.sortFunc);
return [].concat(this.dks);
}
clone(): DeadkeyTracker {
let dkt = new DeadkeyTracker();
let dks = this.toSortedArray();
// Make sure to clone the deadkeys themselves - the Deadkey object is mutable.
dkt.dks = [];
dks.forEach(function(value: Deadkey) {
dkt.dks.push(value.clone());
});
return dkt;
}
/**
* Function isMatch
* Scope Public
* @param {number} caretPos current cursor position
* @param {number} n expected offset of deadkey from cursor
* @param {number} d deadkey
* @return {boolean} True if deadkey found selected context matches val
* Description Match deadkey at current cursor position
*/
isMatch(caretPos: number, n: number, d: number): boolean {
if(this.dks.length == 0) {
return false; // I3318
}
clone(): DeadkeyTracker {
let dkt = new DeadkeyTracker();
let dks = this.toSortedArray();
// Make sure to clone the deadkeys themselves - the Deadkey object is mutable.
dkt.dks = [];
dks.forEach(function(value: Deadkey) {
dkt.dks.push(value.clone());
});
return dkt;
}
/**
* Function isMatch
* Scope Public
* @param {number} caretPos current cursor position
* @param {number} n expected offset of deadkey from cursor
* @param {number} d deadkey
* @return {boolean} True if deadkey found selected context matches val
* Description Match deadkey at current cursor position
*/
isMatch(caretPos: number, n: number, d: number): boolean {
if(this.dks.length == 0) {
return false; // I3318
}
var sp=caretPos;
n = sp - n;
for(var i = 0; i < this.dks.length; i++) {
// Don't re-match an already-matched deadkey. It's possible to have two identical
// entries, and they should be kept separately.
if(this.dks[i].match(n, d) && !this.dks[i].matched) {
this.dks[i].set();
// Assumption: since we match the first possible entry in the array, we
// match the entry with the lower ordinal - the 'first' deadkey in the position.
return true; // I3318
}
}
this.resetMatched(); // I3318
return false;
}
add(dk: Deadkey) {
this.dks = this.dks.concat(dk);
}
remove(dk: Deadkey) {
var index = this.dks.indexOf(dk);
this.dks.splice(index, 1);
}
clear() {
this.dks = [];
}
resetMatched() {
for(let dk of this.dks) {
dk.reset();
}
}
deleteMatched(): void {
for(var Li = 0; Li < this.dks.length; Li++) {
if(this.dks[Li].matched) {
this.dks.splice(Li--, 1); // Don't forget to decrement!
}
var sp=caretPos;
n = sp - n;
for(var i = 0; i < this.dks.length; i++) {
// Don't re-match an already-matched deadkey. It's possible to have two identical
// entries, and they should be kept separately.
if(this.dks[i].match(n, d) && !this.dks[i].matched) {
this.dks[i].set();
// Assumption: since we match the first possible entry in the array, we
// match the entry with the lower ordinal - the 'first' deadkey in the position.
return true; // I3318
}
}
/**
* Function adjustPositions (formerly _DeadkeyAdjustPos)
* Scope Private
* @param {number} Lstart start position in context
* @param {number} Ldelta characters to adjust by
* Description Adjust saved positions of deadkeys in context
*/
adjustPositions(Lstart: number, Ldelta: number): void {
if(Ldelta == 0) {
return;
}
for(let dk of this.dks) {
if(dk.p > Lstart) {
dk.p += Ldelta;
}
this.resetMatched(); // I3318
return false;
}
add(dk: Deadkey) {
this.dks = this.dks.concat(dk);
}
remove(dk: Deadkey) {
var index = this.dks.indexOf(dk);
this.dks.splice(index, 1);
}
clear() {
this.dks = [];
}
resetMatched() {
for(let dk of this.dks) {
dk.reset();
}
}
deleteMatched(): void {
for(var Li = 0; Li < this.dks.length; Li++) {
if(this.dks[Li].matched) {
this.dks.splice(Li--, 1); // Don't forget to decrement!
}
}
}
/**
* Function adjustPositions (formerly _DeadkeyAdjustPos)
* Scope Private
* @param {number} Lstart start position in context
* @param {number} Ldelta characters to adjust by
* Description Adjust saved positions of deadkeys in context
*/
adjustPositions(Lstart: number, Ldelta: number): void {
if(Ldelta == 0) {
return;
}
count(): number {
return this.dks.length;
for(let dk of this.dks) {
if(dk.p > Lstart) {
dk.p += Ldelta;
}
}
}
count(): number {
return this.dks.length;
}
}

View file

@ -1,191 +1,212 @@
// Establishes key-code definitions.
/// <reference path="codes.ts" />
// Defines our generalized "KeyEvent" class.
/// <reference path="keyEvent.ts" />
import Codes from "./codes.js";
import type KeyEvent from "./keyEvent.js";
import type OutputTarget from "./outputTarget.js";
import RuleBehavior from "./ruleBehavior.js";
namespace com.keyman.text {
export enum EmulationKeystrokes {
Enter = '\n',
Backspace = '\b'
export enum EmulationKeystrokes {
Enter = '\n',
Backspace = '\b'
}
/**
* Defines a collection of static library functions that define KeymanWeb's default (implied) keyboard rule behaviors.
*/
export default class DefaultOutput {
private constructor() {
}
static codeForEvent(Lkc: KeyEvent) {
return Codes.keyCodes[Lkc.kName] || Lkc.Lcode;;
}
/**
* Defines a collection of static library functions that define KeymanWeb's default (implied) keyboard rule behaviors.
* Serves as a default keycode lookup table. This may be referenced safely by mnemonic handling without fear of side-effects.
* Also used by Processor.defaultRuleBehavior to generate output after filtering for special cases.
*/
export class DefaultOutput {
private constructor() {
}
public static forAny(Lkc: KeyEvent, isMnemonic: boolean, ruleBehavior?: RuleBehavior) {
var char = '';
static codeForEvent(Lkc: KeyEvent) {
return Codes.keyCodes[Lkc.kName] || Lkc.Lcode;;
}
/**
* Serves as a default keycode lookup table. This may be referenced safely by mnemonic handling without fear of side-effects.
* Also used by Processor.defaultRuleBehavior to generate output after filtering for special cases.
*/
public static forAny(Lkc: KeyEvent, isMnemonic: boolean, ruleBehavior?: RuleBehavior) {
var char = '';
// A pretty simple table of lookups, corresponding VERY closely to the original defaultKeyOutput.
if((char = DefaultOutput.forSpecialEmulation(Lkc, ruleBehavior)) != null) {
return char;
} else if(!isMnemonic && ((char = DefaultOutput.forNumpadKeys(Lkc, ruleBehavior)) != null)) {
return char;
} else if((char = DefaultOutput.forUnicodeKeynames(Lkc, ruleBehavior)) != null) {
return char;
} else if((char = DefaultOutput.forBaseKeys(Lkc, ruleBehavior)) != null) {
return char;
} else {
// // For headless and embeddded, we may well allow '\t'. It's DOM mode that has other uses.
// // Not originally defined for text output within defaultKeyOutput.
// // We can't enable it yet, as it'll cause hardware keystrokes in the DOM to output '\t' rather
// // than rely on the browser-default handling.
let code = DefaultOutput.codeForEvent(Lkc);
switch(code) {
// case Codes.keyCodes['K_TAB']:
// case Codes.keyCodes['K_TABBACK']:
// case Codes.keyCodes['K_TABFWD']:
// return '\t';
default:
return null;
}
}
}
/**
* isCommand - returns a boolean indicating if a non-text event should be triggered by the keystroke.
*/
public static isCommand(Lkc: KeyEvent): boolean {
// A pretty simple table of lookups, corresponding VERY closely to the original defaultKeyOutput.
if((char = DefaultOutput.forSpecialEmulation(Lkc, ruleBehavior)) != null) {
return char;
} else if(!isMnemonic && ((char = DefaultOutput.forNumpadKeys(Lkc, ruleBehavior)) != null)) {
return char;
} else if((char = DefaultOutput.forUnicodeKeynames(Lkc, ruleBehavior)) != null) {
return char;
} else if((char = DefaultOutput.forBaseKeys(Lkc, ruleBehavior)) != null) {
return char;
} else {
// // For headless and embeddded, we may well allow '\t'. It's DOM mode that has other uses.
// // Not originally defined for text output within defaultKeyOutput.
// // We can't enable it yet, as it'll cause hardware keystrokes in the DOM to output '\t' rather
// // than rely on the browser-default handling.
let code = DefaultOutput.codeForEvent(Lkc);
switch(code) {
// Should we ever implement them:
// case Codes.keyCodes['K_LEFT']: // would not output text, but would alter the caret's position in the context.
// case Codes.keyCodes['K_RIGHT']:
// return true;
default:
return false;
}
}
/**
* Used when a RuleBehavior represents a non-text "command" within the Engine. This will generally
* trigger events that require context reset - often by moving the caret or by moving what OutputTarget
* the caret is in. However, we let those events perform the actual context reset.
*
* Note: is extended by DOM-aware KeymanWeb code.
*/
public static applyCommand(Lkc: KeyEvent, outputTarget: OutputTarget): void {
// Notes for potential default-handling extensions:
//
// switch(code) {
// // Problem: clusters, and doing them right.
// // The commented-out code below should be a decent starting point, but clusters make it complex.
// // Mostly based on pre-12.0 code, but the general idea should be relatively clear.
//
// case Codes.keyCodes['K_LEFT']:
// if(touchAlias) {
// var caretPos = keymanweb.getTextCaret(Lelem);
// keymanweb.setTextCaret(Lelem, caretPos - 1 >= 0 ? caretPos - 1 : 0);
// }
// break;
// case Codes.keyCodes['K_RIGHT']:
// if(touchAlias) {
// var caretPos = keymanweb.getTextCaret(Lelem);
// keymanweb.setTextCaret(Lelem, caretPos + 1);
// }
// if(code == VisualKeyboard.keyCodes['K_RIGHT']) {
// break;
// }
// }
//
// Note that these would be useful even outside of a DOM context.
}
/**
* Codes matched here generally have default implementations when in a browser but require emulation
* for 'synthetic' `OutputTarget`s like `Mock`s, which have no default text handling.
*/
public static forSpecialEmulation(Lkc: KeyEvent, ruleBehavior?: RuleBehavior): EmulationKeystrokes {
let code = DefaultOutput.codeForEvent(Lkc);
switch(code) {
case Codes.keyCodes['K_BKSP']:
return EmulationKeystrokes.Backspace;
case Codes.keyCodes['K_ENTER']:
return EmulationKeystrokes.Enter;
// case Codes.keyCodes['K_DEL']:
// return '\u007f'; // 127, ASCII / Unicode control code for DEL.
// case Codes.keyCodes['K_TAB']:
// case Codes.keyCodes['K_TABBACK']:
// case Codes.keyCodes['K_TABFWD']:
// return '\t';
default:
return null;
}
}
}
// Should not be used for mnenomic keyboards. forAny()'s use of this method checks first.
public static forNumpadKeys(Lkc: KeyEvent, ruleBehavior?: RuleBehavior) {
// Translate numpad keystrokes into their non-numpad equivalents
if(Lkc.Lcode >= Codes.keyCodes["K_NP0"] && Lkc.Lcode <= Codes.keyCodes["K_NPSLASH"]) {
// Number pad, numlock on
if(Lkc.Lcode < 106) {
var Lch = Lkc.Lcode-48;
} else {
Lch = Lkc.Lcode-64;
}
let ch = String._kmwFromCharCode(Lch); //I3319
return ch;
/**
* isCommand - returns a boolean indicating if a non-text event should be triggered by the keystroke.
*/
public static isCommand(Lkc: KeyEvent): boolean {
let code = DefaultOutput.codeForEvent(Lkc);
switch(code) {
// Should we ever implement them:
// case Codes.keyCodes['K_LEFT']: // would not output text, but would alter the caret's position in the context.
// case Codes.keyCodes['K_RIGHT']:
// return true;
default:
return false;
}
}
/**
* Used when a RuleBehavior represents a non-text "command" within the Engine. This will generally
* trigger events that require context reset - often by moving the caret or by moving what OutputTarget
* the caret is in. However, we let those events perform the actual context reset.
*
* Note: is extended by DOM-aware KeymanWeb code.
*/
public static applyCommand(Lkc: KeyEvent, outputTarget: OutputTarget): void {
// Notes for potential default-handling extensions:
//
// switch(code) {
// // Problem: clusters, and doing them right.
// // The commented-out code below should be a decent starting point, but clusters make it complex.
// // Mostly based on pre-12.0 code, but the general idea should be relatively clear.
//
// case Codes.keyCodes['K_LEFT']:
// if(touchAlias) {
// var caretPos = keymanweb.getTextCaret(Lelem);
// keymanweb.setTextCaret(Lelem, caretPos - 1 >= 0 ? caretPos - 1 : 0);
// }
// break;
// case Codes.keyCodes['K_RIGHT']:
// if(touchAlias) {
// var caretPos = keymanweb.getTextCaret(Lelem);
// keymanweb.setTextCaret(Lelem, caretPos + 1);
// }
// if(code == VisualKeyboard.keyCodes['K_RIGHT']) {
// break;
// }
// }
//
// Note that these would be useful even outside of a DOM context.
}
/**
* Codes matched here generally have default implementations when in a browser but require emulation
* for 'synthetic' `OutputTarget`s like `Mock`s, which have no default text handling.
*/
public static forSpecialEmulation(Lkc: KeyEvent, ruleBehavior?: RuleBehavior): EmulationKeystrokes {
let code = DefaultOutput.codeForEvent(Lkc);
switch(code) {
case Codes.keyCodes['K_BKSP']:
return EmulationKeystrokes.Backspace;
case Codes.keyCodes['K_ENTER']:
return EmulationKeystrokes.Enter;
// case Codes.keyCodes['K_DEL']:
// return '\u007f'; // 127, ASCII / Unicode control code for DEL.
default:
return null;
}
}
// Should not be used for mnenomic keyboards. forAny()'s use of this method checks first.
public static forNumpadKeys(Lkc: KeyEvent, ruleBehavior?: RuleBehavior) {
// Translate numpad keystrokes into their non-numpad equivalents
if(Lkc.Lcode >= Codes.keyCodes["K_NP0"] && Lkc.Lcode <= Codes.keyCodes["K_NPSLASH"]) {
// Number pad, numlock on
if(Lkc.Lcode < 106) {
var Lch = Lkc.Lcode-48;
} else {
return null;
Lch = Lkc.Lcode-64;
}
}
// Test for fall back to U_xxxxxx key id
// For this first test, we ignore the keyCode and use the keyName
public static forUnicodeKeynames(Lkc: KeyEvent, ruleBehavior?: RuleBehavior) {
const keyName = Lkc.kName;
return keyboards.ActiveKey.unicodeIDToText(keyName, (codeWithError) => {
ruleBehavior.errorLog = ("Suppressing Unicode control code in " + keyName + ": " + codeWithError);
});
}
// Test for otherwise unimplemented keys on the the base default & shift layers.
// Those keys must be blocked by keyboard rules if intentionally unimplemented; otherwise, this function will trigger.
public static forBaseKeys(Lkc: KeyEvent, ruleBehavior?: RuleBehavior) {
let n = Lkc.Lcode;
let keyShiftState = Lkc.Lmodifiers;
// check if exact match to SHIFT's code. Only the 'default' and 'shift' layers should have default key outputs.
// TODO: Extend to allow AltGr as well - better mnemonic support.
if(keyShiftState == Codes.modifierCodes['SHIFT']) {
keyShiftState = 1;
} else if(keyShiftState != 0) {
if(ruleBehavior) {
ruleBehavior.warningLog = "KMW only defines default key output for the 'default' and 'shift' layers!";
}
return null;
}
// Now that keyShiftState is either 0 or 1, we can use the following structure to determine the default output.
try {
if(n == Codes.keyCodes['K_SPACE']) {
return ' ';
} else if(n >= Codes.keyCodes['K_0'] && n <= Codes.keyCodes['K_9']) { // The number keys.
return Codes.codesUS[keyShiftState][0][n-Codes.keyCodes['K_0']];
} else if(n >= Codes.keyCodes['K_A'] && n <= Codes.keyCodes['K_Z']) { // The base letter keys
return String.fromCharCode(n+(keyShiftState?0:32)); // 32 is the offset from uppercase to lowercase.
} else if(n >= Codes.keyCodes['K_COLON'] && n <= Codes.keyCodes['K_BKQUOTE']) {
return Codes.codesUS[keyShiftState][1][n-Codes.keyCodes['K_COLON']];
} else if(n >= Codes.keyCodes['K_LBRKT'] && n <= Codes.keyCodes['K_QUOTE']) {
return Codes.codesUS[keyShiftState][2][n-Codes.keyCodes['K_LBRKT']];
}
} catch (e) {
if(ruleBehavior) {
ruleBehavior.errorLog = "Error detected with default mapping for key: code = " + n + ", shift state = " + (keyShiftState == 1 ? 'shift' : 'default');
}
}
let ch = String._kmwFromCharCode(Lch); //I3319
return ch;
} else {
return null;
}
}
// Test for fall back to U_xxxxxx key id
// For this first test, we ignore the keyCode and use the keyName
public static forUnicodeKeynames(Lkc: KeyEvent, ruleBehavior?: RuleBehavior) {
const keyName = Lkc.kName;
// Test for fall back to U_xxxxxx key id
// For this first test, we ignore the keyCode and use the keyName
if(!keyName || keyName.substr(0,2) != 'U_') {
return null;
}
let result = '';
const codePoints = keyName.substr(2).split('_');
for(let codePoint of codePoints) {
const codePointValue = parseInt(codePoint, 16);
if (((0x0 <= codePointValue) && (codePointValue <= 0x1F)) || ((0x80 <= codePointValue) && (codePointValue <= 0x9F)) || isNaN(codePointValue)) {
// Code points [U_0000 - U_001F] and [U_0080 - U_009F] refer to Unicode C0 and C1 control codes.
// Check the codePoint number and do not allow output of these codes via U_xxxxxx shortcuts.
// Also handles invalid identifiers (e.g. `U_ghij`) for which parseInt returns NaN
if(ruleBehavior) {
ruleBehavior.errorLog = ("Suppressing Unicode control code in " + keyName);
}
// We'll attempt to add valid chars
continue;
} else {
// String.fromCharCode() is inadequate to handle the entire range of Unicode
// Someday after upgrading to ES2015, can use String.fromCodePoint()
result += String.kmwFromCharCode(codePointValue);
}
}
return result ? result : null;
}
// Test for otherwise unimplemented keys on the the base default & shift layers.
// Those keys must be blocked by keyboard rules if intentionally unimplemented; otherwise, this function will trigger.
public static forBaseKeys(Lkc: KeyEvent, ruleBehavior?: RuleBehavior) {
let n = Lkc.Lcode;
let keyShiftState = Lkc.Lmodifiers;
// check if exact match to SHIFT's code. Only the 'default' and 'shift' layers should have default key outputs.
// TODO: Extend to allow AltGr as well - better mnemonic support.
if(keyShiftState == Codes.modifierCodes['SHIFT']) {
keyShiftState = 1;
} else if(keyShiftState != 0) {
if(ruleBehavior) {
ruleBehavior.warningLog = "KMW only defines default key output for the 'default' and 'shift' layers!";
}
return null;
}
// Now that keyShiftState is either 0 or 1, we can use the following structure to determine the default output.
try {
if(n == Codes.keyCodes['K_SPACE']) {
return ' ';
} else if(n >= Codes.keyCodes['K_0'] && n <= Codes.keyCodes['K_9']) { // The number keys.
return Codes.codesUS[keyShiftState][0][n-Codes.keyCodes['K_0']];
} else if(n >= Codes.keyCodes['K_A'] && n <= Codes.keyCodes['K_Z']) { // The base letter keys
return String.fromCharCode(n+(keyShiftState?0:32)); // 32 is the offset from uppercase to lowercase.
} else if(n >= Codes.keyCodes['K_COLON'] && n <= Codes.keyCodes['K_BKQUOTE']) {
return Codes.codesUS[keyShiftState][1][n-Codes.keyCodes['K_COLON']];
} else if(n >= Codes.keyCodes['K_LBRKT'] && n <= Codes.keyCodes['K_QUOTE']) {
return Codes.codesUS[keyShiftState][2][n-Codes.keyCodes['K_LBRKT']];
}
} catch (e) {
if(ruleBehavior) {
ruleBehavior.errorLog = "Error detected with default mapping for key: code = " + n + ", shift state = " + (keyShiftState == 1 ? 'shift' : 'default');
}
}
return null;
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,57 +1,56 @@
/// <reference path="outputTarget.ts" />
import type Keyboard from "../keyboards/keyboard.js";
import {type DeviceSpec} from "@keymanapp/web-utils/build/obj/index.js";
namespace com.keyman.text {
// Represents a probability distribution over a keyboard's keys.
// Defined here to avoid compilation issues.
export type KeyDistribution = {keyId: string, p: number}[];
// Represents a probability distribution over a keyboard's keys.
// Defined here to avoid compilation issues.
export type KeyDistribution = {keyId: string, p: number}[];
/**
* This class is defined within its own file so that it can be loaded by code outside of KMW without
* having to actually load the entirety of KMW.
*/
export default class KeyEvent {
Lcode: number;
Lstates: number;
LmodifierChange?: boolean;
Lmodifiers: number;
LisVirtualKey: boolean;
vkCode: number;
kName: string;
kLayer?: string; // The key's layer property
kbdLayer?: string; // The virtual keyboard's active layer
kNextLayer?: string;
/**
* This class is defined within its own file so that it can be loaded by code outside of KMW without
* having to actually load the entirety of KMW.
* Marks the active keyboard at the time that this KeyEvent was generated by the user.
*
* Note: this is NOT equivalent to the active keyboard at the time that the event handler begins
* processing! It should be set via closure (or similar) on the event handler that can 100%
* guarantee that the keyboard instance known to the handler has not changed during JS execution
* since the user's interaction that raised the event.
*/
export class KeyEvent {
Lcode: number;
Lstates: number;
LmodifierChange?: boolean;
Lmodifiers: number;
LisVirtualKey: boolean;
vkCode: number;
kName: string;
kLayer?: string; // The key's layer property
kbdLayer?: string; // The virtual keyboard's active layer
kNextLayer?: string;
srcKeyboard?: Keyboard;
/**
* Marks the active keyboard at the time that this KeyEvent was generated by the user.
*
* Note: this is NOT equivalent to the active keyboard at the time that the event handler begins
* processing! It should be set via closure (or similar) on the event handler that can 100%
* guarantee that the keyboard instance known to the handler has not changed during JS execution
* since the user's interaction that raised the event.
*/
srcKeyboard?: keyboards.Keyboard;
// Holds relevant event properties leading to construction of this KeyEvent.
source?: any; // Technically, KeyEvent|MouseEvent|Touch - but those are DOM types that must be kept out of headless mode.
// Holds a generated fat-finger distribution (when appropriate)
keyDistribution?: KeyDistribution;
// Holds relevant event properties leading to construction of this KeyEvent.
source?: any; // Technically, KeyEvent|MouseEvent|Touch - but those are DOM types that must be kept out of headless mode.
// Holds a generated fat-finger distribution (when appropriate)
keyDistribution?: KeyDistribution;
/**
* The device model for web-core to follow when processing the keystroke.
*/
device: DeviceSpec;
/**
* The device model for web-core to follow when processing the keystroke.
*/
device: utils.DeviceSpec;
/**
* `true` if this event was produced by sources other than a DOM-based KeyboardEvent.
*/
isSynthetic: boolean = true;
/**
* `true` if this event was produced by sources other than a DOM-based KeyboardEvent.
*/
isSynthetic: boolean = true;
public static constructNullKeyEvent(device: utils.DeviceSpec): KeyEvent {
const keyEvent = new KeyEvent();
keyEvent.Lcode = 0;
keyEvent.kName = '';
keyEvent.device = device;
return keyEvent;
}
};
}
public static constructNullKeyEvent(device: DeviceSpec): KeyEvent {
const keyEvent = new KeyEvent();
keyEvent.Lcode = 0;
keyEvent.kName = '';
keyEvent.device = device;
return keyEvent;
}
};

View file

@ -2,185 +2,185 @@
KeymanWeb 11.0
Copyright 2019 SIL International
***/
namespace com.keyman {
class KeyMap {
[keycode: string]: number;
import type KeyEvent from "./keyEvent.js";
class KeyMap {
[keycode: string]: number;
}
class BrowserKeyMaps {
FF: KeyMap = new KeyMap();
Safari: KeyMap = new KeyMap();
Opera: KeyMap = new KeyMap();
constructor() {
// All three have been around since at least May 2014 / FF 29.
// It'd hard to find precise history, but at least that much has been confirmed.
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode, on Feb 26 2021.
this.FF['k61'] = 187; // = // FF 2.0
this.FF['k59'] = 186; // ;
this.FF['k173'] = 189; // -/_
}
}
class LanguageKeyMaps {
[languageCode: string]: KeyMap;
// // Here are some old legacy definitions that were no longer referenced but are likely related:
// static _BaseLayoutEuro: {[code: string]: string} = {
// 'se': '\u00a71234567890+´~~~QWERTYUIOP\u00c5\u00a8\'~~~ASDFGHJKL\u00d6\u00c4~~~~~<ZXCVBNM,.-~~~~~ ', // Swedish
// 'uk': '`1234567890-=~~~QWERTYUIOP[]#~~~ASDFGHJKL;\'~~~~~\\ZXCVBNM,./~~~~~ ' // UK
constructor() {
/* I732 START - 13/03/2007 MCD: Swedish: Start mapping of keystroke to US keyboard #2 */
// Swedish key map
this['se'] = new KeyMap();
this['se']['k220'] = 192; // `
this['se']['k187'] = 189; // -
this['se']['k219'] = 187; // =
this['se']['k221'] = 219; // [
this['se']['k186'] = 221; // ]
this['se']['k191'] = 220; // \
this['se']['k192'] = 186; // ;
this['se']['k189'] = 191; // /
this['uk'] = new KeyMap(); // I1299
this['uk']['k223'] = 192; // // ` U+00AC (logical not) => ` ~
this['uk']['k192'] = 222; // ' @ => ' "
this['uk']['k222'] = 226; // # ~ => K_oE2 // I1504 - UK keyboard mixup #, \
this['uk']['k220'] = 220; // \ | => \ | // I1504 - UK keyboard mixup #, \
}
}
export default class KeyMapping {
static readonly browserMap: BrowserKeyMaps = new BrowserKeyMaps();
static readonly languageMap: LanguageKeyMaps = new LanguageKeyMaps();
private static _usCharCodes: KeyMap[];
private constructor() {
// Do not construct this class.
}
class BrowserKeyMaps {
FF: KeyMap = new KeyMap();
Safari: KeyMap = new KeyMap();
Opera: KeyMap = new KeyMap();
private static _usCodeInit() {
var s0=new KeyMap(),s1=new KeyMap();
constructor() {
// All three have been around since at least May 2014 / FF 29.
// It'd hard to find precise history, but at least that much has been confirmed.
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode, on Feb 26 2021.
this.FF['k61'] = 187; // = // FF 2.0
this.FF['k59'] = 186; // ;
this.FF['k173'] = 189; // -/_
}
s0['k192'] = 96;
s0['k49'] = 49;
s0['k50'] = 50;
s0['k51'] = 51;
s0['k52'] = 52;
s0['k53'] = 53;
s0['k54'] = 54;
s0['k55'] = 55;
s0['k56'] = 56;
s0['k57'] = 57;
s0['k48'] = 48;
s0['k189'] = 45;
s0['k187'] = 61;
s0['k81'] = 113;
s0['k87'] = 119;
s0['k69'] = 101;
s0['k82'] = 114;
s0['k84'] = 116;
s0['k89'] = 121;
s0['k85'] = 117;
s0['k73'] = 105;
s0['k79'] = 111;
s0['k80'] = 112;
s0['k219'] = 91;
s0['k221'] = 93;
s0['k220'] = 92;
s0['k65'] = 97;
s0['k83'] = 115;
s0['k68'] = 100;
s0['k70'] = 102;
s0['k71'] = 103;
s0['k72'] = 104;
s0['k74'] = 106;
s0['k75'] = 107;
s0['k76'] = 108;
s0['k186'] = 59;
s0['k222'] = 39;
s0['k90'] = 122;
s0['k88'] = 120;
s0['k67'] = 99;
s0['k86'] = 118;
s0['k66'] = 98;
s0['k78'] = 110;
s0['k77'] = 109;
s0['k188'] = 44;
s0['k190'] = 46;
s0['k191'] = 47;
s1['k192'] = 126;
s1['k49'] = 33;
s1['k50'] = 64;
s1['k51'] = 35;
s1['k52'] = 36;
s1['k53'] = 37;
s1['k54'] = 94;
s1['k55'] = 38;
s1['k56'] = 42;
s1['k57'] = 40;
s1['k48'] = 41;
s1['k189'] = 95;
s1['k187'] = 43;
s1['k81'] = 81;
s1['k87'] = 87;
s1['k69'] = 69;
s1['k82'] = 82;
s1['k84'] = 84;
s1['k89'] = 89;
s1['k85'] = 85;
s1['k73'] = 73;
s1['k79'] = 79;
s1['k80'] = 80;
s1['k219'] = 123;
s1['k221'] = 125;
s1['k220'] = 124;
s1['k65'] = 65;
s1['k83'] = 83;
s1['k68'] = 68;
s1['k70'] = 70;
s1['k71'] = 71;
s1['k72'] = 72;
s1['k74'] = 74;
s1['k75'] = 75;
s1['k76'] = 76;
s1['k186'] = 58;
s1['k222'] = 34;
s1['k90'] = 90;
s1['k88'] = 88;
s1['k67'] = 67;
s1['k86'] = 86;
s1['k66'] = 66;
s1['k78'] = 78;
s1['k77'] = 77;
s1['k188'] = 60;
s1['k190'] = 62;
s1['k191'] = 63;
KeyMapping._usCharCodes = [s0,s1];
}
class LanguageKeyMaps {
[languageCode: string]: KeyMap;
/**
* Function _USKeyCodeToCharCode
* Scope Private
* @param {Event} Levent KMW event object
* @return {number} Character code
* Description Translate keyboard codes to standard US layout codes
*/
static _USKeyCodeToCharCode(Levent: KeyEvent) {
return KeyMapping.usCharCodes[Levent.Lmodifiers & 0x10 ? 1 : 0]['k'+Levent.Lcode];
};
// // Here are some old legacy definitions that were no longer referenced but are likely related:
// static _BaseLayoutEuro: {[code: string]: string} = {
// 'se': '\u00a71234567890+´~~~QWERTYUIOP\u00c5\u00a8\'~~~ASDFGHJKL\u00d6\u00c4~~~~~<ZXCVBNM,.-~~~~~ ', // Swedish
// 'uk': '`1234567890-=~~~QWERTYUIOP[]#~~~ASDFGHJKL;\'~~~~~\\ZXCVBNM,./~~~~~ ' // UK
constructor() {
/* I732 START - 13/03/2007 MCD: Swedish: Start mapping of keystroke to US keyboard #2 */
// Swedish key map
this['se'] = new KeyMap();
this['se']['k220'] = 192; // `
this['se']['k187'] = 189; // -
this['se']['k219'] = 187; // =
this['se']['k221'] = 219; // [
this['se']['k186'] = 221; // ]
this['se']['k191'] = 220; // \
this['se']['k192'] = 186; // ;
this['se']['k189'] = 191; // /
this['uk'] = new KeyMap(); // I1299
this['uk']['k223'] = 192; // // ` U+00AC (logical not) => ` ~
this['uk']['k192'] = 222; // ' @ => ' "
this['uk']['k222'] = 226; // # ~ => K_oE2 // I1504 - UK keyboard mixup #, \
this['uk']['k220'] = 220; // \ | => \ | // I1504 - UK keyboard mixup #, \
}
}
export class KeyMapping {
static readonly browserMap: BrowserKeyMaps = new BrowserKeyMaps();
static readonly languageMap: LanguageKeyMaps = new LanguageKeyMaps();
private static _usCharCodes: KeyMap[];
private constructor() {
// Do not construct this class.
public static get usCharCodes() {
if(!KeyMapping._usCharCodes) {
KeyMapping._usCodeInit();
}
private static _usCodeInit() {
var s0=new KeyMap(),s1=new KeyMap();
s0['k192'] = 96;
s0['k49'] = 49;
s0['k50'] = 50;
s0['k51'] = 51;
s0['k52'] = 52;
s0['k53'] = 53;
s0['k54'] = 54;
s0['k55'] = 55;
s0['k56'] = 56;
s0['k57'] = 57;
s0['k48'] = 48;
s0['k189'] = 45;
s0['k187'] = 61;
s0['k81'] = 113;
s0['k87'] = 119;
s0['k69'] = 101;
s0['k82'] = 114;
s0['k84'] = 116;
s0['k89'] = 121;
s0['k85'] = 117;
s0['k73'] = 105;
s0['k79'] = 111;
s0['k80'] = 112;
s0['k219'] = 91;
s0['k221'] = 93;
s0['k220'] = 92;
s0['k65'] = 97;
s0['k83'] = 115;
s0['k68'] = 100;
s0['k70'] = 102;
s0['k71'] = 103;
s0['k72'] = 104;
s0['k74'] = 106;
s0['k75'] = 107;
s0['k76'] = 108;
s0['k186'] = 59;
s0['k222'] = 39;
s0['k90'] = 122;
s0['k88'] = 120;
s0['k67'] = 99;
s0['k86'] = 118;
s0['k66'] = 98;
s0['k78'] = 110;
s0['k77'] = 109;
s0['k188'] = 44;
s0['k190'] = 46;
s0['k191'] = 47;
s1['k192'] = 126;
s1['k49'] = 33;
s1['k50'] = 64;
s1['k51'] = 35;
s1['k52'] = 36;
s1['k53'] = 37;
s1['k54'] = 94;
s1['k55'] = 38;
s1['k56'] = 42;
s1['k57'] = 40;
s1['k48'] = 41;
s1['k189'] = 95;
s1['k187'] = 43;
s1['k81'] = 81;
s1['k87'] = 87;
s1['k69'] = 69;
s1['k82'] = 82;
s1['k84'] = 84;
s1['k89'] = 89;
s1['k85'] = 85;
s1['k73'] = 73;
s1['k79'] = 79;
s1['k80'] = 80;
s1['k219'] = 123;
s1['k221'] = 125;
s1['k220'] = 124;
s1['k65'] = 65;
s1['k83'] = 83;
s1['k68'] = 68;
s1['k70'] = 70;
s1['k71'] = 71;
s1['k72'] = 72;
s1['k74'] = 74;
s1['k75'] = 75;
s1['k76'] = 76;
s1['k186'] = 58;
s1['k222'] = 34;
s1['k90'] = 90;
s1['k88'] = 88;
s1['k67'] = 67;
s1['k86'] = 86;
s1['k66'] = 66;
s1['k78'] = 78;
s1['k77'] = 77;
s1['k188'] = 60;
s1['k190'] = 62;
s1['k191'] = 63;
KeyMapping._usCharCodes = [s0,s1];
}
/**
* Function _USKeyCodeToCharCode
* Scope Private
* @param {Event} Levent KMW event object
* @return {number} Character code
* Description Translate keyboard codes to standard US layout codes
*/
static _USKeyCodeToCharCode(Levent: com.keyman.text.KeyEvent) {
return KeyMapping.usCharCodes[Levent.Lmodifiers & 0x10 ? 1 : 0]['k'+Levent.Lcode];
};
public static get usCharCodes() {
if(!KeyMapping._usCharCodes) {
KeyMapping._usCodeInit();
}
return KeyMapping._usCharCodes;
}
return KeyMapping._usCharCodes;
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,466 +1,463 @@
// Defines deadkey management in a manner attachable to each element interface.
///<reference path="../text/deadkeys.ts" />
// Defines the KeyEvent type.
///<reference path="keyEvent.ts" />
///<reference types="@keymanapp/models-types" />
// Defines deadkey management in a manner attachable to each element interface.
import type KeyEvent from "./keyEvent.js";
import { Deadkey, DeadkeyTracker } from "./deadkeys.js";
// Also relies on string-extensions provided by the web-utils package.
namespace com.keyman.text {
export class TextTransform implements Transform {
readonly insert: string;
readonly deleteLeft: number;
readonly deleteRight?: number;
export class TextTransform implements Transform {
readonly insert: string;
readonly deleteLeft: number;
readonly deleteRight?: number;
constructor(insert: string, deleteLeft: number, deleteRight?: number) {
this.insert = insert;
this.deleteLeft = deleteLeft;
this.deleteRight = deleteRight || 0;
}
public static readonly nil = new TextTransform('', 0, 0);
public isNoOp(): boolean {
return this.insert === '' && this.deleteLeft === 0 && this.deleteRight === 0;
}
constructor(insert: string, deleteLeft: number, deleteRight?: number) {
this.insert = insert;
this.deleteLeft = deleteLeft;
this.deleteRight = deleteRight || 0;
}
export class Transcription {
readonly token: number;
readonly keystroke: KeyEvent;
readonly transform: Transform;
alternates: Alternate[]; // constructed after the rest of the transcription.
readonly preInput: Mock;
public static readonly nil = new TextTransform('', 0, 0);
private static tokenSeed: number = 0;
public isNoOp(): boolean {
return this.insert === '' && this.deleteLeft === 0 && this.deleteRight === 0;
}
}
constructor(keystroke: KeyEvent, transform: Transform, preInput: Mock, alternates?: Alternate[]/*, removedDks: Deadkey[], insertedDks: Deadkey[]*/) {
let token = this.token = Transcription.tokenSeed++;
export class Transcription {
readonly token: number;
readonly keystroke: KeyEvent;
readonly transform: Transform;
alternates: Alternate[]; // constructed after the rest of the transcription.
readonly preInput: Mock;
this.keystroke = keystroke;
this.transform = transform;
this.alternates = alternates;
this.preInput = preInput;
private static tokenSeed: number = 0;
this.transform.id = this.token;
constructor(keystroke: KeyEvent, transform: Transform, preInput: Mock, alternates?: Alternate[]/*, removedDks: Deadkey[], insertedDks: Deadkey[]*/) {
let token = this.token = Transcription.tokenSeed++;
// Assign the ID to each alternate, as well.
if(alternates) {
alternates.forEach(function(alt) {
alt.sample.id = token;
});
}
this.keystroke = keystroke;
this.transform = transform;
this.alternates = alternates;
this.preInput = preInput;
this.transform.id = this.token;
// Assign the ID to each alternate, as well.
if(alternates) {
alternates.forEach(function(alt) {
alt.sample.id = token;
});
}
}
}
export type Alternate = ProbabilityMass<Transform>;
export type Alternate = ProbabilityMass<Transform>;
export abstract class OutputTarget {
private _dks: text.DeadkeyTracker;
export default abstract class OutputTarget {
private _dks: DeadkeyTracker;
constructor() {
this._dks = new text.DeadkeyTracker();
}
/**
* Signifies that this OutputTarget has no default key processing behaviors. This should be false
* for OutputTargets backed by web elements like HTMLInputElement or HTMLTextAreaElement.
*/
get isSynthetic(): boolean {
return true;
}
resetContext(): void {
this.deadkeys().clear();
}
deadkeys(): text.DeadkeyTracker {
return this._dks;
}
hasDeadkeyMatch(n: number, d: number): boolean {
return this.deadkeys().isMatch(this.getDeadkeyCaret(), n, d);
}
insertDeadkeyBeforeCaret(d: number) {
var dk: Deadkey = new Deadkey(this.getDeadkeyCaret(), d);
this.deadkeys().add(dk);
}
/**
* Should be called by each output target immediately before text mutation operations occur.
*
* Maintains solutions to old issues: I3318,I3319
* @param {number} delta Use negative values if characters were deleted, positive if characters were added.
*/
protected adjustDeadkeys(delta: number) {
this.deadkeys().adjustPositions(this.getDeadkeyCaret(), delta);
}
/**
* Needed to properly clone deadkeys for use with Mock element interfaces toward predictive text purposes.
* @param {object} dks An existing set of deadkeys to deep-copy for use by this element interface.
*/
protected setDeadkeys(dks: text.DeadkeyTracker) {
this._dks = dks.clone();
}
/**
* Determines the basic operations needed to reconstruct the current OutputTarget's text from the prior state specified
* by another OutputTarget based on their text and caret positions.
*
* This is designed for use as a "before and after" comparison to determine the effect of a single keyboard rule at a time.
* As such, it assumes that the caret is immediately after any inserted text.
* @param from An output target (preferably a Mock) representing the prior state of the input/output system.
*/
buildTransformFrom(original: OutputTarget): Transform {
let to = this.getText();
let from = original.getText();
let fromCaret = original.getDeadkeyCaret();
let toCaret = this.getDeadkeyCaret();
// Step 1: Determine the number of left-deletions.
let maxSMPLeftMatch = fromCaret < toCaret ? fromCaret : toCaret;
// We need the corresponding non-SMP caret location in order to binary-search efficiently.
// (Examining code units is much more computationally efficient.)
let maxLeftMatch = to._kmwCodePointToCodeUnit(maxSMPLeftMatch);
// 1.1: use a non-SMP-aware binary search to determine the divergence point.
let start = 0;
let end = maxLeftMatch; // the index AFTER the last possible matching char.
// This search is O(maxLeftMatch). 1/2 + 1/4 + 1/8 + ... converges to = 1.
while(start < end) {
let mid = Math.floor((end+start+1) / 2); // round up (compare more)
let fromLeft = from.substr(start, mid-start);
let toLeft = to.substr(start, mid-start);
if(fromLeft == toLeft) {
start = mid;
} else {
end = mid - 1;
}
}
// At the loop's end: `end` now holds the non-SMP-aware divergence point.
// The 'caret' is after the last matching code unit.
// 1.2: detect a possible surrogate-pair split scenario, correcting for it
// (by moving the split before the high-surrogate) if detected.
// If the split location is precisely on either end of the context, we can't
// have split a surrogate pair.
if(end > 0 && end < maxLeftMatch) {
let potentialHigh = from.charCodeAt(end-1);
let potentialFromLow = from.charCodeAt(end);
let potentialToLow = to.charCodeAt(end);
// if potentialHigh is a possible high surrogate...
if(potentialHigh >= 0xD800 && potentialHigh <= 0xDBFF) {
// and at least one potential 'low' is a possible low surrogate...
let flag = potentialFromLow >= 0xDC00 && potentialFromLow <= 0xDFFF;
flag = flag || (potentialToLow >= 0XDC00 && potentialToLow <= 0xDFFF);
// Correct the split location, moving it 'before' the high surrogate.
if(flag) {
end = end - 1;
}
}
}
// 1.3: take substring from start to the split point; determine SMP-aware length.
// This yields the SMP-aware divergence index, which gives the number of left-deletes.
let newCaret = from._kmwCodeUnitToCodePoint(end);
let deletedLeft = fromCaret - newCaret;
// Step 2: Determine the other properties.
// Since the 'after' OutputTarget's caret indicates the end of any inserted text, we
// can easily calculate the rest.
let insertedLength = toCaret - newCaret;
let delta = to._kmwSubstr(newCaret, insertedLength);
let undeletedRight = to._kmwLength() - toCaret;
let originalRight = from._kmwLength() - fromCaret;
let deletedRight = originalRight - undeletedRight;
// May occur when reverting a suggestion that had been applied mid-word.
if(deletedRight < 0) {
// Restores deleteRight characters.
delta = delta + to._kmwSubstr(toCaret, -deletedRight);
deletedRight = 0;
}
return new TextTransform(delta, deletedLeft, deletedRight);
}
buildTranscriptionFrom(original: OutputTarget, keyEvent: KeyEvent, readonly: boolean, alternates?: Alternate[]): Transcription {
let transform = this.buildTransformFrom(original);
// If we ever decide to re-add deadkey tracking, this is the place for it.
return new Transcription(keyEvent, transform, Mock.from(original, readonly), alternates);
}
/**
* Restores the `OutputTarget` to the indicated state. Designed for use with `Transcription.preInput`.
* @param original An `OutputTarget` (usually a `Mock`).
*/
restoreTo(original: OutputTarget) {
//
this.setTextBeforeCaret(original.getTextBeforeCaret());
this.setTextAfterCaret(original.getTextAfterCaret());
// Also, restore the deadkeys!
this._dks = original._dks.clone();
}
apply(transform: Transform) {
if(transform.deleteRight) {
this.setTextAfterCaret(this.getTextAfterCaret()._kmwSubstr(transform.deleteRight));
}
if(transform.deleteLeft) {
this.deleteCharsBeforeCaret(transform.deleteLeft);
}
if(transform.insert) {
this.insertTextBeforeCaret(transform.insert);
}
// We assume that all deadkeys are invalidated after applying a Transform, since
// prediction implies we'll be completing a word, post-deadkeys.
this._dks.clear();
}
/**
* Helper to `restoreTo` - allows directly setting the 'before' context to that of another
* `OutputTarget`.
* @param s
*/
protected setTextBeforeCaret(s: string): void {
// This one's easy enough to provide a default implementation for.
this.deleteCharsBeforeCaret(this.getTextBeforeCaret()._kmwLength());
this.insertTextBeforeCaret(s);
}
/**
* Helper to `restoreTo` - allows directly setting the 'after' context to that of another
* `OutputTarget`.
* @param s
*/
protected abstract setTextAfterCaret(s: string): void;
/**
* Clears any selected text within the wrapper's element(s).
* Silently does nothing if no such text exists.
*/
abstract clearSelection(): void;
/**
* Clears any cached selection-related state values.
*/
abstract invalidateSelection(): void;
/**
* Indicates whether or not the underlying element has its own selection (input, textarea)
* or is part of (or possesses) the DOM's active selection. Don't confuse with isSelectionEmpty().
*
* TODO: rename to supportsOwnSelection
*/
abstract hasSelection(): boolean;
/**
* Returns true if there is no current selection -- that is, the selection range is empty
*/
abstract isSelectionEmpty(): boolean;
/**
* Returns an index corresponding to the caret's position for use with deadkeys.
*/
abstract getDeadkeyCaret(): number;
/**
* Relative to the caret, gets the current context within the wrapper's element.
*/
abstract getTextBeforeCaret(): string;
/**
* Relative to the caret (and/or active selection), gets the element's text after the caret,
* excluding any actively selected text that would be immediately replaced upon text entry.
*/
abstract getTextAfterCaret(): string;
/**
* Gets the element's full text, including any text that is actively selected.
*/
abstract getText(): string;
/**
* Performs context deletions (from the left of the caret) as needed by the KeymanWeb engine and
* corrects the location of any affected deadkeys.
*
* Does not delete deadkeys (b/c KMW 1 & 2 behavior maintenance).
* @param dn The number of characters to delete. If negative, context will be left unchanged.
*/
abstract deleteCharsBeforeCaret(dn: number): void;
/**
* Inserts text immediately before the caret's current position, moving the caret after the
* newly inserted text in the process along with any affected deadkeys.
*
* @param s Text to insert before the caret's current position.
*/
abstract insertTextBeforeCaret(s: string): void;
/**
* Allows element-specific handling for ENTER key inputs. Conceptually, this should usually
* correspond to `insertTextBeforeCaret('\n'), but actual implementation will vary greatly among
* elements.
*/
abstract handleNewlineAtCaret(): void;
/**
* Saves element-specific state properties prone to mutation, enabling restoration after
* text-output operations.
*/
saveProperties() {
// Most element interfaces won't need anything here.
}
/**
* Restores previously-saved element-specific state properties. Designed for use after text-output
* ops to facilitate more-seamless web-dev and user interactions.
*/
restoreProperties(){
// Most element interfaces won't need anything here.
}
/**
* Generates a synthetic event on the underlying element, signalling that its value has changed.
*/
abstract doInputEvent(): void;
constructor() {
this._dks = new DeadkeyTracker();
}
// Due to some interesting requirements on compile ordering in TS,
// this needs to be in the same file as OutputTarget now.
export class Mock extends OutputTarget {
text: string;
caretIndex: number;
/**
* Signifies that this OutputTarget has no default key processing behaviors. This should be false
* for OutputTargets backed by web elements like HTMLInputElement or HTMLTextAreaElement.
*/
get isSynthetic(): boolean {
return true;
}
constructor(text?: string, caretPos?: number) {
super();
resetContext(): void {
this.deadkeys().clear();
}
this.text = text ? text : "";
var defaultLength = this.text._kmwLength();
// Ensures that `caretPos == 0` is handled correctly.
this.caretIndex = typeof caretPos == "number" ? caretPos : defaultLength;
}
deadkeys(): DeadkeyTracker {
return this._dks;
}
// Clones the state of an existing EditableElement, creating a Mock version of its state.
static from(outputTarget: OutputTarget, readonly: boolean) {
let clone: Mock;
hasDeadkeyMatch(n: number, d: number): boolean {
return this.deadkeys().isMatch(this.getDeadkeyCaret(), n, d);
}
if(outputTarget instanceof Mock) {
// Avoids the need to run expensive kmwstring.ts / `_kmwLength()`
// calculations when deep-copying Mock instances.
let priorMock = outputTarget as Mock;
clone = new Mock(priorMock.text, priorMock.caretIndex);
insertDeadkeyBeforeCaret(d: number) {
var dk: Deadkey = new Deadkey(this.getDeadkeyCaret(), d);
this.deadkeys().add(dk);
}
/**
* Should be called by each output target immediately before text mutation operations occur.
*
* Maintains solutions to old issues: I3318,I3319
* @param {number} delta Use negative values if characters were deleted, positive if characters were added.
*/
protected adjustDeadkeys(delta: number) {
this.deadkeys().adjustPositions(this.getDeadkeyCaret(), delta);
}
/**
* Needed to properly clone deadkeys for use with Mock element interfaces toward predictive text purposes.
* @param {object} dks An existing set of deadkeys to deep-copy for use by this element interface.
*/
protected setDeadkeys(dks: DeadkeyTracker) {
this._dks = dks.clone();
}
/**
* Determines the basic operations needed to reconstruct the current OutputTarget's text from the prior state specified
* by another OutputTarget based on their text and caret positions.
*
* This is designed for use as a "before and after" comparison to determine the effect of a single keyboard rule at a time.
* As such, it assumes that the caret is immediately after any inserted text.
* @param from An output target (preferably a Mock) representing the prior state of the input/output system.
*/
buildTransformFrom(original: OutputTarget): Transform {
let to = this.getText();
let from = original.getText();
let fromCaret = original.getDeadkeyCaret();
let toCaret = this.getDeadkeyCaret();
// Step 1: Determine the number of left-deletions.
let maxSMPLeftMatch = fromCaret < toCaret ? fromCaret : toCaret;
// We need the corresponding non-SMP caret location in order to binary-search efficiently.
// (Examining code units is much more computationally efficient.)
let maxLeftMatch = to._kmwCodePointToCodeUnit(maxSMPLeftMatch);
// 1.1: use a non-SMP-aware binary search to determine the divergence point.
let start = 0;
let end = maxLeftMatch; // the index AFTER the last possible matching char.
// This search is O(maxLeftMatch). 1/2 + 1/4 + 1/8 + ... converges to = 1.
while(start < end) {
let mid = Math.floor((end+start+1) / 2); // round up (compare more)
let fromLeft = from.substr(start, mid-start);
let toLeft = to.substr(start, mid-start);
if(fromLeft == toLeft) {
start = mid;
} else {
// If we're 'cloning' a different OutputTarget type, we don't have a
// guaranteed way to more efficiently get these values; these are the
// best methods specified by the abstraction.
end = mid - 1;
}
}
if(readonly) {
// for NewContext and PostOutput, we want the whole text
let text = outputTarget.getText();
let afterText = outputTarget.getTextAfterCaret();
let caretIndex = text._kmwLength() - afterText._kmwLength();
clone = new Mock(text, caretIndex);
} else {
// We choose to ignore (rather, pre-emptively remove) any actively-selected text,
// as since it's always removed instantly during any text mutation operations.
let preText = outputTarget.getTextBeforeCaret();
let caretIndex = preText._kmwLength();
clone = new Mock(preText + outputTarget.getTextAfterCaret(), caretIndex);
// At the loop's end: `end` now holds the non-SMP-aware divergence point.
// The 'caret' is after the last matching code unit.
// 1.2: detect a possible surrogate-pair split scenario, correcting for it
// (by moving the split before the high-surrogate) if detected.
// If the split location is precisely on either end of the context, we can't
// have split a surrogate pair.
if(end > 0 && end < maxLeftMatch) {
let potentialHigh = from.charCodeAt(end-1);
let potentialFromLow = from.charCodeAt(end);
let potentialToLow = to.charCodeAt(end);
// if potentialHigh is a possible high surrogate...
if(potentialHigh >= 0xD800 && potentialHigh <= 0xDBFF) {
// and at least one potential 'low' is a possible low surrogate...
let flag = potentialFromLow >= 0xDC00 && potentialFromLow <= 0xDFFF;
flag = flag || (potentialToLow >= 0XDC00 && potentialToLow <= 0xDFFF);
// Correct the split location, moving it 'before' the high surrogate.
if(flag) {
end = end - 1;
}
}
// Also duplicate deadkey state! (Needed for fat-finger ops.)
clone.setDeadkeys(outputTarget.deadkeys());
return clone;
}
clearSelection(): void {
return;
// 1.3: take substring from start to the split point; determine SMP-aware length.
// This yields the SMP-aware divergence index, which gives the number of left-deletes.
let newCaret = from._kmwCodeUnitToCodePoint(end);
let deletedLeft = fromCaret - newCaret;
// Step 2: Determine the other properties.
// Since the 'after' OutputTarget's caret indicates the end of any inserted text, we
// can easily calculate the rest.
let insertedLength = toCaret - newCaret;
let delta = to._kmwSubstr(newCaret, insertedLength);
let undeletedRight = to._kmwLength() - toCaret;
let originalRight = from._kmwLength() - fromCaret;
let deletedRight = originalRight - undeletedRight;
// May occur when reverting a suggestion that had been applied mid-word.
if(deletedRight < 0) {
// Restores deleteRight characters.
delta = delta + to._kmwSubstr(toCaret, -deletedRight);
deletedRight = 0;
}
invalidateSelection(): void {
return;
return new TextTransform(delta, deletedLeft, deletedRight);
}
buildTranscriptionFrom(original: OutputTarget, keyEvent: KeyEvent, readonly: boolean, alternates?: Alternate[]): Transcription {
let transform = this.buildTransformFrom(original);
// If we ever decide to re-add deadkey tracking, this is the place for it.
return new Transcription(keyEvent, transform, Mock.from(original, readonly), alternates);
}
/**
* Restores the `OutputTarget` to the indicated state. Designed for use with `Transcription.preInput`.
* @param original An `OutputTarget` (usually a `Mock`).
*/
restoreTo(original: OutputTarget) {
//
this.setTextBeforeCaret(original.getTextBeforeCaret());
this.setTextAfterCaret(original.getTextAfterCaret());
// Also, restore the deadkeys!
this._dks = original._dks.clone();
}
apply(transform: Transform) {
if(transform.deleteRight) {
this.setTextAfterCaret(this.getTextAfterCaret()._kmwSubstr(transform.deleteRight));
}
isSelectionEmpty(): boolean {
// TODO: consider if we need to maintain selection information in Mocks
return true;
if(transform.deleteLeft) {
this.deleteCharsBeforeCaret(transform.deleteLeft);
}
hasSelection(): boolean {
return true;
if(transform.insert) {
this.insertTextBeforeCaret(transform.insert);
}
getDeadkeyCaret(): number {
return this.caretIndex;
}
// We assume that all deadkeys are invalidated after applying a Transform, since
// prediction implies we'll be completing a word, post-deadkeys.
this._dks.clear();
}
setDeadkeyCaret(index: number) {
if(index < 0 || index > this.text._kmwLength()) {
throw new Error("Provided caret index is out of range.");
}
this.caretIndex = index;
}
/**
* Helper to `restoreTo` - allows directly setting the 'before' context to that of another
* `OutputTarget`.
* @param s
*/
protected setTextBeforeCaret(s: string): void {
// This one's easy enough to provide a default implementation for.
this.deleteCharsBeforeCaret(this.getTextBeforeCaret()._kmwLength());
this.insertTextBeforeCaret(s);
}
getTextBeforeCaret(): string {
return this.text.kmwSubstr(0, this.caretIndex);
}
/**
* Helper to `restoreTo` - allows directly setting the 'after' context to that of another
* `OutputTarget`.
* @param s
*/
protected abstract setTextAfterCaret(s: string): void;
getTextAfterCaret(): string {
return this.text.kmwSubstr(this.caretIndex);
}
/**
* Clears any selected text within the wrapper's element(s).
* Silently does nothing if no such text exists.
*/
abstract clearSelection(): void;
getText(): string {
return this.text;
}
/**
* Clears any cached selection-related state values.
*/
abstract invalidateSelection(): void;
deleteCharsBeforeCaret(dn: number): void {
if(dn >= 0) {
if(dn > this.caretIndex) {
dn = this.caretIndex;
}
this.adjustDeadkeys(-dn);
this.text = this.text.kmwSubstr(0, this.caretIndex - dn) + this.getTextAfterCaret();
this.caretIndex -= dn;
/**
* Indicates whether or not the underlying element has its own selection (input, textarea)
* or is part of (or possesses) the DOM's active selection. Don't confuse with isSelectionEmpty().
*
* TODO: rename to supportsOwnSelection
*/
abstract hasSelection(): boolean;
/**
* Returns true if there is no current selection -- that is, the selection range is empty
*/
abstract isSelectionEmpty(): boolean;
/**
* Returns an index corresponding to the caret's position for use with deadkeys.
*/
abstract getDeadkeyCaret(): number;
/**
* Relative to the caret, gets the current context within the wrapper's element.
*/
abstract getTextBeforeCaret(): string;
/**
* Relative to the caret (and/or active selection), gets the element's text after the caret,
* excluding any actively selected text that would be immediately replaced upon text entry.
*/
abstract getTextAfterCaret(): string;
/**
* Gets the element's full text, including any text that is actively selected.
*/
abstract getText(): string;
/**
* Performs context deletions (from the left of the caret) as needed by the KeymanWeb engine and
* corrects the location of any affected deadkeys.
*
* Does not delete deadkeys (b/c KMW 1 & 2 behavior maintenance).
* @param dn The number of characters to delete. If negative, context will be left unchanged.
*/
abstract deleteCharsBeforeCaret(dn: number): void;
/**
* Inserts text immediately before the caret's current position, moving the caret after the
* newly inserted text in the process along with any affected deadkeys.
*
* @param s Text to insert before the caret's current position.
*/
abstract insertTextBeforeCaret(s: string): void;
/**
* Allows element-specific handling for ENTER key inputs. Conceptually, this should usually
* correspond to `insertTextBeforeCaret('\n'), but actual implementation will vary greatly among
* elements.
*/
abstract handleNewlineAtCaret(): void;
/**
* Saves element-specific state properties prone to mutation, enabling restoration after
* text-output operations.
*/
saveProperties() {
// Most element interfaces won't need anything here.
}
/**
* Restores previously-saved element-specific state properties. Designed for use after text-output
* ops to facilitate more-seamless web-dev and user interactions.
*/
restoreProperties(){
// Most element interfaces won't need anything here.
}
/**
* Generates a synthetic event on the underlying element, signalling that its value has changed.
*/
abstract doInputEvent(): void;
}
// Due to some interesting requirements on compile ordering in TS,
// this needs to be in the same file as OutputTarget now.
export class Mock extends OutputTarget {
text: string;
caretIndex: number;
constructor(text?: string, caretPos?: number) {
super();
this.text = text ? text : "";
var defaultLength = this.text._kmwLength();
// Ensures that `caretPos == 0` is handled correctly.
this.caretIndex = typeof caretPos == "number" ? caretPos : defaultLength;
}
// Clones the state of an existing EditableElement, creating a Mock version of its state.
static from(outputTarget: OutputTarget, readonly: boolean) {
let clone: Mock;
if(outputTarget instanceof Mock) {
// Avoids the need to run expensive kmwstring.ts / `_kmwLength()`
// calculations when deep-copying Mock instances.
let priorMock = outputTarget as Mock;
clone = new Mock(priorMock.text, priorMock.caretIndex);
} else {
// If we're 'cloning' a different OutputTarget type, we don't have a
// guaranteed way to more efficiently get these values; these are the
// best methods specified by the abstraction.
if(readonly) {
// for NewContext and PostOutput, we want the whole text
let text = outputTarget.getText();
let afterText = outputTarget.getTextAfterCaret();
let caretIndex = text._kmwLength() - afterText._kmwLength();
clone = new Mock(text, caretIndex);
} else {
// We choose to ignore (rather, pre-emptively remove) any actively-selected text,
// as since it's always removed instantly during any text mutation operations.
let preText = outputTarget.getTextBeforeCaret();
let caretIndex = preText._kmwLength();
clone = new Mock(preText + outputTarget.getTextAfterCaret(), caretIndex);
}
}
insertTextBeforeCaret(s: string): void {
this.adjustDeadkeys(s._kmwLength());
this.text = this.getTextBeforeCaret() + s + this.getTextAfterCaret();
this.caretIndex += s.kmwLength();
}
// Also duplicate deadkey state! (Needed for fat-finger ops.)
clone.setDeadkeys(outputTarget.deadkeys());
handleNewlineAtCaret(): void {
this.insertTextBeforeCaret('\n');
}
return clone;
}
protected setTextAfterCaret(s: string): void {
this.text = this.getTextBeforeCaret() + s;
}
clearSelection(): void {
return;
}
doInputEvent() {
// Mock isn't backed by an element, so it won't have any event listeners.
invalidateSelection(): void {
return;
}
isSelectionEmpty(): boolean {
// TODO: consider if we need to maintain selection information in Mocks
return true;
}
hasSelection(): boolean {
return true;
}
getDeadkeyCaret(): number {
return this.caretIndex;
}
setDeadkeyCaret(index: number) {
if(index < 0 || index > this.text._kmwLength()) {
throw new Error("Provided caret index is out of range.");
}
this.caretIndex = index;
}
getTextBeforeCaret(): string {
return this.text.kmwSubstr(0, this.caretIndex);
}
getTextAfterCaret(): string {
return this.text.kmwSubstr(this.caretIndex);
}
getText(): string {
return this.text;
}
deleteCharsBeforeCaret(dn: number): void {
if(dn >= 0) {
if(dn > this.caretIndex) {
dn = this.caretIndex;
}
this.adjustDeadkeys(-dn);
this.text = this.text.kmwSubstr(0, this.caretIndex - dn) + this.getTextAfterCaret();
this.caretIndex -= dn;
}
}
insertTextBeforeCaret(s: string): void {
this.adjustDeadkeys(s._kmwLength());
this.text = this.getTextBeforeCaret() + s + this.getTextAfterCaret();
this.caretIndex += s.kmwLength();
}
handleNewlineAtCaret(): void {
this.insertTextBeforeCaret('\n');
}
protected setTextAfterCaret(s: string): void {
this.text = this.getTextBeforeCaret() + s;
}
doInputEvent() {
// Mock isn't backed by an element, so it won't have any event listeners.
}
}

View file

@ -1,133 +1,139 @@
namespace com.keyman.text {
///<reference types="@keymanapp/models-types" />
import DefaultOutput from "./defaultOutput.js";
import KeyboardProcessor from "./keyboardProcessor.js";
import OutputTarget, { Mock, type Transcription } from "./outputTarget.js";
import { VariableStoreDictionary } from "../keyboards/keyboard.js";
import type { VariableStore } from "./kbdInterface.js";
/**
* Represents the commands and state changes that result from a matched keyboard rule.
*/
export default class RuleBehavior {
/**
* Represents the commands and state changes that result from a matched keyboard rule.
* The before-and-after Transform from matching a keyboard rule. May be `null`
* if no keyboard rules were matched for the keystroke.
*/
export class RuleBehavior {
/**
* The before-and-after Transform from matching a keyboard rule. May be `null`
* if no keyboard rules were matched for the keystroke.
*/
transcription: Transcription = null;
transcription: Transcription = null;
/**
* Indicates whether or not a BEEP command was issued by the matched keyboard rule.
*/
beep?: boolean;
/**
* Indicates whether or not a BEEP command was issued by the matched keyboard rule.
*/
beep?: boolean;
/**
* A set of changed store values triggered by the matched keyboard rule.
*/
setStore: {[id: number]: string} = {};
/**
* A set of changed store values triggered by the matched keyboard rule.
*/
setStore: {[id: number]: string} = {};
/**
* A set of variable stores with save requests triggered by the matched keyboard rule
*/
saveStore: {[name: string]: VariableStore} = {};
/**
* A set of variable stores with save requests triggered by the matched keyboard rule
*/
saveStore: {[name: string]: VariableStore} = {};
/**
* A set of variable stores with possible changes to be applied during finalization.
*/
variableStores: keyboards.VariableStoreDictionary = {};
/**
* A set of variable stores with possible changes to be applied during finalization.
*/
variableStores: VariableStoreDictionary = {};
/**
* Denotes a non-output default behavior; this should be evaluated later, against the true keystroke.
*/
triggersDefaultCommand: boolean = false;
/**
* Denotes a non-output default behavior; this should be evaluated later, against the true keystroke.
*/
triggersDefaultCommand: boolean = false;
/**
* Denotes error log messages generated when attempting to generate this behavior.
*/
errorLog?: string;
/**
* Denotes error log messages generated when attempting to generate this behavior.
*/
errorLog?: string;
/**
* Denotes warning log messages generated when attempting to generate this behavior.
*/
warningLog?: string;
/**
* Denotes warning log messages generated when attempting to generate this behavior.
*/
warningLog?: string;
/**
* If predictive text is active, contains a Promise returning predictive Suggestions.
*/
predictionPromise?: Promise<Suggestion[]>;
/**
* If predictive text is active, contains a Promise returning predictive Suggestions.
*/
predictionPromise?: Promise<Suggestion[]>;
/**
* In reference to https://github.com/keymanapp/keyman/pull/4350#issuecomment-768753852:
*
* If the final group processed is a context and keystroke group (using keys),
* and there is no nomatch rule, and the keystroke is not matched in the group,
* the keystroke's default behavior should trigger, regardless of whether or not any
* rules in prior groups matched.
*/
triggerKeyDefault?: boolean;
/**
* In reference to https://github.com/keymanapp/keyman/pull/4350#issuecomment-768753852:
*
* If the final group processed is a context and keystroke group (using keys),
* and there is no nomatch rule, and the keystroke is not matched in the group,
* the keystroke's default behavior should trigger, regardless of whether or not any
* rules in prior groups matched.
*/
triggerKeyDefault?: boolean;
finalize(processor: KeyboardProcessor, outputTarget: OutputTarget, readonly: boolean) {
if(!this.transcription) {
throw "Cannot finalize a RuleBehavior with no transcription.";
}
finalize(processor: KeyboardProcessor, outputTarget: OutputTarget, readonly: boolean) {
if(!this.transcription) {
throw "Cannot finalize a RuleBehavior with no transcription.";
}
if(processor.beepHandler && this.beep) {
processor.beepHandler(outputTarget);
}
if(processor.beepHandler && this.beep) {
processor.beepHandler(outputTarget);
}
for(let storeID in this.setStore) {
let sysStore = processor.keyboardInterface.systemStores[storeID];
if(sysStore) {
try {
sysStore.set(this.setStore[storeID]);
} catch (error) {
if(processor.errorLogger) {
processor.errorLogger("Rule attempted to perform illegal operation - 'platform' may not be changed.");
}
for(let storeID in this.setStore) {
let sysStore = processor.keyboardInterface.systemStores[storeID];
if(sysStore) {
try {
sysStore.set(this.setStore[storeID]);
} catch (error) {
if(processor.errorLogger) {
processor.errorLogger("Rule attempted to perform illegal operation - 'platform' may not be changed.");
}
} else if(processor.warningLogger) {
processor.warningLogger("Unknown store affected by keyboard rule: " + storeID);
}
}
processor.keyboardInterface.applyVariableStores(this.variableStores);
if(processor.keyboardInterface.variableStoreSerializer) {
for(let storeID in this.saveStore) {
processor.keyboardInterface.variableStoreSerializer.saveStore(processor.activeKeyboard.id, storeID, this.saveStore[storeID]);
}
}
if(this.triggersDefaultCommand) {
let keyEvent = this.transcription.keystroke;
DefaultOutput.applyCommand(keyEvent, outputTarget);
}
if(processor.warningLogger && this.warningLog) {
processor.warningLogger(this.warningLog);
} else if(processor.errorLogger && this.errorLog) {
processor.errorLogger(this.errorLog);
} else if(processor.warningLogger) {
processor.warningLogger("Unknown store affected by keyboard rule: " + storeID);
}
}
/**
* Merges default-related behaviors from another RuleBehavior into this one. Assumes that the current instance
* "came first" chronologically. Both RuleBehaviors must be sourced from the same keystroke.
*
* Intended use: merging rule-based behavior with default key behavior during scenarios like those described
* at https://github.com/keymanapp/keyman/pull/4350#issuecomment-768753852.
*
* This function does not attempt a "complete" merge for two fully-constructed RuleBehaviors! Things
* WILL break for unintended uses.
* @param other
*/
mergeInDefaults(other: RuleBehavior) {
let keystroke = this.transcription.keystroke;
let keyFromOther = other.transcription.keystroke;
if(keystroke.Lcode != keyFromOther.Lcode || keystroke.Lmodifiers != keyFromOther.Lmodifiers) {
throw "RuleBehavior default-merge not supported unless keystrokes are identical!";
processor.keyboardInterface.applyVariableStores(this.variableStores);
if(processor.keyboardInterface.variableStoreSerializer) {
for(let storeID in this.saveStore) {
processor.keyboardInterface.variableStoreSerializer.saveStore(processor.activeKeyboard.id, storeID, this.saveStore[storeID]);
}
}
this.triggersDefaultCommand = this.triggersDefaultCommand || other.triggersDefaultCommand;
if(this.triggersDefaultCommand) {
let keyEvent = this.transcription.keystroke;
DefaultOutput.applyCommand(keyEvent, outputTarget);
}
let mergingMock = Mock.from(this.transcription.preInput, false);
mergingMock.apply(this.transcription.transform);
mergingMock.apply(other.transcription.transform);
this.transcription = mergingMock.buildTranscriptionFrom(this.transcription.preInput, keystroke, false, this.transcription.alternates);
if(processor.warningLogger && this.warningLog) {
processor.warningLogger(this.warningLog);
} else if(processor.errorLogger && this.errorLog) {
processor.errorLogger(this.errorLog);
}
}
/**
* Merges default-related behaviors from another RuleBehavior into this one. Assumes that the current instance
* "came first" chronologically. Both RuleBehaviors must be sourced from the same keystroke.
*
* Intended use: merging rule-based behavior with default key behavior during scenarios like those described
* at https://github.com/keymanapp/keyman/pull/4350#issuecomment-768753852.
*
* This function does not attempt a "complete" merge for two fully-constructed RuleBehaviors! Things
* WILL break for unintended uses.
* @param other
*/
mergeInDefaults(other: RuleBehavior) {
let keystroke = this.transcription.keystroke;
let keyFromOther = other.transcription.keystroke;
if(keystroke.Lcode != keyFromOther.Lcode || keystroke.Lmodifiers != keyFromOther.Lmodifiers) {
throw "RuleBehavior default-merge not supported unless keystrokes are identical!";
}
this.triggersDefaultCommand = this.triggersDefaultCommand || other.triggersDefaultCommand;
let mergingMock = Mock.from(this.transcription.preInput, false);
mergingMock.apply(this.transcription.transform);
mergingMock.apply(other.transcription.transform);
this.transcription = mergingMock.buildTranscriptionFrom(this.transcription.preInput, keystroke, false, this.transcription.alternates);
}
}

View file

@ -1,133 +1,134 @@
namespace com.keyman.text {
/**
* Defines common behaviors associated with system stores.
*/
export abstract class SystemStore {
public readonly id: number;
import type KeyboardInterface from "./kbdInterface.js";
import { SystemStoreIDs } from "./kbdInterface.js";
constructor(id: number) {
this.id = id;
}
/**
* Defines common behaviors associated with system stores.
*/
export abstract class SystemStore {
public readonly id: number;
abstract matches(value: string): boolean;
set(value: string): void {
throw new Error("System store with ID " + this.id + " may not be directly set.");
}
constructor(id: number) {
this.id = id;
}
/**
* A handler designed to receive feedback whenever a system store's value is changed.
* @param source The system store being mutated, before the value change occurs.
* @param newValue The new value being set
* @returns `false` / `undefined` to allow the change, `true` to block the change.
*/
export type SystemStoreMutationHandler = (source: MutableSystemStore, newValue: string) => boolean;
abstract matches(value: string): boolean;
export class MutableSystemStore extends SystemStore {
private _value: string;
handler?: SystemStoreMutationHandler = null;
set(value: string): void {
throw new Error("System store with ID " + this.id + " may not be directly set.");
}
}
constructor(id: number, defaultValue: string) {
super(id);
this._value = defaultValue;
}
/**
* A handler designed to receive feedback whenever a system store's value is changed.
* @param source The system store being mutated, before the value change occurs.
* @param newValue The new value being set
* @returns `false` / `undefined` to allow the change, `true` to block the change.
*/
export type SystemStoreMutationHandler = (source: MutableSystemStore, newValue: string) => boolean;
get value() {
return this._value;
}
export class MutableSystemStore extends SystemStore {
private _value: string;
handler?: SystemStoreMutationHandler = null;
matches(value: string) {
return this._value == value;
}
constructor(id: number, defaultValue: string) {
super(id);
this._value = defaultValue;
}
set(value: string) {
// Even if things stay the same, we should still signal this.
// It's important for tracking if a rule directly set the layer
// versus if it passively remained.
if(this.handler) {
if(this.handler(this, value)) {
return;
}
get value() {
return this._value;
}
matches(value: string) {
return this._value == value;
}
set(value: string) {
// Even if things stay the same, we should still signal this.
// It's important for tracking if a rule directly set the layer
// versus if it passively remained.
if(this.handler) {
if(this.handler(this, value)) {
return;
}
this._value = value;
}
this._value = value;
}
}
/**
* Handles checks against the current platform.
*/
export class PlatformSystemStore extends SystemStore {
private readonly kbdInterface: KeyboardInterface;
constructor(keyboardInterface: KeyboardInterface) {
super(SystemStoreIDs.TSS_PLATFORM);
this.kbdInterface = keyboardInterface;
}
/**
* Handles checks against the current platform.
*/
export class PlatformSystemStore extends SystemStore {
private readonly kbdInterface: KeyboardInterface;
matches(value: string) {
var i,constraint,constraints=value.split(' ');
let device = this.kbdInterface.activeDevice;
constructor(keyboardInterface: KeyboardInterface) {
super(KeyboardInterface.TSS_PLATFORM);
this.kbdInterface = keyboardInterface;
}
matches(value: string) {
var i,constraint,constraints=value.split(' ');
let device = this.kbdInterface.activeDevice;
for(i=0; i<constraints.length; i++) {
constraint=constraints[i].toLowerCase();
switch(constraint) {
case 'touch':
case 'hardware':
if(device.touchable != (constraint == 'touch')) {
return false;
}
break;
case 'macos':
case 'mac':
constraint = 'macosx';
// fall through
case 'macosx':
case 'windows':
case 'android':
case 'ios':
case 'linux':
if(device.OS != constraint) {
return false;
}
break;
case 'tablet':
case 'phone':
case 'desktop':
if(device.formFactor != constraint) {
return false;
}
break;
case 'web':
if(device.browser == 'native') {
return false; // web matches anything other than 'native'
}
break;
case 'native':
// This will return true for embedded KeymanWeb
case 'chrome':
case 'firefox':
case 'safari':
case 'edge':
case 'opera':
if(device.browser != constraint) {
return false;
}
break;
default:
for(i=0; i<constraints.length; i++) {
constraint=constraints[i].toLowerCase();
switch(constraint) {
case 'touch':
case 'hardware':
if(device.touchable != (constraint == 'touch')) {
return false;
}
}
}
break;
// Everything we checked against was valid and had matches - it's a match!
return true;
case 'macos':
case 'mac':
constraint = 'macosx';
// fall through
case 'macosx':
case 'windows':
case 'android':
case 'ios':
case 'linux':
if(device.OS != constraint) {
return false;
}
break;
case 'tablet':
case 'phone':
case 'desktop':
if(device.formFactor != constraint) {
return false;
}
break;
case 'web':
if(device.browser == 'native') {
return false; // web matches anything other than 'native'
}
break;
case 'native':
// This will return true for embedded KeymanWeb
case 'chrome':
case 'firefox':
case 'safari':
case 'edge':
case 'opera':
if(device.browser != constraint) {
return false;
}
break;
default:
return false;
}
}
// Everything we checked against was valid and had matches - it's a match!
return true;
}
}

View file

@ -1,15 +0,0 @@
{
// This variant of the tsconfig.json exists to create a 'leaf', 'bundled'
// version of the keyboard-processor build product. The same reference
// cannot be prepended twice in a composite tsc build, posing problems
// for certain down-line builds if the two tsconfigs are not differentiated.
"extends": "./tsconfig.json",
"compilerOptions": {
"outFile": "../build/index.bundled.js"
},
"references": [
{ "path": "../../../models/types" },
{ "path": "../../keyman-version/", "prepend": true },
{ "path": "../../utils/", "prepend": true}
]
}

View file

@ -1,24 +0,0 @@
{
"extends": "../../../../tsconfig-base.json",
"compilerOptions": {
"allowJs": true,
"module": "none",
"outDir": "../build/",
"declaration": true,
"inlineSources": true,
"sourceMap": true,
"sourceRoot": "keyman/",
"target": "es5",
"types": ["node"],
"lib": ["es6"],
"outFile": "../build/index.js",
"experimentalDecorators": true,
},
"references": [
{ "path": "../../../models/types" },
{ "path": "../../keyman-version/" },
{ "path": "../../utils/" }
],
"include": ["./**/*.ts"],
"files": ["text/outputTarget.ts", "text/keyboardProcessor.ts"]
}

View file

@ -1,20 +1,18 @@
var assert = require('chai').assert;
let fs = require('fs');
let vm = require('vm');
import { assert } from 'chai';
import fs from 'fs';
import vm from 'vm';
let KeyboardProcessor = require('../../build/index.bundled.js');
// Required initialization setup.
global.com = KeyboardProcessor.com; // exports all keyboard-processor namespacing.
let KMWRecorder = require('../../../recorder/build/nodeProctor');
import KeyboardProcessor from '@keymanapp/keyboard-processor/build/obj/text/keyboardProcessor.js';
import { KeyboardTest } from '@keymanapp/recorder-core/build/obj/index.js';
import NodeProctor from '@keymanapp/recorder-core/build/obj/nodeProctor.js';
describe('Engine - Basic Simulation', function() {
let testJSONtext = fs.readFileSync('../../test/resources/json/engine_tests/basic_lao_simulation.json');
// Common test suite setup.
let testSuite = new KMWRecorder.KeyboardTest(JSON.parse(testJSONtext));
let testSuite = new KeyboardTest(JSON.parse(testJSONtext));
var keyboard;
let device = {
formFactor: 'desktop',
OS: 'windows',
@ -41,14 +39,14 @@ describe('Engine - Basic Simulation', function() {
// Converts each test set into its own Mocha-level test.
for(let set of testSuite.inputTestSets) {
let proctor = new KMWRecorder.NodeProctor(keyboard, device, assert.equal);
let proctor = new NodeProctor(keyboard, device, assert.equal);
if(!proctor.compatibleWithSuite(testSuite)) {
it.skip(set.toTestName() + " - Cannot run this test suite on Node.");
} else {
it(set.toTestName(), function() {
// Refresh the proctor instance at runtime.
let proctor = new KMWRecorder.NodeProctor(keyboard, device, assert.equal);
let proctor = new NodeProctor(keyboard, device, assert.equal);
set.test(proctor);
});
}

View file

@ -1,11 +1,9 @@
var assert = require('chai').assert;
var fs = require("fs");
var vm = require("vm");
import { assert } from 'chai';
import fs from 'fs';
import vm from 'vm';
let KeyboardProcessor = require('../../build/index.bundled.js');
import KeyboardProcessor from '@keymanapp/keyboard-processor/build/obj/text/keyboardProcessor.js';
// Required initialization setup.
global.com = KeyboardProcessor.com; // exports all keyboard-processor namespacing.
global.keyman = {}; // So that keyboard-based checks against the global `keyman` succeed.
// 10.0+ dependent keyboards, like khmer_angkor, will otherwise fail to load.

View file

@ -0,0 +1,50 @@
import { assert } from "chai";
import * as Package from "../../build/lib/index.mjs";
// A few small tests to ensure that the ES Module bundle was successfully constructed and is usable.
var toSupplementaryPairString = function(code){
var H = Math.floor((code - 0x10000) / 0x400) + 0xD800;
var L = (code - 0x10000) % 0x400 + 0xDC00;
return String.fromCharCode(H, L);
}
let u = toSupplementaryPairString;
describe('Bundled ES Module', function() {
describe('KeyboardProcessor', function () {
it('should initialize without errors', function () {
let kp = new Package.KeyboardProcessor();
assert.isNotNull(kp);
});
});
describe('Mock', () => {
it('basic functionality test', () => {
let target = new Package.Mock("aple", 2); // ap | le
target.insertTextBeforeCaret('p');
assert.equal(target.getText(), "apple");
});
it('smp test', () => {
// Is installed as a _side effect_ from importing the module.
// We could disable that and require a call of `extendString()` instead.
String.kmwEnableSupplementaryPlane(true); // Declared & defined in web-utils.
try {
let target = new Package.Mock(u(0x1d5ba)+u(0x1d5c9)+u(0x1d5c5)+u(0x1d5be), 2); // ap | le
target.insertTextBeforeCaret(u(0x1d5c9));
assert.equal(target.getText(), u(0x1d5ba)+u(0x1d5c9)+u(0x1d5c9)+u(0x1d5c5)+u(0x1d5be));
} finally {
String.kmwEnableSupplementaryPlane(false);
}
});
});
describe("Imported `utils`", function() {
it("should include `utils` package's Version class", () => {
let v16 = new Package.Version([16, 1]);
assert.equal(v16.toString(), "16.1");
});
})
});

View file

@ -1,19 +1,18 @@
var assert = require('chai').assert;
let fs = require('fs');
let vm = require('vm');
import { assert } from 'chai';
import fs from 'fs';
import vm from 'vm';
let KeyboardProcessor = require('../../build/index.bundled.js');
let KMWRecorder = require('../../../recorder/build/nodeProctor');
import Codes from '@keymanapp/keyboard-processor/build/obj/text/codes.js';
import KeyboardInterface from '@keymanapp/keyboard-processor/build/obj/text/kbdInterface.js';
import KeyboardProcessor from '@keymanapp/keyboard-processor/build/obj/text/keyboardProcessor.js';
// Required initialization setup.
global.com = KeyboardProcessor.com; // exports all keyboard-processor namespacing.
let KeyboardInterface = com.keyman.text.KeyboardInterface;
let Codes = com.keyman.text.Codes;
import { KeyboardTest } from '@keymanapp/recorder-core/build/obj/index.js';
import NodeProctor from '@keymanapp/recorder-core/build/obj/nodeProctor.js';
describe('Engine - Chirality', function() {
let testJSONtext = fs.readFileSync('../../test/resources/json/engine_tests/chirality.json');
// Common test suite setup.
let testSuite = new KMWRecorder.KeyboardTest(JSON.parse(testJSONtext));
let testSuite = new KeyboardTest(JSON.parse(testJSONtext));
var keyboard;
let device = {
@ -42,14 +41,14 @@ describe('Engine - Chirality', function() {
// Converts each test set into its own Mocha-level test.
for(let set of testSuite.inputTestSets) {
let proctor = new KMWRecorder.NodeProctor(keyboard, device, assert.equal);
let proctor = new NodeProctor(keyboard, device, assert.equal);
if(!proctor.compatibleWithSuite(testSuite)) {
it.skip(set.toTestName() + " - Cannot run this test suite on Node.");
} else if(set.constraint.target == 'hardware') {
it(set.toTestName(), function() {
// Refresh the proctor instance at runtime.
let proctor = new KMWRecorder.NodeProctor(keyboard, device, assert.equal);
let proctor = new NodeProctor(keyboard, device, assert.equal);
set.test(proctor);
});
} else {
@ -92,7 +91,7 @@ describe('Engine - Chirality', function() {
// We should get the same results whether or not there actually is a corresponding modifier
// expected by the rule we're examining.
mappedModifiers = KeyboardInterface.matchModifiersToRuleChirality(initialModifiers, VIRTUAL_KEY_CODE);
let mappedModifiers = KeyboardInterface.matchModifiersToRuleChirality(initialModifiers, VIRTUAL_KEY_CODE);
assert.equal(targetModifiers, mappedModifiers);
mappedModifiers = KeyboardInterface.matchModifiersToRuleChirality(initialModifiers, VIRTUAL_KEY_CODE | ALT_CODE);
@ -111,7 +110,7 @@ describe('Engine - Chirality', function() {
// We should get the same results whether or not there actually is a corresponding modifier
// expected by the rule we're examining.
mappedModifiers = KeyboardInterface.matchModifiersToRuleChirality(initialModifiers, VIRTUAL_KEY_CODE);
let mappedModifiers = KeyboardInterface.matchModifiersToRuleChirality(initialModifiers, VIRTUAL_KEY_CODE);
assert.equal(targetModifiers, mappedModifiers);
let ctrlPlusAlt = ALT_CODE | CTRL_CODE;
@ -250,7 +249,7 @@ describe('Engine - Chirality', function() {
let modifierTarget = VIRTUAL_KEY_CODE | ALT_CODE | LCTRL_CODE | SHIFT_CODE;
mappedModifiers = KeyboardInterface.matchModifiersToRuleChirality(initialModifiers, VIRTUAL_KEY_CODE | LALT_CODE | RCTRL_CODE);
let mappedModifiers = KeyboardInterface.matchModifiersToRuleChirality(initialModifiers, VIRTUAL_KEY_CODE | LALT_CODE | RCTRL_CODE);
assert.equal(modifierTarget, mappedModifiers);
});
});

View file

@ -1,18 +1,16 @@
var assert = require('chai').assert;
let fs = require('fs');
let vm = require('vm');
import { assert } from 'chai';
import fs from 'fs';
import vm from 'vm';
let KeyboardProcessor = require('../../build/index.bundled.js');
import KeyboardProcessor from '@keymanapp/keyboard-processor/build/obj/text/keyboardProcessor.js';
// Required initialization setup.
global.com = KeyboardProcessor.com; // exports all keyboard-processor namespacing.
let KMWRecorder = require('../../../recorder/build/nodeProctor');
import { KeyboardTest } from '@keymanapp/recorder-core/build/obj/index.js';
import NodeProctor from '@keymanapp/recorder-core/build/obj/nodeProctor.js';
describe('Engine - Deadkeys', function() {
let testJSONtext = fs.readFileSync('../../test/resources/json/engine_tests/deadkeys.json');
// Common test suite setup.
let testSuite = new KMWRecorder.KeyboardTest(JSON.parse(testJSONtext));
let testSuite = new KeyboardTest(JSON.parse(testJSONtext));
var keyboard;
let device = {
@ -41,14 +39,14 @@ describe('Engine - Deadkeys', function() {
// Converts each test set into its own Mocha-level test.
for(let set of testSuite.inputTestSets) {
let proctor = new KMWRecorder.NodeProctor(keyboard, device, assert.equal);
let proctor = new NodeProctor(keyboard, device, assert.equal);
if(!proctor.compatibleWithSuite(testSuite)) {
it.skip(set.toTestName() + " - Cannot run this test suite on Node.");
} else {
it(set.toTestName(), function() {
// Refresh the proctor instance at runtime.
let proctor = new KMWRecorder.NodeProctor(keyboard, device, assert.equal);
let proctor = new NodeProctor(keyboard, device, assert.equal);
set.test(proctor);
});
}

View file

@ -1,13 +1,12 @@
var assert = require('chai').assert;
let fs = require('fs');
let vm = require('vm');
import { assert } from 'chai';
import fs from 'fs';
import vm from 'vm';
let KeyboardProcessor = require('../../../build/index.bundled.js');
import KeyboardProcessor from '@keymanapp/keyboard-processor/build/obj/text/keyboardProcessor.js';
import { Mock } from '@keymanapp/keyboard-processor/build/obj/text/outputTarget.js';
// Required initialization setup.
global.com = KeyboardProcessor.com; // exports all keyboard-processor namespacing.
let KMWRecorder = require('../../../../recorder/build/nodeProctor');
import { RecordedKeystrokeSequence } from '@keymanapp/recorder-core/build/obj/index.js';
import NodeProctor from '@keymanapp/recorder-core/build/obj/nodeProctor.js';
/*
* ABOUT THIS TEST SUITE
@ -60,11 +59,11 @@ function runEngineRuleSet(ruleSet, defaultNoun) {
for(var j = 0; j < matchDefs.length; j++) {
// Prepare the context!
var matchTest = matchDefs[j];
var ruleSeq = new KMWRecorder.RecordedKeystrokeSequence(matchTest.sequence);
let proctor = new KMWRecorder.NodeProctor(keyboard, device, assert.equal);
var ruleSeq = new RecordedKeystrokeSequence(matchTest.sequence);
let proctor = new NodeProctor(keyboard, device, assert.equal);
// We want to specify the OutputTarget for this test; our actual concern is the resulting context.
var target = new com.keyman.text.Mock();
var target = new Mock();
ruleSeq.test(proctor, target);
// Now for the real test!
@ -999,11 +998,11 @@ describe('Engine - Context Matching', function() {
for(var j = 0; j < matchDefs.length; j++) {
// Prepare the context!
var ruleDef = matchDefs[j];
var ruleSeq = new KMWRecorder.RecordedKeystrokeSequence(ruleDef.baseSequence);
let proctor = new KMWRecorder.NodeProctor(keyboard, device, assert.equal);
var ruleSeq = new RecordedKeystrokeSequence(ruleDef.baseSequence);
let proctor = new NodeProctor(keyboard, device, assert.equal);
// We want to specify the OutputTarget for this test; our actual concern is the resulting context.
var target = new com.keyman.text.Mock();
var target = new Mock();
ruleSeq.test(proctor, target);
// Now for the real test!

View file

@ -1,15 +1,16 @@
const assert = require('chai').assert;
const fs = require('fs');
const vm = require('vm');
import { assert } from 'chai';
import fs from 'fs';
import vm from 'vm';
let KeyboardProcessor = require('../../../build/index.bundled.js');
import KeyboardProcessor from '@keymanapp/keyboard-processor/build/obj/text/keyboardProcessor.js';
import { Mock } from '@keymanapp/keyboard-processor/build/obj/text/outputTarget.js';
// Required initialization setup.
global.com = KeyboardProcessor.com; // exports all keyboard-processor namespacing.
global.keyman = {}; // So that keyboard-based checks against the global `keyman` succeed.
// 10.0+ dependent keyboards, like khmer_angkor, will otherwise fail to load.
import { RecordedKeystrokeSequence } from '@keymanapp/recorder-core/build/obj/index.js';
import NodeProctor from '@keymanapp/recorder-core/build/obj/nodeProctor.js';
let KMWRecorder = require('../../../../recorder/build/nodeProctor');
import extendString from '@keymanapp/web-utils/build/obj/kmwstring.js'
extendString(); // Ensure KMW's string-extension functionality is available.
// Initialize supplementary plane string extensions
String.kmwEnableSupplementaryPlane(false);
@ -25,9 +26,9 @@ let keyboard;
function runEngineRuleSet(ruleSet) {
for(let ruleDef of ruleSet) {
// Prepare the context!
const ruleSeq = new KMWRecorder.RecordedKeystrokeSequence(ruleDef);
const proctor = new KMWRecorder.NodeProctor(keyboard, device, assert.equal);
const target = new com.keyman.text.Mock();
const ruleSeq = new RecordedKeystrokeSequence(ruleDef);
const proctor = new NodeProctor(keyboard, device, assert.equal);
const target = new Mock();
ruleSeq.test(proctor, target);
}
}

View file

@ -1,11 +1,11 @@
var assert = require('chai').assert;
let fs = require('fs');
let vm = require('vm');
import { assert } from 'chai';
let KeyboardProcessor = require('../../../build/index.bundled.js');
import Keyboard from '@keymanapp/keyboard-processor/build/obj/keyboards/keyboard.js';
import KeyboardProcessor from '@keymanapp/keyboard-processor/build/obj/text/keyboardProcessor.js';
// Required initialization setup.
global.com = KeyboardProcessor.com; // exports all keyboard-processor namespacing.
import extendString from '@keymanapp/web-utils/build/obj/kmwstring.js'
extendString();
let device = {
formFactor: 'desktop',
@ -25,7 +25,7 @@ describe('Engine - Stores', function() {
let processor = new KeyboardProcessor(device);
// A 'hollow' Keyboard that only follows default rules. That said, we need a Keyboard
// instance to host cache data for our exploded store tests.
processor.activeKeyboard = new com.keyman.keyboards.Keyboard();
processor.activeKeyboard = new Keyboard();
// Function defined at top of file; creates supplementary pairs for extended Unicode codepoints.
var u = toSupplementaryPairString;

View file

@ -1,18 +1,16 @@
var assert = require('chai').assert;
let fs = require('fs');
let vm = require('vm');
import { assert } from 'chai';
import fs from 'fs';
import vm from 'vm';
let KeyboardProcessor = require('../../../build/index.bundled.js');
import KeyboardProcessor from '@keymanapp/keyboard-processor/build/obj/text/keyboardProcessor.js';
// Required initialization setup.
global.com = KeyboardProcessor.com; // exports all keyboard-processor namespacing.
let KMWRecorder = require('../../../../recorder/build/nodeProctor');
import { KeyboardTest } from '@keymanapp/recorder-core/build/obj/index.js';
import NodeProctor from '@keymanapp/recorder-core/build/obj/nodeProctor.js';
describe('Engine - Unmatched Final Groups', function() {
let testJSONtext = fs.readFileSync('../../test/resources/json/engine_tests/ghp_enter.json');
// Common test suite setup.
let testSuite = new KMWRecorder.KeyboardTest(JSON.parse(testJSONtext));
let testSuite = new KeyboardTest(JSON.parse(testJSONtext));
var keyboard;
let device = {
@ -40,7 +38,7 @@ describe('Engine - Unmatched Final Groups', function() {
});
it('Emits default enter AND matches rule from early group', function() {
let proctor = new KMWRecorder.NodeProctor(keyboard, device, assert.equal);
let proctor = new NodeProctor(keyboard, device, assert.equal);
testSuite.test(proctor);
});
});

View file

@ -1,8 +1,9 @@
var assert = require('chai').assert;
let KeyboardProcessor = require('../../build/index.bundled.js');
import { assert } from 'chai';
// Required initialization setup.
global.com = KeyboardProcessor.com; // exports all keyboard-processor namespacing.
import { Mock } from '@keymanapp/keyboard-processor/build/obj/text/outputTarget.js';
import extendString from '@keymanapp/web-utils/build/obj/kmwstring.js'
extendString(); // Ensure KMW's string-extension functionality is available.
String.kmwEnableSupplementaryPlane(false);
@ -19,8 +20,6 @@ describe("Transcriptions and Transforms", function() {
let smpApple = u(0x1d5ba)+u(0x1d5c9)+u(0x1d5c9)+u(0x1d5c5)+u(0x1d5be);
it("does not store an alias for related OutputTargets", function() {
var Mock = com.keyman.text.Mock;
// We have other texts validating Mocks; by using them as our base 'element', this unit test file
// could eventually run in 'headless' mode.
var target = new Mock("apple");
@ -38,8 +37,6 @@ describe("Transcriptions and Transforms", function() {
describe("Plain text operations", function() {
it("handles context-free single-char output rules", function() {
var Mock = com.keyman.text.Mock;
// We have other texts validating Mocks; by using them as our base 'element', this unit test file
// could eventually run in 'headless' mode.
var target = new Mock("apple");
@ -67,8 +64,6 @@ describe("Transcriptions and Transforms", function() {
});
it("handles operations with moderately long text", function() {
var Mock = com.keyman.text.Mock;
var target = new Mock("The quick brown cat jumped onto the lazy dog.", 19);
var original = Mock.from(target);
target.setDeadkeyCaret(30); // 19 + 11: moves it to after "onto".
@ -86,8 +81,6 @@ describe("Transcriptions and Transforms", function() {
});
it("handles operations with long text", function() {
var Mock = com.keyman.text.Mock;
// Eh... had to pick SOMETHING.
let text = `Did you ever hear the Tragedy of Darth Plagueis the wise? I thought not.
It's not a story the Jedi would tell you. It's a Sith legend. Darth Plagueis was a
@ -116,8 +109,6 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels.
});
it("handles deletions around the caret without text insertion", function() {
var Mock = com.keyman.text.Mock;
var target = new Mock("apple", 2);
var original = Mock.from(target);
target.setDeadkeyCaret(3);
@ -136,8 +127,6 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels.
it("handles deletions around the caret without text insertion (SMP text)", function() {
try {
String.kmwEnableSupplementaryPlane(true);
var Mock = com.keyman.text.Mock;
var target = new Mock(smpApple, 2);
var original = Mock.from(target);
target.setDeadkeyCaret(3);
@ -157,8 +146,6 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels.
});
it("handles deletions around the caret with text insertion", function() {
var Mock = com.keyman.text.Mock;
// We have other texts validating Mocks; by using them as our base 'element', this unit test file
// could eventually run in 'headless' mode.
var target = new Mock("apple", 2);
@ -232,7 +219,6 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels.
it("handles deletions around the caret with text insertion (SMP text)", function() {
try {
String.kmwEnableSupplementaryPlane(true);
var Mock = com.keyman.text.Mock;
// We have other texts validating Mocks; by using them as our base 'element', this unit test file
// could eventually run in 'headless' mode.
@ -316,7 +302,6 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels.
/*describe("Operations with deadkeys", function() {
// Just one, less nuanced/subdivided; it's not a present priority for our work, but it should provide a decent basis if/when it's needed.
it("Correctly recognizes deadkey set mutations", function() {
var Mock = com.keyman.text.Mock;
// We have other texts validating Mocks; by using them as our base 'element', this unit test file
// could eventually run in 'headless' mode.

View file

@ -1,33 +1,31 @@
var assert = require('chai').assert;
let KeyboardProcessor = require('../../build/index.bundled.js');
import { assert } from 'chai';
// Required initialization setup.
global.com = KeyboardProcessor.com; // exports all keyboard-processor namespacing.
import Version from '@keymanapp/web-utils/build/obj/version.js';
describe('Version Logic', function() {
it('Should provide a default, fallback value when nothing is specified', function() {
var fallback = new com.keyman.utils.Version(undefined);
assert.isTrue(fallback.equals(com.keyman.utils.Version.DEVELOPER_VERSION_FALLBACK));
var fallback = new Version(undefined);
assert.isTrue(fallback.equals(Version.DEVELOPER_VERSION_FALLBACK));
});
it('Should properly process a simple major.minor version string.', function() {
var version = new com.keyman.utils.Version("1.2");
var version = new Version("1.2");
assert.equal(version.major, 1);
assert.equal(version.minor, 2);
});
it('Should handle long/deep version specifications.', function() {
var version = new com.keyman.utils.Version("1.2.3.4.5.6");
var version = new Version("1.2.3.4.5.6");
assert.equal(version.components.length, 6);
assert.equal(version.major, 1);
assert.equal(version.minor, 2);
});
it('Should properly compare two versions.', function() {
var v9_0_1 = new com.keyman.utils.Version("9.0.1");
var v9_1_0 = new com.keyman.utils.Version("9.1.0");
var v10_0 = new com.keyman.utils.Version("10.0");
var v10_0_0 = new com.keyman.utils.Version("10.0.0");
var v9_0_1 = new Version("9.0.1");
var v9_1_0 = new Version("9.1.0");
var v10_0 = new Version("10.0");
var v10_0_0 = new Version("10.0.0");
// "Precede" checks
assert.equal(v9_0_1.compareTo(v9_1_0), -1);

View file

@ -0,0 +1,26 @@
{
"extends": "../../../tsconfig-base.json",
"compilerOptions": {
"allowJs": true,
"module": "es6",
"moduleResolution": "Node",
"declaration": true,
"inlineSources": true,
"sourceMap": true,
"sourceRoot": "keyman/",
"target": "es5",
"types": ["node"],
"lib": ["es6"],
"experimentalDecorators": true,
"baseUrl": "./",
"outDir": "build/obj/",
"tsBuildInfoFile": "build/obj/tsconfig.tsbuildinfo",
"rootDir": "./src"
},
"references": [
{ "path": "../../models/types" },
{ "path": "../keyman-version/" },
{ "path": "../utils/" }
],
"include": ["./src/**/*.ts"]
}

View file

@ -22,16 +22,11 @@ builder_describe \
"@../keyman-version" \
configure \
clean \
build \
":module Builds recorder-core module" \
":proctor Builds headless-testing, node-oriented 'proctor' component"
build
builder_describe_outputs \
configure "/node_modules" \
configure:module "/node_modules" \
configure:proctor "/node_modules" \
build:module "build/index.js" \
build:proctor "build/nodeProctor/index.js"
configure "/node_modules" \
build "build/index.js"
builder_parse "$@"
@ -40,22 +35,12 @@ if builder_start_action configure; then
builder_finish_action success configure
fi
if builder_start_action clean:module; then
npm run tsc -- -b --clean "$THIS_SCRIPT_PATH/src/tsconfig.json"
builder_finish_action success clean:module
if builder_start_action clean; then
npm run tsc -- -b --clean "$THIS_SCRIPT_PATH/tsconfig.json"
builder_finish_action success clean
fi
if builder_start_action clean:proctor; then
npm run tsc -- -b --clean "$THIS_SCRIPT_PATH/src/nodeProctor.tsconfig.json"
builder_finish_action success clean:proctor
fi
if builder_start_action build:module; then
npm run tsc -- --build "$THIS_SCRIPT_PATH/src/tsconfig.json"
builder_finish_action success build:module
fi
if builder_start_action build:proctor; then
npm run tsc -- --build "$THIS_SCRIPT_PATH/src/nodeProctor.tsconfig.json"
builder_finish_action success build:proctor
if builder_start_action build; then
npm run tsc -- --build "$THIS_SCRIPT_PATH/tsconfig.json"
builder_finish_action success build
fi

View file

@ -2,6 +2,7 @@
"name": "@keymanapp/recorder-core",
"description": "Core classes used to develop KeymanWeb test cases based on keystrokes",
"main": "index.js",
"type": "module",
"scripts": {
"tsc": "tsc",
"clean": "tsc -b --clean src/tsconfig.json && tsc -b --clean src/nodeProctor.tsconfig.json"

File diff suppressed because it is too large Load diff

View file

@ -1,94 +1,101 @@
import Proctor, { AssertCallback } from "./proctor.js";
import {
KeyboardTest,
TestSet,
TestSequence,
RecordedKeystrokeSequence,
RecordedPhysicalKeystroke,
RecordedSyntheticKeystroke
} from "./index.js";
namespace KMWRecorder {
export class NodeProctor extends Proctor {
private keyboard: com.keyman.keyboards.Keyboard;
public __debug = false;
import { Keyboard, type KeyEvent, KeyboardProcessor, Mock, type OutputTarget } from "@keymanapp/keyboard-processor/build/obj/index.js";
constructor(keyboard: com.keyman.keyboards.Keyboard, device: com.keyman.utils.DeviceSpec, assert: AssertCallback) {
super(device, assert);
import { DeviceSpec } from "@keymanapp/web-utils/build/obj/index.js";
this.keyboard = keyboard;
}
export default class NodeProctor extends Proctor {
private keyboard: Keyboard;
public __debug = false;
beforeAll() {
//
}
before() {
//
}
compatibleWithSuite(testSuite: KeyboardTest): boolean {
// Original-version tests did not supply core-compatible KeyEvent data.
return !testSuite.specVersion.equals(KeyboardTest.FALLBACK_VERSION);
}
get debugMode(): boolean {
return this.__debug;
}
set debugMode(value: boolean) {
this.__debug = value;
}
matchesTestSet(testSet: TestSet<any>) {
// KeyboardProcessor is abstract enough to run tests aimed at any platform.
return true;
}
simulateSequence(sequence: TestSequence<any>, target?: com.keyman.text.OutputTarget): string {
// Start with an empty OutputTarget and a fresh KeyboardProcessor.
if(!target) {
target = new com.keyman.text.Mock();
}
// Establish a fresh processor, setting its keyboard appropriately for the test.
let processor = new com.keyman.text.KeyboardProcessor(this.device);
processor.activeKeyboard = this.keyboard;
if(sequence instanceof RecordedKeystrokeSequence) {
for(let keystroke of sequence.inputs) {
let keyEvent: com.keyman.text.KeyEvent;
if(keystroke instanceof RecordedPhysicalKeystroke) {
// Use the keystroke's stored data to reconstruct the KeyEvent.
keyEvent = {
Lcode: keystroke.keyCode,
Lmodifiers: keystroke.modifiers,
LmodifierChange: keystroke.modifierChanged,
vkCode: keystroke.vkCode,
Lstates: keystroke.states,
kName: '',
device: this.device,
isSynthetic: false,
LisVirtualKey: this.keyboard.definesPositionalOrMnemonic // Only false for 1.0 keyboards.
}
} else if(keystroke instanceof RecordedSyntheticKeystroke) {
let key = this.keyboard.layout(this.device.formFactor).getLayer(keystroke.layer).getKey(keystroke.keyName);
keyEvent = key.constructKeyEvent(processor, this.device);
}
// Fill in the final details of the KeyEvent...
keyEvent.device = this.device;
// And now, execute the keystroke!
// We don't care too much about particularities of per-keystroke behavior yet.
// ... we _could_ if we wanted to, though. The framework is mostly in place;
// it's a matter of actually adding the feature.
let ruleBehavior = processor.processKeystroke(keyEvent, target);
if(this.debugMode) {
console.log(JSON.stringify(target, null, ' '));
console.log(JSON.stringify(ruleBehavior, null, ' '));
}
}
} else {
throw new Error("NodeProctor only supports RecordedKeystrokeSequences for testing at present.");
}
return target.getText();
}
constructor(keyboard: Keyboard, device: DeviceSpec, assert: AssertCallback) {
super(device, assert);
this.keyboard = keyboard;
}
}
// Export the namespace itself, giving access to all contained classes.
module.exports = KMWRecorder;
beforeAll() {
//
}
before() {
//
}
compatibleWithSuite(testSuite: KeyboardTest): boolean {
// Original-version tests did not supply core-compatible KeyEvent data.
return !testSuite.specVersion.equals(KeyboardTest.FALLBACK_VERSION);
}
get debugMode(): boolean {
return this.__debug;
}
set debugMode(value: boolean) {
this.__debug = value;
}
matchesTestSet(testSet: TestSet<any>) {
// KeyboardProcessor is abstract enough to run tests aimed at any platform.
return true;
}
simulateSequence(sequence: TestSequence<any>, target?: OutputTarget): string {
// Start with an empty OutputTarget and a fresh KeyboardProcessor.
if(!target) {
target = new Mock();
}
// Establish a fresh processor, setting its keyboard appropriately for the test.
let processor = new KeyboardProcessor(this.device);
processor.activeKeyboard = this.keyboard;
if(sequence instanceof RecordedKeystrokeSequence) {
for(let keystroke of sequence.inputs) {
let keyEvent: KeyEvent;
if(keystroke instanceof RecordedPhysicalKeystroke) {
// Use the keystroke's stored data to reconstruct the KeyEvent.
keyEvent = {
Lcode: keystroke.keyCode,
Lmodifiers: keystroke.modifiers,
LmodifierChange: keystroke.modifierChanged,
vkCode: keystroke.vkCode,
Lstates: keystroke.states,
kName: '',
device: this.device,
isSynthetic: false,
LisVirtualKey: this.keyboard.definesPositionalOrMnemonic // Only false for 1.0 keyboards.
}
} else if(keystroke instanceof RecordedSyntheticKeystroke) {
let key = this.keyboard.layout(this.device.formFactor).getLayer(keystroke.layer).getKey(keystroke.keyName);
keyEvent = key.constructKeyEvent(processor, this.device);
}
// Fill in the final details of the KeyEvent...
keyEvent.device = this.device;
// And now, execute the keystroke!
// We don't care too much about particularities of per-keystroke behavior yet.
// ... we _could_ if we wanted to, though. The framework is mostly in place;
// it's a matter of actually adding the feature.
let ruleBehavior = processor.processKeystroke(keyEvent, target);
if(this.debugMode) {
console.log(JSON.stringify(target, null, ' '));
console.log(JSON.stringify(ruleBehavior, null, ' '));
}
}
} else {
throw new Error("NodeProctor only supports RecordedKeystrokeSequences for testing at present.");
}
return target.getText();
}
}

View file

@ -1,28 +0,0 @@
{
"extends": "../../../../tsconfig-base.json",
"compilerOptions": {
"allowJs": true,
"module": "none",
"outDir": "../build/nodeProctor/",
"outFile": "../build/nodeProctor/index.js",
"inlineSources": true,
"inlineSourceMap": true,
"target": "es5",
"types": ["node"],
"lib": ["es6"]
},
"files": [
"index.ts",
"proctor.ts",
"nodeProctor.ts"
],
"references": [
{ "path": "../../keyman-version" },
{ "path": "../../utils" },
{ "path": "../../keyboard-processor/src" },
{ "path": "../../lm-message-types" }
]
}

View file

@ -1,50 +1,53 @@
namespace KMWRecorder {
export type AssertCallback = (s1: any, s2: any, msg?: string) => void;
import { type DeviceSpec } from "@keymanapp/web-utils/build/obj/index.js";
import { type OutputTarget } from "@keymanapp/keyboard-processor/build/obj/index.js";
import type { KeyboardTest, TestSet, TestSequence } from "./index.js";
export type AssertCallback = (s1: any, s2: any, msg?: string) => void;
/**
* Facilitates running Recorder-generated tests on various platforms.
*
* Note that DOM-aware KeymanWeb will implement a Browser-based version, while
* keyboard-processor and input-processor will use a Node-based version instead.
*/
export default abstract class Proctor {
device: DeviceSpec;
_assert: AssertCallback;
constructor(device: DeviceSpec, assert: AssertCallback) {
this.device = device;
this._assert = assert;
}
assertEquals(s1: unknown, s2: unknown, msg?: string) {
if(this._assert) {
this._assert(s1, s2, msg);
}
}
// Performs global test prep.
abstract beforeAll();
// Performs per-test setup
abstract before();
/**
* Facilitates running Recorder-generated tests on various platforms.
*
* Note that DOM-aware KeymanWeb will implement a Browser-based version, while
* keyboard-processor and input-processor will use a Node-based version instead.
* Allows the proctor to indicate if is capable of executing a suite of tests or not.
* @param testSuite
*/
export abstract class Proctor {
device: com.keyman.utils.DeviceSpec;
abstract compatibleWithSuite(testSuite: KeyboardTest): boolean;
_assert: AssertCallback;
/**
* Indicates whether or not this Proctor is capable of running the specified set of tests.
*/
abstract matchesTestSet(testSet: TestSet<any>);
constructor(device: com.keyman.utils.DeviceSpec, assert: AssertCallback) {
this.device = device;
this._assert = assert;
}
assertEquals(s1: unknown, s2: unknown, msg?: string) {
if(this._assert) {
this._assert(s1, s2, msg);
}
}
// Performs global test prep.
abstract beforeAll();
// Performs per-test setup
abstract before();
/**
* Allows the proctor to indicate if is capable of executing a suite of tests or not.
* @param testSuite
*/
abstract compatibleWithSuite(testSuite: KeyboardTest): boolean;
/**
* Indicates whether or not this Proctor is capable of running the specified set of tests.
*/
abstract matchesTestSet(testSet: TestSet<any>);
/**
* Simulates the specified test sequence for use in testing.
* @param sequence The recorded sequence, generally provided by a test set.
*/
abstract simulateSequence(sequence: TestSequence<any>, target?: com.keyman.text.OutputTarget);
}
/**
* Simulates the specified test sequence for use in testing.
* @param sequence The recorded sequence, generally provided by a test set.
*/
abstract simulateSequence(sequence: TestSequence<any>, target?: OutputTarget);
}

View file

@ -1,27 +0,0 @@
{
"extends": "../../../../tsconfig-base.json",
"compilerOptions": {
"allowJs": true,
"module": "none",
"outDir": "../build/",
"inlineSources": true,
"inlineSourceMap": true,
"target": "es5",
"types": ["node"],
"lib": ["es6"],
"outFile": "../build/index.js"
},
"files": [
"index.ts",
"proctor.ts"
],
"references": [
{ "path": "../../keyman-version" },
{ "path": "../../utils" },
{ "path": "../../keyboard-processor/src" },
{ "path": "../../lm-message-types" }
]
}

View file

@ -0,0 +1,30 @@
{
"extends": "../../../tsconfig-base.json",
"compilerOptions": {
"allowJs": true,
"module": "es6",
"moduleResolution": "Node",
"declaration": true,
"inlineSources": true,
"inlineSourceMap": true,
"target": "es5",
"types": ["node"],
"lib": ["es6"],
"baseUrl": "./",
"outDir": "build/obj/",
"tsBuildInfoFile": "build/obj/tsconfig.tsbuildinfo",
"rootDir": "./src"
},
"include": [
"src/**/*.ts"
],
"references": [
{ "path": "../keyman-version" },
{ "path": "../utils/" },
{ "path": "../keyboard-processor/" },
{ "path": "../lm-message-types" }
],
}

View file

@ -15,7 +15,7 @@
"@keymanapp/models-types": ["./common/models/types"],
"@keymanapp/models-templates": ["./common/models/templates"],
"@keymanapp/models-wordbreakers": ["./common/models/wordbreakers"],
"@keymanapp/utils": ["./common/web/utils"],
"@keymanapp/web-utils": ["./common/web/utils"],
"@keymanapp/lm-message-types": ["./common/web/lm-message-types"],
"@keymanapp/keyman-version": ["./common/web/keyman-version"],
}