From 991fb8ad05a6bc0728e58003a1a33a3d20575dae Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 24 Nov 2022 10:41:02 +0700 Subject: [PATCH 01/23] chore(web): base esmodule conversion of keyboard-processor --- common/web/keyboard-processor/build.sh | 2 +- common/web/keyboard-processor/package.json | 3 +- .../src/keyboards/activeLayout.ts | 1406 +++++------ .../src/keyboards/defaultLayouts.ts | 1618 ++++++------ .../src/keyboards/keyboard.ts | 818 +++--- .../web/keyboard-processor/src/text/codes.ts | 190 +- .../keyboard-processor/src/text/deadkeys.ts | 285 ++- .../src/text/defaultOutput.ts | 365 +-- .../src/text/kbdInterface.ts | 2230 +++++++++-------- .../keyboard-processor/src/text/keyEvent.ts | 97 +- .../keyboard-processor/src/text/keyMapping.ts | 342 +-- .../src/text/keyboardProcessor.ts | 1415 ++++++----- .../src/text/outputTarget.ts | 825 +++--- .../src/text/ruleBehavior.ts | 222 +- .../src/text/systemStores.ts | 229 +- .../web/keyboard-processor/src/tsconfig.json | 24 - common/web/keyboard-processor/tsconfig.json | 29 + 17 files changed, 5055 insertions(+), 5045 deletions(-) delete mode 100644 common/web/keyboard-processor/src/tsconfig.json create mode 100644 common/web/keyboard-processor/tsconfig.json diff --git a/common/web/keyboard-processor/build.sh b/common/web/keyboard-processor/build.sh index 86c02f9806..e93e8b64fa 100755 --- a/common/web/keyboard-processor/build.sh +++ b/common/web/keyboard-processor/build.sh @@ -49,7 +49,7 @@ 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" builder_finish_action success build fi diff --git a/common/web/keyboard-processor/package.json b/common/web/keyboard-processor/package.json index 437f358dba..47c5851fe6 100644 --- a/common/web/keyboard-processor/package.json +++ b/common/web/keyboard-processor/package.json @@ -35,5 +35,6 @@ "@keymanapp/keyman-version": "*", "@keymanapp/web-utils": "*", "@types/node": "^11.9.4" - } + }, + "type": "module" } diff --git a/common/web/keyboard-processor/src/keyboards/activeLayout.ts b/common/web/keyboard-processor/src/keyboards/activeLayout.ts index 398682becd..49f2104375 100644 --- a/common/web/keyboard-processor/src/keyboards/activeLayout.ts +++ b/common/web/keyboard-processor/src/keyboards/activeLayout.ts @@ -1,782 +1,758 @@ -namespace com.keyman.keyboards { - type KeyDistribution = text.KeyDistribution; +import Codes from "../text/codes.js"; +import type KeyEvent from "../text/keyEvent.js"; +import KeyMapping from "../text/keyMapping.js"; +import type { KeyDistribution } from "../text/keyEvent.js"; +import type { LayoutKey, LayoutRow, LayoutLayer, LayoutFormFactor, ButtonClass } from "./defaultLayouts.js"; +import type Keyboard from "./keyboard.js"; - // TS 3.9 changed behavior of getters to make them - // non-enumerable by default. This broke our 'polyfill' - // functions which depended on enumeration to copy the - // relevant props over. - // https://github.com/microsoft/TypeScript/pull/32264#issuecomment-677718191 - function Enumerable( - target: unknown, - propertyKey: string, - descriptor: PropertyDescriptor - ) { - descriptor.enumerable = true; +import { KeyboardProcessor } from "text/keyboardProcessor.js"; + +import { deepCopy, type DeviceSpec } from "utils/build/modules/index.js"; + +// TS 3.9 changed behavior of getters to make them +// non-enumerable by default. This broke our 'polyfill' +// functions which depended on enumeration to copy the +// relevant props over. +// https://github.com/microsoft/TypeScript/pull/32264#issuecomment-677718191 +function Enumerable( + target: unknown, + propertyKey: string, + descriptor: PropertyDescriptor +) { + descriptor.enumerable = true; +}; + +export class ActiveKey implements LayoutKey { + static readonly DEFAULT_PAD=15; // Padding to left of key, in virtual units + static readonly DEFAULT_RIGHT_MARGIN=15; // Padding to right of right-most key, in virtual units + static readonly DEFAULT_KEY_WIDTH=100; // Width of a key, if not specified, in virtual units + + // Defines key defaults + static readonly DEFAULT_KEY = { + text: '', + width: ActiveKey.DEFAULT_KEY_WIDTH, + sp: 0, + pad: ActiveKey.DEFAULT_PAD }; - export class ActiveKey implements LayoutKey { - static readonly DEFAULT_PAD=15; // Padding to left of key, in virtual units - static readonly DEFAULT_RIGHT_MARGIN=15; // Padding to right of right-most key, in virtual units - static readonly DEFAULT_KEY_WIDTH=100; // Width of a key, if not specified, in virtual units + /** WARNING - DO NOT USE DIRECTLY outside of @keymanapp/keyboard-processor! */ + id?: string; - // Defines key defaults - static readonly DEFAULT_KEY = { - text: '', - width: ActiveKey.DEFAULT_KEY_WIDTH, - sp: 0, - pad: ActiveKey.DEFAULT_PAD + // These are fine. + width?: number; + pad?: number; + + layer: string; + displayLayer: string; + nextlayer: string; + sp?: ButtonClass; + + private baseKeyEvent: KeyEvent; + isMnemonic: boolean = false; + + proportionalPad: number; + proportionalX: number; + proportionalWidth: number; + + sk?: ActiveKey[]; + + // Keeping things simple here, as this was added LATE in 14.0 beta. + // Could definitely extend in the future to instead return an object + // that denotes the 'nature' of the key. + // - isUnicode + // - isHardwareKey + // - etc. + + // Reference for the terminology in the comments below: + // https://help.keyman.com/developer/current-version/guides/develop/creating-a-touch-keyboard-layout-for-amharic-the-nitty-gritty + + /** + * Matches the key code as set within Keyman Developer for the layout. + * For example, K_R or U_0020. Denotes either physical keys or virtual keys with custom output, + * with no additional metadata like layer or active modifiers. + * + * Is used to determine the keycode for input events, rule-matching, and keystroke processing. + */ + @Enumerable + public get baseKeyID(): string { + if(typeof this.id === 'undefined') { + return undefined; + } + + return this.id; + } + + @Enumerable + public get isPadding(): boolean { + // Does not include 9 (class: blank) as that may be an intentional 'catch' for misplaced + // keystrokes. + return this['sp'] == 10; // Button class: hidden. + } + + /** + * A unique identifier based on both the key ID & the 'desktop layer' to be used for the key. + * + * Allows diambiguation of scenarios where the same key ID is used twice within a layer, but + * with different innate modifiers. (Refer to https://github.com/keymanapp/keyman/issues/4617) + * The 'desktop layer' may be omitted if it matches the key's display layer. + * + * Examples, given a 'default' display layer, matching keys to Keyman keyboard language: + * + * ``` + * "K_Q" + * + [K_Q] + * "K_Q+shift" + * + [K_Q SHIFT] + * ``` + * + * Useful when the active layer of an input-event is already known. + */ + @Enumerable + public get coreID(): string { + if(typeof this.id === 'undefined') { + return undefined; + } + + let baseID = this.id || ''; + + if(this.displayLayer != this.layer) { + baseID = baseID + '+' + this.layer; + } + + return baseID; + } + + /** + * A keyboard-unique identifier to be used for any display elements representing this key + * in user interfaces and/or on-screen keyboards. + * + * Distinguishes between otherwise-identical keys on different layers of an OSK. + * Includes identifying information about the key's display layer. + * + * Examples, given a 'default' display layer, matching keys to Keyman keyboard language: + * + * ``` + * "default-K_Q" + * + [K_Q] + * "default-K_Q+shift" + * + [K_Q SHIFT] + * ``` + * + * Useful when only the active keyboard is known about an input event. + */ + @Enumerable + public get elementID(): string { + if(typeof this.id === 'undefined') { + return undefined; + } + + return this.displayLayer + '-' + this.coreID; + } + + static sanitize(rawKey: LayoutKey) { + if(typeof rawKey.width == 'string') { + rawKey.width = parseInt(rawKey.width, 10); + } + // Handles NaN cases as well as 'set to 0' cases; both are intentional here. + rawKey.width ||= ActiveKey.DEFAULT_KEY_WIDTH; + + if(typeof rawKey.pad == 'string') { + rawKey.pad = parseInt(rawKey.pad, 10); + } + rawKey.pad ||= ActiveKey.DEFAULT_PAD; + + if(typeof rawKey.sp == 'string') { + rawKey.sp = Number.parseInt(rawKey.sp, 10) as ButtonClass; + } + rawKey.sp ||= 0; // The default button class. + } + + static polyfill(key: LayoutKey, layout: ActiveLayout, displayLayer: string) { + // Add class functions to the existing layout object, allowing it to act as an ActiveLayout. + let dummy = new ActiveKey(); + let proto = Object.getPrototypeOf(dummy); + + for(let prop in dummy) { + if(!key.hasOwnProperty(prop)) { + let descriptor = Object.getOwnPropertyDescriptor(proto, prop); + if(descriptor) { + // It's a computed property! Copy the descriptor onto the key's object. + Object.defineProperty(key, prop, descriptor); + } else { + key[prop] = dummy[prop]; + } + } + } + + // Ensure subkeys are also properly extended. + if(key.sk) { + for(let subkey of key.sk) { + ActiveKey.polyfill(subkey, layout, displayLayer); + } + } + + let aKey = key as ActiveKey; + aKey.displayLayer = displayLayer; + aKey.layer = aKey.layer || displayLayer; + + // Compute the key's base KeyEvent properties for use in future event generation + aKey.constructBaseKeyEvent(layout, displayLayer); + } + + private constructBaseKeyEvent(layout: ActiveLayout, displayLayer: string) { + // Get key name and keyboard shift state (needed only for default layouts and physical keyboard handling) + // Note - virtual keys should be treated case-insensitive, so we force uppercasing here. + let layer = this.layer || displayLayer || ''; + let keyName= this.id ? this.id.toUpperCase() : null; + + // Start: mirrors _GetKeyEventProperties + + + // First check the virtual key, and process shift, control, alt or function keys + var Lkc: KeyEvent = { + // Override key shift state if specified for key in layout (corrected for popup keys KMEW-93) + Lmodifiers: KeyboardProcessor.getModifierState(layer), + Lstates: KeyboardProcessor.getStateFromLayer(layer), + Lcode: keyName ? Codes.keyCodes[keyName] : 0, + LisVirtualKey: true, + vkCode: 0, + kName: keyName, + kLayer: layer, + kbdLayer: displayLayer, + kNextLayer: this.nextlayer, + device: null, + isSynthetic: true }; - /** WARNING - DO NOT USE DIRECTLY outside of @keymanapp/keyboard-processor! */ - id?: string; + if(layout.keyboard) { + let keyboard = layout.keyboard; - // These are fine. - width?: number; - pad?: number; - - layer: string; - displayLayer: string; - nextlayer: string; - sp?: ButtonClass; - - private baseKeyEvent: text.KeyEvent; - isMnemonic: boolean = false; - - proportionalPad: number; - proportionalX: number; - proportionalWidth: number; - - sk?: ActiveKey[]; - - // Keeping things simple here, as this was added LATE in 14.0 beta. - // Could definitely extend in the future to instead return an object - // that denotes the 'nature' of the key. - // - isUnicode - // - isHardwareKey - // - etc. - - // Reference for the terminology in the comments below: - // https://help.keyman.com/developer/current-version/guides/develop/creating-a-touch-keyboard-layout-for-amharic-the-nitty-gritty - - /** - * Matches the key code as set within Keyman Developer for the layout. - * For example, K_R or U_0020. Denotes either physical keys or virtual keys with custom output, - * with no additional metadata like layer or active modifiers. - * - * Is used to determine the keycode for input events, rule-matching, and keystroke processing. - */ - @Enumerable - public get baseKeyID(): string { - if(typeof this.id === 'undefined') { - return undefined; - } - - return this.id; - } - - @Enumerable - public get isPadding(): boolean { - // Does not include 9 (class: blank) as that may be an intentional 'catch' for misplaced - // keystrokes. - return this['sp'] == 10; // Button class: hidden. - } - - /** - * A unique identifier based on both the key ID & the 'desktop layer' to be used for the key. - * - * Allows diambiguation of scenarios where the same key ID is used twice within a layer, but - * with different innate modifiers. (Refer to https://github.com/keymanapp/keyman/issues/4617) - * The 'desktop layer' may be omitted if it matches the key's display layer. - * - * Examples, given a 'default' display layer, matching keys to Keyman keyboard language: - * - * ``` - * "K_Q" - * + [K_Q] - * "K_Q+shift" - * + [K_Q SHIFT] - * ``` - * - * Useful when the active layer of an input-event is already known. - */ - @Enumerable - public get coreID(): string { - if(typeof this.id === 'undefined') { - return undefined; - } - - let baseID = this.id || ''; - - if(this.displayLayer != this.layer) { - baseID = baseID + '+' + this.layer; - } - - return baseID; - } - - /** - * A keyboard-unique identifier to be used for any display elements representing this key - * in user interfaces and/or on-screen keyboards. - * - * Distinguishes between otherwise-identical keys on different layers of an OSK. - * Includes identifying information about the key's display layer. - * - * Examples, given a 'default' display layer, matching keys to Keyman keyboard language: - * - * ``` - * "default-K_Q" - * + [K_Q] - * "default-K_Q+shift" - * + [K_Q SHIFT] - * ``` - * - * Useful when only the active keyboard is known about an input event. - */ - @Enumerable - public get elementID(): string { - if(typeof this.id === 'undefined') { - return undefined; - } - - return this.displayLayer + '-' + this.coreID; - } - - /** - * Converts key IDs of the U_* form to their corresponding UTF-16 text. - * If an ID not matching the pattern is received, returns null. - * @param id - * @returns - */ - static unicodeIDToText(id: string, errorCallback?: (codeAsString: string) => void) { - if(!id || id.substring(0,2) != 'U_') { - return null; - } - - let result = ''; - const codePoints = id.substring(2).split('_'); - for(let codePoint of codePoints) { - const codePointValue = parseInt(codePoint, 16); - if (((0x0 <= codePointValue) && (codePointValue <= 0x1F)) || - ((0x80 <= codePointValue) && (codePointValue <= 0x9F)) || - isNaN(codePointValue)) { - if(errorCallback) { - errorCallback(codePoint); - } - 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); + // Include *limited* support for mnemonic keyboards (Sept 2012) + // If a touch layout has been defined for a mnemonic keyout, do not perform mnemonic mapping for rules on touch devices. + if(keyboard.isMnemonic && !(layout.isDefault && layout.formFactor != 'desktop')) { + if(Lkc.Lcode != Codes.keyCodes['K_SPACE']) { // exception required, March 2013 + // Jan 2019 - interesting that 'K_SPACE' also affects the caps-state check... + Lkc.vkCode = Lkc.Lcode; + this.isMnemonic = true; } + } else { + Lkc.vkCode=Lkc.Lcode; + } + + // Support version 1.0 KeymanWeb keyboards that do not define positional vs mnemonic + if(!keyboard.definesPositionalOrMnemonic) { + // Not the best pattern, but currently safe - we don't look up any properties of any of the + // arguments in this use case, and the object's scope is extremely limited. + Lkc.Lcode = KeyMapping._USKeyCodeToCharCode(this.constructKeyEvent(null, null)); + Lkc.LisVirtualKey=false; } - return result ? result : null; } - static sanitize(rawKey: LayoutKey) { - if(typeof rawKey.width == 'string') { - rawKey.width = parseInt(rawKey.width, 10); - } - // Handles NaN cases as well as 'set to 0' cases; both are intentional here. - rawKey.width ||= ActiveKey.DEFAULT_KEY_WIDTH; + this.baseKeyEvent = Lkc; + } - if(typeof rawKey.pad == 'string') { - rawKey.pad = parseInt(rawKey.pad, 10); - } - rawKey.pad ||= ActiveKey.DEFAULT_PAD; + constructKeyEvent(keyboardProcessor: KeyboardProcessor, device: DeviceSpec): KeyEvent { + // Make a deep copy of our preconstructed key event, filling it out from there. + let Lkc = deepCopy(this.baseKeyEvent); + Lkc.device = device; - if(typeof rawKey.sp == 'string') { - rawKey.sp = Number.parseInt(rawKey.sp, 10) as ButtonClass; - } - rawKey.sp ||= 0; // The default button class. + if(this.isMnemonic) { + KeyboardProcessor.setMnemonicCode(Lkc, this.layer.indexOf('shift') != -1, keyboardProcessor ? keyboardProcessor.stateKeys['K_CAPS'] : false); } - static polyfill(key: LayoutKey, layout: ActiveLayout, displayLayer: string) { - // Add class functions to the existing layout object, allowing it to act as an ActiveLayout. - let dummy = new ActiveKey(); - let proto = Object.getPrototypeOf(dummy); + // Performs common pre-analysis for both 'native' and 'embedded' OSK key & subkey input events. + // This part depends on the keyboard processor's active state. + if(keyboardProcessor) { + keyboardProcessor.setSyntheticEventDefaults(Lkc); - for(let prop in dummy) { - if(!key.hasOwnProperty(prop)) { - let descriptor = Object.getOwnPropertyDescriptor(proto, prop); - if(descriptor) { - // It's a computed property! Copy the descriptor onto the key's object. - Object.defineProperty(key, prop, descriptor); - } else { - key[prop] = dummy[prop]; - } - } - } - - // Ensure subkeys are also properly extended. - if(key.sk) { - for(let subkey of key.sk) { - ActiveKey.polyfill(subkey, layout, displayLayer); - } - } - - let aKey = key as ActiveKey; - aKey.displayLayer = displayLayer; - aKey.layer = aKey.layer || displayLayer; - - // Compute the key's base KeyEvent properties for use in future event generation - aKey.constructBaseKeyEvent(layout, displayLayer); - } - - private constructBaseKeyEvent(layout: ActiveLayout, displayLayer: string) { - // Get key name and keyboard shift state (needed only for default layouts and physical keyboard handling) - // Note - virtual keys should be treated case-insensitive, so we force uppercasing here. - let layer = this.layer || displayLayer || ''; - let keyName= this.id ? this.id.toUpperCase() : null; - - // Start: mirrors _GetKeyEventProperties - - - // First check the virtual key, and process shift, control, alt or function keys - var Lkc: text.KeyEvent = { - // Override key shift state if specified for key in layout (corrected for popup keys KMEW-93) - Lmodifiers: text.KeyboardProcessor.getModifierState(layer), - Lstates: text.KeyboardProcessor.getStateFromLayer(layer), - Lcode: keyName ? text.Codes.keyCodes[keyName] : 0, - LisVirtualKey: true, - vkCode: 0, - kName: keyName, - kLayer: layer, - kbdLayer: displayLayer, - kNextLayer: this.nextlayer, - device: null, - isSynthetic: true + // If it's a state key modifier, trigger its effects as part of the + // keystroke. + const bitmap = { + 'K_CAPS': Codes.stateBitmasks.CAPS, + 'K_NUMLOCK': Codes.stateBitmasks.NUM_LOCK, + 'K_SCROLL': Codes.stateBitmasks.SCROLL_LOCK }; + const bitmask = bitmap[Lkc.kName]; - if(layout.keyboard) { - let keyboard = layout.keyboard; - - // Include *limited* support for mnemonic keyboards (Sept 2012) - // If a touch layout has been defined for a mnemonic keyout, do not perform mnemonic mapping for rules on touch devices. - if(keyboard.isMnemonic && !(layout.isDefault && layout.formFactor != 'desktop')) { - if(Lkc.Lcode != text.Codes.keyCodes['K_SPACE']) { // exception required, March 2013 - // Jan 2019 - interesting that 'K_SPACE' also affects the caps-state check... - Lkc.vkCode = Lkc.Lcode; - this.isMnemonic = true; - } - } else { - Lkc.vkCode=Lkc.Lcode; - } - - // Support version 1.0 KeymanWeb keyboards that do not define positional vs mnemonic - if(!keyboard.definesPositionalOrMnemonic) { - // Not the best pattern, but currently safe - we don't look up any properties of any of the - // arguments in this use case, and the object's scope is extremely limited. - Lkc.Lcode = KeyMapping._USKeyCodeToCharCode(this.constructKeyEvent(null, null)); - Lkc.LisVirtualKey=false; - } + if(bitmask) { + Lkc.Lstates ^= bitmask; + Lkc.LmodifierChange = true; } - - this.baseKeyEvent = Lkc; } - constructKeyEvent(keyboardProcessor: text.KeyboardProcessor, device: utils.DeviceSpec): text.KeyEvent { - // Make a deep copy of our preconstructed key event, filling it out from there. - let Lkc = utils.deepCopy(this.baseKeyEvent); - Lkc.device = device; - - if(this.isMnemonic) { - text.KeyboardProcessor.setMnemonicCode(Lkc, this.layer.indexOf('shift') != -1, keyboardProcessor ? keyboardProcessor.stateKeys['K_CAPS'] : false); - } - - // Performs common pre-analysis for both 'native' and 'embedded' OSK key & subkey input events. - // This part depends on the keyboard processor's active state. - if(keyboardProcessor) { - keyboardProcessor.setSyntheticEventDefaults(Lkc); - - // If it's a state key modifier, trigger its effects as part of the - // keystroke. - const bitmap = { - 'K_CAPS': text.Codes.stateBitmasks.CAPS, - 'K_NUMLOCK': text.Codes.stateBitmasks.NUM_LOCK, - 'K_SCROLL': text.Codes.stateBitmasks.SCROLL_LOCK - }; - const bitmask = bitmap[Lkc.kName]; - - if(bitmask) { - Lkc.Lstates ^= bitmask; - Lkc.LmodifierChange = true; - } - } - - return Lkc; - } - - public getSubkey(coreID: string): ActiveKey { - if(this.sk) { - for(let key of this.sk) { - if(key.coreID == coreID) { - return key; - } - } - } - - return null; - } + return Lkc; } - export class ActiveRow implements LayoutRow { - // Identify key labels (e.g. *Shift*) that require the special OSK font - static readonly SPECIAL_LABEL=/\*\w+\*/; - - id: number; - key: ActiveKey[]; - - /** - * Used for calculating fat-fingering offsets. - */ - proportionalY: number; - - private constructor() { - - } - - static sanitize(rawRow: LayoutRow) { - for(const key of rawRow.key) { - // Test for a trailing comma included in spec, added as null object by IE - // It has only ever appeared at the end of a row's spec. - if(key == null) { - rawRow.key.length = rawRow.key.length-1; - } else { - ActiveKey.sanitize(key); + public getSubkey(coreID: string): ActiveKey { + if(this.sk) { + for(let key of this.sk) { + if(key.coreID == coreID) { + return key; } } - - if(typeof rawRow.id == 'string') { - rawRow.id = Number.parseInt(rawRow.id, 10); - } } - static polyfill(row: LayoutRow, layout: ActiveLayout, displayLayer: string, totalWidth: number, proportionalY: number) { - // Apply defaults, setting the width and other undefined properties for each key - let keys=row['key']; - for(let j=0; j 0) { - const finalKey = keys[keys.length-1] as ActiveKey; - - // If a single key, and padding is negative, add padding to right align the key - if(keys.length == 1 && finalKey.pad < 0) { - const keyPercent = finalKey.width/totalWidth; - const padPercent = 1-(totalPercent + keyPercent + rightMargin); - - // compute center's default x-coord (used in headless modes) - setProportions(finalKey, padPercent, keyPercent, totalPercent); - } else { - const padPercent = finalKey.pad/totalWidth; - const keyPercent = 1-(totalPercent + padPercent + rightMargin); - - // compute center's default x-coord (used in headless modes) - setProportions(finalKey, padPercent, keyPercent, totalPercent); - } - } - - // Add class functions to the existing layout object, allowing it to act as an ActiveLayout. - let dummy = new ActiveRow(); - for(let key in dummy) { - if(!row.hasOwnProperty(key)) { - row[key] = dummy[key]; - } - } - - let aRow = row as ActiveRow; - aRow.proportionalY = proportionalY; - } - - populateKeyMap(map: {[keyId: string]: ActiveKey}) { - this.key.forEach(function(key: ActiveKey) { - if(key.coreID) { - map[key.coreID] = key; - } - }); - } } - export class ActiveLayer implements LayoutLayer { - row: ActiveRow[]; - id: string; - - // These already exist on the objects, pre-polyfill... - // but they still need to be proactively declared on this type. - capsKey?: ActiveKey; - numKey?: ActiveKey; - scrollKey?: ActiveKey; - - totalWidth: number; - - defaultKeyProportionalWidth: number; - rowProportionalHeight: number; - - /** - * Facilitates mapping key id strings to their specification objects. - */ - keyMap: {[keyId: string]: ActiveKey}; - - constructor() { - - } - - static sanitize(rawLayer: LayoutLayer) { - for(const row of rawLayer.row) { - ActiveRow.sanitize(row); - } - } - - static polyfill(layer: LayoutLayer, layout: ActiveLayout) { - layer.aligned=false; - - // Create a DIV for each row of the group - let rows=layer['row']; - - // Calculate the maximum row width (in layout units) - let totalWidth=0; - for(const row of rows) { - let width=0; - const keys=row['key']; - - for(const key of keys) { - // So long as `sanitize` is called first, these coercions are safe. - width += (key.width as number) + (key.pad as number); - } - - if(width > totalWidth) { - totalWidth = width; - } - } - - // Add default right margin - if(layout.formFactor == 'desktop') { - totalWidth += 5; // TODO: resolve difference between touch and desktop; why don't we use ActiveKey.DEFAULT_RIGHT_MARGIN? + static sanitize(rawRow: LayoutRow) { + for(const key of rawRow.key) { + // Test for a trailing comma included in spec, added as null object by IE + // It has only ever appeared at the end of a row's spec. + if(key == null) { + rawRow.key.length = rawRow.key.length-1; } else { - totalWidth += ActiveKey.DEFAULT_RIGHT_MARGIN; + ActiveKey.sanitize(key); } + } - let rowCount = layer.row.length; - for(let i=0; i probability, use a function parameter in place - // of the formula in the loop below. - for(let key in keyDists) { - totalMass += keyProbs[key] = 1 / (Math.pow(keyDists[key], 2) + 1e-6); // Prevent div-by-0 errors. - } - - for(let key in keyProbs) { - keyProbs[key] /= totalMass; - } - - return keyProbs; + let setProportions = function(key: ActiveKey, padPc: number, keyPc: number, totalPc: number) { + key.proportionalPad = padPc; + key.proportionalWidth = keyPc; + key.proportionalX = (totalPc + padPc + (keyPc/2)); } - /** - * Computes a squared 'pseudo-distance' for the touch from each key. (Not a proper metric.) - * Intended for use in generating a probability distribution over the keys based on the touch input. - * @param touchCoords A proportional (x, y) coordinate of the touch within the keyboard's geometry. - * Should be within [0, 0] to [1, 1]. - * @param kbdScaleRatio The ratio of the keyboard's horizontal scale to its vertical scale. - * For a 400 x 200 keyboard, should be 2. - */ - private keyTouchDistances(touchCoords: {x: number, y: number}, kbdScaleRatio: number): {[keyId: string]: number} { - let layer = this; + // Calculate percentage-based scalings by summing defined widths and scaling each key to %. + // Save each percentage key width as a separate member (do *not* overwrite layout specified width!) + let totalPercent=0; + for(let j=0; j 0) { + const finalKey = keys[keys.length-1] as ActiveKey; + + // If a single key, and padding is negative, add padding to right align the key + if(keys.length == 1 && finalKey.pad < 0) { + const keyPercent = finalKey.width/totalWidth; + const padPercent = 1-(totalPercent + keyPercent + rightMargin); + + // compute center's default x-coord (used in headless modes) + setProportions(finalKey, padPercent, keyPercent, totalPercent); + } else { + const padPercent = finalKey.pad/totalWidth; + const keyPercent = 1-(totalPercent + padPercent + rightMargin); + + // compute center's default x-coord (used in headless modes) + setProportions(finalKey, padPercent, keyPercent, totalPercent); + } + } + + // Add class functions to the existing layout object, allowing it to act as an ActiveLayout. + let dummy = new ActiveRow(); + for(let key in dummy) { + if(!row.hasOwnProperty(key)) { + row[key] = dummy[key]; + } + } + + let aRow = row as ActiveRow; + aRow.proportionalY = proportionalY; + } + + populateKeyMap(map: {[keyId: string]: ActiveKey}) { + this.key.forEach(function(key: ActiveKey) { + if(key.coreID) { + map[key.coreID] = key; + } + }); + } +} + +export class ActiveLayer implements LayoutLayer { + row: ActiveRow[]; + id: string; + + // These already exist on the objects, pre-polyfill... + // but they still need to be proactively declared on this type. + capsKey?: ActiveKey; + numKey?: ActiveKey; + scrollKey?: ActiveKey; + + totalWidth: number; + + defaultKeyProportionalWidth: number; + rowProportionalHeight: number; + + /** + * Facilitates mapping key id strings to their specification objects. + */ + keyMap: {[keyId: string]: ActiveKey}; + + constructor() { + + } + + static sanitize(rawLayer: LayoutLayer) { + for(const row of rawLayer.row) { + ActiveRow.sanitize(row); + } + } + + static polyfill(layer: LayoutLayer, layout: ActiveLayout) { + layer.aligned=false; + + // Create a DIV for each row of the group + let rows=layer['row']; + + // Calculate the maximum row width (in layout units) + let totalWidth=0; + for(const row of rows) { + let width=0; + const keys=row['key']; + + for(const key of keys) { + // So long as `sanitize` is called first, these coercions are safe. + width += (key.width as number) + (key.pad as number); + } + + if(width > totalWidth) { + totalWidth = width; + } + } + + // Add default right margin + if(layout.formFactor == 'desktop') { + totalWidth += 5; // TODO: resolve difference between touch and desktop; why don't we use ActiveKey.DEFAULT_RIGHT_MARGIN? + } else { + totalWidth += ActiveKey.DEFAULT_RIGHT_MARGIN; + } + + let rowCount = layer.row.length; + for(let i=0; i probability, use a function parameter in place + // of the formula in the loop below. + for(let key in keyDists) { + totalMass += keyProbs[key] = 1 / (Math.pow(keyDists[key], 2) + 1e-6); // Prevent div-by-0 errors. + } + + for(let key in keyProbs) { + keyProbs[key] /= totalMass; + } + + return keyProbs; + } + + /** + * Computes a squared 'pseudo-distance' for the touch from each key. (Not a proper metric.) + * Intended for use in generating a probability distribution over the keys based on the touch input. + * @param touchCoords A proportional (x, y) coordinate of the touch within the keyboard's geometry. + * Should be within [0, 0] to [1, 1]. + * @param kbdScaleRatio The ratio of the keyboard's horizontal scale to its vertical scale. + * For a 400 x 200 keyboard, should be 2. + */ + private keyTouchDistances(touchCoords: {x: number, y: number}, kbdScaleRatio: number): {[keyId: string]: number} { + let layer = this; + + let keyDists: {[keyId: string]: number} = {}; + + // This double-nested loop computes a pseudo-distance for the touch from each key. Quite useful for + // generating a probability distribution. + this.row.forEach(function(row: ActiveRow): void { + row.key.forEach(function(key: ActiveKey): void { + // If the key lacks an ID, just skip it. Sometimes used for padding. + if(!key.baseKeyID) { + return; + } else { + // Attempt to filter out known non-output keys. + // Results in a more optimized distribution. + if(Codes.isKnownOSKModifierKey(key.baseKeyID)) { + return; + } else if(key.isPadding) { // to the user, blank / padding keys do not exist. return; - } else { - // Attempt to filter out known non-output keys. - // Results in a more optimized distribution. - if(text.Codes.isKnownOSKModifierKey(key.baseKeyID)) { - return; - } else if(key.isPadding) { // to the user, blank / padding keys do not exist. - return; - } } - // These represent the within-key distance of the touch from the key's center. - // Both should be on the interval [0, 0.5]. - let dx = Math.abs(touchCoords.x - key.proportionalX); - let dy = Math.abs(touchCoords.y - row.proportionalY); + } + // These represent the within-key distance of the touch from the key's center. + // Both should be on the interval [0, 0.5]. + let dx = Math.abs(touchCoords.x - key.proportionalX); + let dy = Math.abs(touchCoords.y - row.proportionalY); - // If the touch isn't within the key, these store the out-of-key distance - // from the closest point on the key being checked. - let distX: number, distY: number; + // If the touch isn't within the key, these store the out-of-key distance + // from the closest point on the key being checked. + let distX: number, distY: number; - if(dx > 0.5 * key.proportionalWidth) { - distX = (dx - 0.5 * key.proportionalWidth); - dx = 0.5; - } else { - distX = 0; - dx /= key.proportionalWidth; - } + if(dx > 0.5 * key.proportionalWidth) { + distX = (dx - 0.5 * key.proportionalWidth); + dx = 0.5; + } else { + distX = 0; + dx /= key.proportionalWidth; + } - if(dy > 0.5 * layer.rowProportionalHeight) { - distY = (dy - 0.5 * layer.rowProportionalHeight); - dy = 0.5; - } else { - distY = 0; - dy /= layer.rowProportionalHeight; - } + if(dy > 0.5 * layer.rowProportionalHeight) { + distY = (dy - 0.5 * layer.rowProportionalHeight); + dy = 0.5; + } else { + distY = 0; + dy /= layer.rowProportionalHeight; + } - // Now that the differentials are computed, it's time to do distance scaling. - // - // For out-of-key distance, we scale the X component by the keyboard's aspect ratio - // to get the actual out-of-key distance rather than proportional. - distX *= kbdScaleRatio; + // Now that the differentials are computed, it's time to do distance scaling. + // + // For out-of-key distance, we scale the X component by the keyboard's aspect ratio + // to get the actual out-of-key distance rather than proportional. + distX *= kbdScaleRatio; - // While the keys are rarely perfect squares, we map all within-key distance - // to a square shape. (ALT/CMD should seem as close to SPACE as a 'B'.) - // - // For that square, we take the rowHeight as its edge lengths. - distX += dx * layer.rowProportionalHeight; - distY += dy * layer.rowProportionalHeight; + // While the keys are rarely perfect squares, we map all within-key distance + // to a square shape. (ALT/CMD should seem as close to SPACE as a 'B'.) + // + // For that square, we take the rowHeight as its edge lengths. + distX += dx * layer.rowProportionalHeight; + distY += dy * layer.rowProportionalHeight; - let distance = distX * distX + distY * distY; - keyDists[key.coreID] = distance; - }); + let distance = distX * distX + distY * distY; + keyDists[key.coreID] = distance; }); + }); - return keyDists; - } - - getKey(keyId: string) { - // Keys usually are specified in a "long form" prefixed with their layer's ID. - if(keyId.indexOf(this.id + '-') == 0) { - keyId = keyId.replace(this.id + '-', ''); - } - - let idComponents = keyId.split('::'); - if(idComponents.length > 1) { - let baseKey = this.keyMap[idComponents[0]]; - return baseKey.getSubkey(idComponents[1]); - } else { - return this.keyMap[keyId]; - } - } + return keyDists; } - export class ActiveLayout implements LayoutFormFactor{ - layer: ActiveLayer[]; - font: string; - keyLabels: boolean; - isDefault?: boolean; - keyboard: Keyboard; - formFactor: utils.FormFactor; - - /** - * Facilitates mapping layer id strings to their specification objects. - */ - layerMap: {[layerId: string]: ActiveLayer}; - - private constructor() { - + getKey(keyId: string) { + // Keys usually are specified in a "long form" prefixed with their layer's ID. + if(keyId.indexOf(this.id + '-') == 0) { + keyId = keyId.replace(this.id + '-', ''); } - getLayer(layerId: string): ActiveLayer { - return this.layerMap[layerId]; - } - - /** - * Refer to https://github.com/keymanapp/keyman/issues/254, which mentions - * KD-11 from a prior issue-tracking system from the closed-source days that - * resulted in an unintended extra empty row. - * - * It'll be pretty rare to see a keyboard affected by the bug, but we don't - * 100% control all keyboards out there, so it's best we make sure the edge - * case is covered. - * - * @param layers The layer group to be loaded for the form factor. Will be - * mutated by this operation. - */ - static correctLayerEmptyRowBug(layers: LayoutLayer[]) { - for(let n=0; n=0; i--) { - if(!Array.isArray(rows[i]['key']) || rows[i]['key'].length == 0) { - rows.splice(i, 1) - } - } - } - } - - static sanitize(rawLayout: LayoutFormFactor) { - ActiveLayout.correctLayerEmptyRowBug(rawLayout.layer); - - for(const layer of rawLayout.layer) { - ActiveLayer.sanitize(layer); - } - } - - /** - * - * @param layout - * @param formFactor - */ - static polyfill(layout: LayoutFormFactor, keyboard: Keyboard, formFactor: utils.FormFactor): ActiveLayout { - if(layout == null) { - throw new Error("Cannot build an ActiveLayout for a null specification."); - } - - /* Standardize the layout object's data types. - * - * In older versions of KMW, some numeric properties were long represented as strings instead, - * and that lives on within a _lot_ of keyboards. The data should be sanitized before it - * is processed by this method. - */ - this.sanitize(layout); - - // Create a separate OSK div for each OSK layer, only one of which will ever be visible - var n: number; - let layerMap: {[layerId: string]: ActiveLayer} = {}; - - let layers=layout.layer; - - // Add class functions to the existing layout object, allowing it to act as an ActiveLayout. - let dummy = new ActiveLayout(); - for(let key in dummy) { - if(!layout.hasOwnProperty(key)) { - layout[key] = dummy[key]; - } - } - - let aLayout = layout as ActiveLayout; - aLayout.keyboard = keyboard; - aLayout.formFactor = formFactor; - - for(n=0; n 1) { + let baseKey = this.keyMap[idComponents[0]]; + return baseKey.getSubkey(idComponents[1]); + } else { + return this.keyMap[keyId]; } } } + +export class ActiveLayout implements LayoutFormFactor{ + layer: ActiveLayer[]; + font: string; + keyLabels: boolean; + isDefault?: boolean; + keyboard: Keyboard; + formFactor: DeviceSpec.FormFactor; + + /** + * Facilitates mapping layer id strings to their specification objects. + */ + layerMap: {[layerId: string]: ActiveLayer}; + + private constructor() { + + } + + getLayer(layerId: string): ActiveLayer { + return this.layerMap[layerId]; + } + + /** + * Refer to https://github.com/keymanapp/keyman/issues/254, which mentions + * KD-11 from a prior issue-tracking system from the closed-source days that + * resulted in an unintended extra empty row. + * + * It'll be pretty rare to see a keyboard affected by the bug, but we don't + * 100% control all keyboards out there, so it's best we make sure the edge + * case is covered. + * + * @param layers The layer group to be loaded for the form factor. Will be + * mutated by this operation. + */ + static correctLayerEmptyRowBug(layers: LayoutLayer[]) { + for(let n=0; n=0; i--) { + if(!Array.isArray(rows[i]['key']) || rows[i]['key'].length == 0) { + rows.splice(i, 1) + } + } + } + } + + static sanitize(rawLayout: LayoutFormFactor) { + ActiveLayout.correctLayerEmptyRowBug(rawLayout.layer); + + for(const layer of rawLayout.layer) { + ActiveLayer.sanitize(layer); + } + } + + /** + * + * @param layout + * @param formFactor + */ + static polyfill(layout: LayoutFormFactor, keyboard: Keyboard, formFactor: DeviceSpec.FormFactor): ActiveLayout { + if(layout == null) { + throw new Error("Cannot build an ActiveLayout for a null specification."); + } + + /* Standardize the layout object's data types. + * + * In older versions of KMW, some numeric properties were long represented as strings instead, + * and that lives on within a _lot_ of keyboards. The data should be sanitized before it + * is processed by this method. + */ + this.sanitize(layout); + + // Create a separate OSK div for each OSK layer, only one of which will ever be visible + var n: number; + let layerMap: {[layerId: string]: ActiveLayer} = {}; + + let layers=layout.layer; + + // Add class functions to the existing layout object, allowing it to act as an ActiveLayout. + let dummy = new ActiveLayout(); + for(let key in dummy) { + if(!layout.hasOwnProperty(key)) { + layout[key] = dummy[key]; + } + } + + let aLayout = layout as ActiveLayout; + aLayout.keyboard = keyboard; + aLayout.formFactor = formFactor; + + for(n=0; n?~~~~~ '; + + static readonly DEFAULT_RAW_SPEC = {'F':'Tahoma', 'BK': Layouts.dfltText}; + + // Cross-reference with the ids in osk.setButtonClass. + static buttonClasses: {[name: string]: ButtonClass} = { + 'DEFAULT':0, + 'SHIFT':1, + 'SHIFT-ON':2, + 'SPECIAL':3, + 'SPECIAL-ON':4, + 'DEADKEY':8, + 'BLANK':9, + 'HIDDEN':10 }; - export type LayoutLayer = { - "id": string, - "row": LayoutRow[], + static modifierSpecials = { + 'leftalt': '*LAlt*', + 'rightalt': '*RAlt*', + 'alt': '*Alt*', + 'leftctrl': '*LCtrl*', + 'rightctrl': '*RCtrl*', + 'ctrl': '*Ctrl*', + 'ctrl-alt': '*AltGr*', + 'leftctrl-leftalt': '*LAltCtrl*', + 'rightctrl-rightalt': '*RAltCtrl*', + 'leftctrl-leftalt-shift': '*LAltCtrlShift*', + 'rightctrl-rightalt-shift': '*RAltCtrlShift*', + 'shift': '*Shift*', + 'shift-alt': '*AltShift*', + 'shift-ctrl': '*CtrlShift*', + 'shift-ctrl-alt': '*AltCtrlShift*', + 'leftalt-shift': '*LAltShift*', + 'rightalt-shift': '*RAltShift*', + 'leftctrl-shift': '*LCtrlShift*', + 'rightctrl-shift': '*RCtrlShift*' + }; - // Post-processing elements. - shiftKey?: LayoutKey, - capsKey?: LayoutKey, - numKey?: LayoutKey, - scrollKey?: LayoutKey, - aligned?: boolean - } + /** + * Build a default layout for keyboards with no explicit layout + * + * @param {Object} PVK raw specifications + * @param {Keyboard} keyboard keyboard object (as loaded) + * @param {string} formFactor (really utils.FormFactor) + * @return {LayoutFormFactor} + */ + static buildDefaultLayout(PVK, keyboard: Keyboard, formFactor: string): LayoutFormFactor { + // Build a layout using the default for the device + var layoutType=formFactor; - export type LayoutFormFactor = { - "displayUnderlying"?: boolean, - "font": string, - "layer": LayoutLayer[], - isDefault?: boolean - } - - export type LayoutSpec = { - "desktop"?: LayoutFormFactor, - "phone"?: LayoutFormFactor, - "tablet"?: LayoutFormFactor - } - - // This class manages default layout construction for consumption by OSKs without a specified layout. - export class Layouts { - static dfltCodes=[ - "K_BKQUOTE","K_1","K_2","K_3","K_4","K_5","K_6","K_7","K_8","K_9","K_0", - "K_HYPHEN","K_EQUAL","K_*","K_*","K_*","K_Q","K_W","K_E","K_R","K_T", - "K_Y","K_U","K_I","K_O","K_P","K_LBRKT","K_RBRKT","K_BKSLASH","K_*", - "K_*","K_*","K_A","K_S","K_D","K_F","K_G","K_H","K_J","K_K","K_L", - "K_COLON","K_QUOTE","K_*","K_*","K_*","K_*","K_*","K_oE2", - "K_Z","K_X","K_C","K_V","K_B","K_N","K_M","K_COMMA","K_PERIOD", - "K_SLASH","K_*","K_*","K_*","K_*","K_*","K_SPACE" - ]; - - static dfltText='`1234567890-=\xA7~~qwertyuiop[]\\~~~asdfghjkl;\'~~~~~?zxcvbnm,./~~~~~ ' - +'~!@#$%^&*()_+\xA7~~QWERTYUIOP{}\\~~~ASDFGHJKL:"~~~~~?ZXCVBNM<>?~~~~~ '; - - static readonly DEFAULT_RAW_SPEC = {'F':'Tahoma', 'BK': Layouts.dfltText}; - - // Cross-reference with the ids in osk.setButtonClass. - static buttonClasses: {[name: string]: ButtonClass} = { - 'DEFAULT':0, - 'SHIFT':1, - 'SHIFT-ON':2, - 'SPECIAL':3, - 'SPECIAL-ON':4, - 'DEADKEY':8, - 'BLANK':9, - 'HIDDEN':10 - }; - - static modifierSpecials = { - 'leftalt': '*LAlt*', - 'rightalt': '*RAlt*', - 'alt': '*Alt*', - 'leftctrl': '*LCtrl*', - 'rightctrl': '*RCtrl*', - 'ctrl': '*Ctrl*', - 'ctrl-alt': '*AltGr*', - 'leftctrl-leftalt': '*LAltCtrl*', - 'rightctrl-rightalt': '*RAltCtrl*', - 'leftctrl-leftalt-shift': '*LAltCtrlShift*', - 'rightctrl-rightalt-shift': '*RAltCtrlShift*', - 'shift': '*Shift*', - 'shift-alt': '*AltShift*', - 'shift-ctrl': '*CtrlShift*', - 'shift-ctrl-alt': '*AltCtrlShift*', - 'leftalt-shift': '*LAltShift*', - 'rightalt-shift': '*RAltShift*', - 'leftctrl-shift': '*LCtrlShift*', - 'rightctrl-shift': '*RCtrlShift*' - }; - - /** - * Build a default layout for keyboards with no explicit layout - * - * @param {Object} PVK raw specifications - * @param {Keyboard} keyboard keyboard object (as loaded) - * @param {string} formFactor (really utils.FormFactor) - * @return {LayoutFormFactor} - */ - static buildDefaultLayout(PVK, keyboard: Keyboard, formFactor: string): LayoutFormFactor { - // Build a layout using the default for the device - var layoutType=formFactor; - - if(typeof Layouts.dfltLayout[layoutType] != 'object') { - layoutType = 'desktop'; - } - - let kbdBitmask = Codes.modifierBitmasks['NON_CHIRAL']; - // An unfortunate dependency there. Should probably also set a version within web-core for use. - let kbdDevVersion = utils.Version.CURRENT; - if(keyboard) { - kbdBitmask = keyboard.modifierBitmask; - kbdDevVersion = keyboard.compilerVersion; - } - - if(!PVK) { - PVK = this.DEFAULT_RAW_SPEC; - } - - // Clone the default layout object for this device - var layout: LayoutFormFactor = utils.deepCopy(Layouts.dfltLayout[layoutType]); - - var n,layers=layout['layer'], keyLabels: KLS=PVK['KLS'], key102=PVK['K102']; - var i, j, k, m, row, rows: LayoutRow[], key: LayoutKey, keys: LayoutKey[]; - var chiral: boolean = (kbdBitmask & Codes.modifierBitmasks.IS_CHIRAL) != 0; - - if(PVK['F']) { - // The KeymanWeb compiler generates a string of the format `[italic ][bold ] 1em ""` - // We will ignore the bold, italic and font size spec - let legacyFontSpec = /^(?:(?:italic|bold) )* *[0-9.eE-]+(?:[a-z]+) "(.+)"$/.exec(PVK['F']); - if(legacyFontSpec) { - layout.font = legacyFontSpec[1]; - } - } - - var kmw10Plus = !(typeof keyLabels == 'undefined' || !keyLabels); - if(!kmw10Plus) { - // Save the processed key label information to the keyboard's general data. - // Makes things more efficient elsewhere and for reloading after keyboard swaps. - keyLabels = PVK['KLS'] = Layouts.processLegacyDefinitions(PVK['BK']); - } - - // Identify key labels (e.g. *Shift*) that require the special OSK font - var specialLabel=/\*\w+\*/; - - // *** Step 1: instantiate the layer objects. *** - - // Get the list of valid layers, enforcing that the 'default' layer must be the first one processed. - var validIdList = Object.getOwnPropertyNames(keyLabels), invalidIdList = []; - validIdList.splice(validIdList.indexOf('default'), 1); - validIdList = [ 'default' ].concat(validIdList); - - // Automatic AltGr emulation if the 'leftctrl-leftalt' layer is otherwise undefined. - if(keyboard && keyboard.emulatesAltGr) { - // We insert only the layers that need to be emulated. - if((validIdList.indexOf('leftctrl-leftalt') == -1) && validIdList.indexOf('rightalt') != -1) { - validIdList.push('leftctrl-leftalt'); - keyLabels['leftctrl-leftalt'] = keyLabels['rightalt']; - } - - if((validIdList.indexOf('leftctrl-leftalt-shift') == -1) && validIdList.indexOf('rightalt-shift') != -1) { - validIdList.push('leftctrl-leftalt-shift'); - keyLabels['leftctrl-leftalt-shift'] = keyLabels['rightalt-shift']; - } - } - - // If there is no predefined layout, even touch layouts will follow the desktop's - // setting for the displayUnderlying flag. As the desktop layout uses a different - // format for its layout spec, that's found at the field referenced below. - layout["displayUnderlying"] = keyboard ? !!keyboard.scriptObject['KDU'] : false; - - // For desktop devices, we must create all layers, even if invalid. - if(formFactor == 'desktop') { - invalidIdList = Layouts.generateLayerIds(chiral); - - // Filter out all ids considered valid. (We also don't want duplicates in the following list...) - for(n=0; n 0) { - layers[n]=utils.deepCopy(layers[0]); - } - layers[n]['id']=idList[n]; - layers[n]['nextlayer']=idList[n]; // This would only be different for a dynamic keyboard - - // Extraced into a helper method to improve readability. - Layouts.formatDefaultLayer(layers[n], chiral, formFactor, !!key102); - } - - // *** Step 2: Layer objects now exist; time to fill them with the appropriate key labels and key styles *** - for(n=0; n= 0 && kx < layerSpec.length) key['text']=layerSpec[kx]; - } - - // Legacy (pre 12.0) behavior: fall back to US English keycap text as default for the base two layers - // if a key cap is not otherwise defined. (Any intentional 'ghost' keys must be explicitly defined.) - if(isDefault && kbdDevVersion.precedes(utils.Version.NO_DEFAULT_KEYCAPS)) { - if(key['id'] != 'K_SPACE' && kx+65 * isShift < Layouts.dfltText.length && key['text'] !== null) { - key['text'] = key['text'] || Layouts.dfltText[kx+65*isShift]; - } - } - } - - // Leave any unmarked key caps as null strings - if(key['text'] !== null) { - key['text'] = key['text'] || ''; - } - - // Detect important tracking keys. - switch(key['id']) { - case "K_SHIFT": - shiftKey=key; - break; - case "K_TAB": - nextKey=key; - break; - case "K_CAPS": - capsKey=key; - break; - case "K_NUMLOCK": - numKey=key; - break; - case "K_SCROLL": - scrollKey=key; - break; - } - - // Remove pop-up shift keys referencing invalid layers (Build 349) - if(key['sk'] != null) { - for(k=0; k 0 && shiftKey != null) { - shiftKey['sp']=Layouts.buttonClasses['SHIFT-ON']; - shiftKey['sk']=null; - shiftKey['text'] = Layouts.modifierSpecials[layers[n].id] ? Layouts.modifierSpecials[layers[n].id] : "*Shift*"; - } - } - } - - return layout; + if(typeof Layouts.dfltLayout[layoutType] != 'object') { + layoutType = 'desktop'; } - /** - * Function getLayerId - * Scope Private - * @param {number} m shift modifier code - * @return {string} layer string from shift modifier code (desktop keyboards) - * Description Get name of layer from code, where the modifer order is determined by ascending bit-flag value. - */ - static getLayerId(m: number): string { - let modifierCodes = Codes.modifierCodes; + let kbdBitmask = Codes.modifierBitmasks['NON_CHIRAL']; + // An unfortunate dependency there. Should probably also set a version within web-core for use. + let kbdDevVersion = Version.CURRENT; + if(keyboard) { + kbdBitmask = keyboard.modifierBitmask; + kbdDevVersion = keyboard.compilerVersion; + } - var s=''; - if(m == 0) { - return 'default'; + if(!PVK) { + PVK = this.DEFAULT_RAW_SPEC; + } + + // Clone the default layout object for this device + var layout: LayoutFormFactor = deepCopy(Layouts.dfltLayout[layoutType]); + + var n,layers=layout['layer'], keyLabels: KLS=PVK['KLS'], key102=PVK['K102']; + var i, j, k, m, row, rows: LayoutRow[], key: LayoutKey, keys: LayoutKey[]; + var chiral: boolean = (kbdBitmask & Codes.modifierBitmasks.IS_CHIRAL) != 0; + + if(PVK['F']) { + // The KeymanWeb compiler generates a string of the format `[italic ][bold ] 1em ""` + // We will ignore the bold, italic and font size spec + let legacyFontSpec = /^(?:(?:italic|bold) )* *[0-9.eE-]+(?:[a-z]+) "(.+)"$/.exec(PVK['F']); + if(legacyFontSpec) { + layout.font = legacyFontSpec[1]; + } + } + + var kmw10Plus = !(typeof keyLabels == 'undefined' || !keyLabels); + if(!kmw10Plus) { + // Save the processed key label information to the keyboard's general data. + // Makes things more efficient elsewhere and for reloading after keyboard swaps. + keyLabels = PVK['KLS'] = Layouts.processLegacyDefinitions(PVK['BK']); + } + + // Identify key labels (e.g. *Shift*) that require the special OSK font + var specialLabel=/\*\w+\*/; + + // *** Step 1: instantiate the layer objects. *** + + // Get the list of valid layers, enforcing that the 'default' layer must be the first one processed. + var validIdList = Object.getOwnPropertyNames(keyLabels), invalidIdList = []; + validIdList.splice(validIdList.indexOf('default'), 1); + validIdList = [ 'default' ].concat(validIdList); + + // Automatic AltGr emulation if the 'leftctrl-leftalt' layer is otherwise undefined. + if(keyboard && keyboard.emulatesAltGr) { + // We insert only the layers that need to be emulated. + if((validIdList.indexOf('leftctrl-leftalt') == -1) && validIdList.indexOf('rightalt') != -1) { + validIdList.push('leftctrl-leftalt'); + keyLabels['leftctrl-leftalt'] = keyLabels['rightalt']; + } + + if((validIdList.indexOf('leftctrl-leftalt-shift') == -1) && validIdList.indexOf('rightalt-shift') != -1) { + validIdList.push('leftctrl-leftalt-shift'); + keyLabels['leftctrl-leftalt-shift'] = keyLabels['rightalt-shift']; + } + } + + // If there is no predefined layout, even touch layouts will follow the desktop's + // setting for the displayUnderlying flag. As the desktop layout uses a different + // format for its layout spec, that's found at the field referenced below. + layout["displayUnderlying"] = keyboard ? !!keyboard.scriptObject['KDU'] : false; + + // For desktop devices, we must create all layers, even if invalid. + if(formFactor == 'desktop') { + invalidIdList = Layouts.generateLayerIds(chiral); + + // Filter out all ids considered valid. (We also don't want duplicates in the following list...) + for(n=0; n 0 ? s + '-' : '') + 'leftctrl'; - } - if(m & modifierCodes['RCTRL']) { - s = (s.length > 0 ? s + '-' : '') + 'rightctrl'; - } - if(m & modifierCodes['LALT']) { - s = (s.length > 0 ? s + '-' : '') + 'leftalt'; - } - if(m & modifierCodes['RALT']) { - s = (s.length > 0 ? s + '-' : '') + 'rightalt'; - } - if(m & modifierCodes['SHIFT']) { - s = (s.length > 0 ? s + '-' : '') + 'shift'; - } - if(m & modifierCodes['CTRL']) { - s = (s.length > 0 ? s + '-' : '') + 'ctrl'; - } - if(m & modifierCodes['ALT']) { - s = (s.length > 0 ? s + '-' : '') + 'alt'; - } - return s; + // Seriously, this should never happen. It's here for the debugging log only. + console.warn("Error in default layout - cannot find default Shift key!"); } } - /** - * Generates a list of potential layer ids for the specified chirality mode. - * - * @param {boolean} chiral // Does the keyboard use chiral modifiers or not? - */ - static generateLayerIds(chiral: boolean): string[] { - var layerCnt, offset; - - if(chiral) { - layerCnt=32; - offset=0x01; - } else { - layerCnt=8; - offset=0x10; + for(n=0; n 0) { + layers[n]=deepCopy(layers[0]); } + layers[n]['id']=idList[n]; + layers[n]['nextlayer']=idList[n]; // This would only be different for a dynamic keyboard - var layerIds = []; - - for(var i=0; i < layerCnt; i++) { - layerIds.push(Layouts.getLayerId(i * offset)); - } - - return layerIds; + // Extraced into a helper method to improve readability. + Layouts.formatDefaultLayer(layers[n], chiral, formFactor, !!key102); } - /** - * Sets a formatting property for the modifier keys when constructing a default layout for a keyboard. - * - * @param {Object} layer // One layer specification - * @param {boolean} chiral // Whether or not the keyboard uses chiral modifier information. - * @param {string} formFactor // The form factor of the device the layout is being constructed for. - * @param {boolean} key102 // Whether or not the extended key 102 should be hidden. - */ - static formatDefaultLayer(layer: LayoutLayer, chiral: boolean, formFactor: string, key102: boolean) { - var layerId = layer['id']; - let buttonClasses = Layouts.buttonClasses; + // *** Step 2: Layer objects now exist; time to fill them with the appropriate key labels and key styles *** + for(n=0; n= 0 && kx < layerSpec.length) key['text']=layerSpec[kx]; + } + + // Legacy (pre 12.0) behavior: fall back to US English keycap text as default for the base two layers + // if a key cap is not otherwise defined. (Any intentional 'ghost' keys must be explicitly defined.) + if(isDefault && kbdDevVersion.precedes(Version.NO_DEFAULT_KEYCAPS)) { + if(key['id'] != 'K_SPACE' && kx+65 * isShift < Layouts.dfltText.length && key['text'] !== null) { + key['text'] = key['text'] || Layouts.dfltText[kx+65*isShift]; + } + } + } + + // Leave any unmarked key caps as null strings + if(key['text'] !== null) { + key['text'] = key['text'] || ''; + } + + // Detect important tracking keys. switch(key['id']) { - case 'K_SHIFT': - case 'K_LSHIFT': - case 'K_RSHIFT': - if(layerId.indexOf('shift') != -1) { + case "K_SHIFT": + shiftKey=key; + break; + case "K_TAB": + nextKey=key; + break; + case "K_CAPS": + capsKey=key; + break; + case "K_NUMLOCK": + numKey=key; + break; + case "K_SCROLL": + scrollKey=key; + break; + } + + // Remove pop-up shift keys referencing invalid layers (Build 349) + if(key['sk'] != null) { + for(k=0; k 0 && shiftKey != null) { + shiftKey['sp']=Layouts.buttonClasses['SHIFT-ON']; + shiftKey['sk']=null; + shiftKey['text'] = Layouts.modifierSpecials[layers[n].id] ? Layouts.modifierSpecials[layers[n].id] : "*Shift*"; + } + } + } + + return layout; + } + + /** + * Function getLayerId + * Scope Private + * @param {number} m shift modifier code + * @return {string} layer string from shift modifier code (desktop keyboards) + * Description Get name of layer from code, where the modifer order is determined by ascending bit-flag value. + */ + static getLayerId(m: number): string { + let modifierCodes = Codes.modifierCodes; + + var s=''; + if(m == 0) { + return 'default'; + } else { + if(m & modifierCodes['LCTRL']) { + s = (s.length > 0 ? s + '-' : '') + 'leftctrl'; + } + if(m & modifierCodes['RCTRL']) { + s = (s.length > 0 ? s + '-' : '') + 'rightctrl'; + } + if(m & modifierCodes['LALT']) { + s = (s.length > 0 ? s + '-' : '') + 'leftalt'; + } + if(m & modifierCodes['RALT']) { + s = (s.length > 0 ? s + '-' : '') + 'rightalt'; + } + if(m & modifierCodes['SHIFT']) { + s = (s.length > 0 ? s + '-' : '') + 'shift'; + } + if(m & modifierCodes['CTRL']) { + s = (s.length > 0 ? s + '-' : '') + 'ctrl'; + } + if(m & modifierCodes['ALT']) { + s = (s.length > 0 ? s + '-' : '') + 'alt'; + } + return s; + } + } + + /** + * Generates a list of potential layer ids for the specified chirality mode. + * + * @param {boolean} chiral // Does the keyboard use chiral modifiers or not? + */ + static generateLayerIds(chiral: boolean): string[] { + var layerCnt, offset; + + if(chiral) { + layerCnt=32; + offset=0x01; + } else { + layerCnt=8; + offset=0x10; + } + + var layerIds = []; + + for(var i=0; i < layerCnt; i++) { + layerIds.push(Layouts.getLayerId(i * offset)); + } + + return layerIds; + } + + /** + * Sets a formatting property for the modifier keys when constructing a default layout for a keyboard. + * + * @param {Object} layer // One layer specification + * @param {boolean} chiral // Whether or not the keyboard uses chiral modifier information. + * @param {string} formFactor // The form factor of the device the layout is being constructed for. + * @param {boolean} key102 // Whether or not the extended key 102 should be hidden. + */ + static formatDefaultLayer(layer: LayoutLayer, chiral: boolean, formFactor: string, key102: boolean) { + var layerId = layer['id']; + let buttonClasses = Layouts.buttonClasses; + + // Correct appearance of state-dependent modifier keys according to group + for(var i=0; i -/// -/// +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 "utils/build/modules/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 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 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]; + } } \ No newline at end of file diff --git a/common/web/keyboard-processor/src/text/codes.ts b/common/web/keyboard-processor/src/text/codes.ts index 48fde0d728..c778400cf1 100644 --- a/common/web/keyboard-processor/src/text/codes.ts +++ b/common/web/keyboard-processor/src/text/codes.ts @@ -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. - }, +let 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. + let 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; } -} \ No newline at end of file +} + +export default Codes; \ No newline at end of file diff --git a/common/web/keyboard-processor/src/text/deadkeys.ts b/common/web/keyboard-processor/src/text/deadkeys.ts index d0f775ae04..e5b32a788f 100644 --- a/common/web/keyboard-processor/src/text/deadkeys.ts +++ b/common/web/keyboard-processor/src/text/deadkeys.ts @@ -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; + } } \ No newline at end of file diff --git a/common/web/keyboard-processor/src/text/defaultOutput.ts b/common/web/keyboard-processor/src/text/defaultOutput.ts index 9d96f4bd1c..5d5f8f3755 100644 --- a/common/web/keyboard-processor/src/text/defaultOutput.ts +++ b/common/web/keyboard-processor/src/text/defaultOutput.ts @@ -1,191 +1,212 @@ -// Establishes key-code definitions. -/// -// Defines our generalized "KeyEvent" class. -/// +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; + } } diff --git a/common/web/keyboard-processor/src/text/kbdInterface.ts b/common/web/keyboard-processor/src/text/kbdInterface.ts index 672ecd4ded..249c8ad8fd 100644 --- a/common/web/keyboard-processor/src/text/kbdInterface.ts +++ b/common/web/keyboard-processor/src/text/kbdInterface.ts @@ -1,1164 +1,1176 @@ -/// -/// - -// Defines classes for handling system stores -/// - /*** KeymanWeb 11.0 Copyright 2019 SIL International ***/ -namespace com.keyman.text { - //#region Helper type definitions +//#region Imports - export class KeyInformation { - vk: boolean; - code: number; - modifiers: number; - } +import Codes from "./codes.js"; +import type KeyEvent from "./keyEvent.js"; +import type { Deadkey } from "./deadkeys.js"; +import KeyMapping from "./keyMapping.js"; +import { SystemStore, MutableSystemStore, PlatformSystemStore } from "./systemStores.js"; +import type { VariableStoreSerializer } from "./keyboardProcessor.js"; - /* - * Type alias definitions to reflect the parameters of the fullContextMatch() callback (KMW 10+). - * No constructors or methods since keyboards will not utilize the same backing prototype, and - * property names are shorthanded to promote minification. - */ - type PlainKeyboardStore = string; +import type OutputTarget from "./outputTarget.js"; +import { Mock } from "./outputTarget.js"; - export type KeyboardStoreElement = (string|StoreNonCharEntry); - export type ComplexKeyboardStore = KeyboardStoreElement[]; +import RuleBehavior from "./ruleBehavior.js"; +import Keyboard, { VariableStoreDictionary } from "../keyboards/keyboard.js"; - type KeyboardStore = PlainKeyboardStore | ComplexKeyboardStore; +import { type DeviceSpec } from "utils/build/modules/index.js"; - export type VariableStore = {[name: string]: string}; +//#endregion - type RuleChar = string; +//#region Helper type definitions - class RuleDeadkey { - /** Discriminant field - 'd' for Deadkey. - */ - ['t']: 'd'; +export class KeyInformation { + vk: boolean; + code: number; + modifiers: number; +} - /** - * Value: the deadkey's ID. - */ - ['d']: number; // For 'd'eadkey; also reflects the Deadkey class's 'd' property. - } +/* +* Type alias definitions to reflect the parameters of the fullContextMatch() callback (KMW 10+). +* No constructors or methods since keyboards will not utilize the same backing prototype, and +* property names are shorthanded to promote minification. +*/ +type PlainKeyboardStore = string; - class ContextAny { - /** Discriminant field - 'a' for `any()`. - */ - ['t']: 'a'; +export type KeyboardStoreElement = (string|StoreNonCharEntry); +export type ComplexKeyboardStore = KeyboardStoreElement[]; - /** - * Value: the store to search. - */ - ['a']: KeyboardStore; // For 'a'ny statement. +type KeyboardStore = PlainKeyboardStore | ComplexKeyboardStore; - /** - * If set to true, negates the 'any'. - */ - ['n']: boolean|0|1; - } +export type VariableStore = {[name: string]: string}; - class RuleIndex { - /** Discriminant field - 'i' for `index()`. - */ - ['t']: 'i'; +type RuleChar = string; - /** - * Value: the Store from which to output - */ - ['i']: KeyboardStore; - - /** - * Offset: the offset in context for the corresponding `any()`. - */ - ['o']: number; - } - - class ContextEx { - /** Discriminant field - 'c' for `context()`. - */ - ['t']: 'c'; - - /** - * Value: The offset into the current rule's context to be matched. - */ - ['c']: number; // For 'c'ontext statement. - } - - class ContextNul { - /** Discriminant field - 'n' for `nul` - */ - ['t']: 'n'; - } - - class StoreBeep { - /** Discriminant field - 'b' for `beep` - */ - ['t']: 'b'; - } - - type ContextNonCharEntry = RuleDeadkey | ContextAny | RuleIndex | ContextEx | ContextNul; - type ContextEntry = RuleChar | ContextNonCharEntry; - - type StoreNonCharEntry = RuleDeadkey | StoreBeep; - - /** - * Cache of context storing and retrieving return values from KC - * Must be reset prior to each keystroke and after any text changes - * MCD 3/1/14 - **/ - class CachedContext { - _cache: string[][]; - - reset(): void { - this._cache = []; - } - - get(n: number, ln: number): string { - // return null; // uncomment this line to disable context caching - if(typeof this._cache[n] == 'undefined') { - return null; - } else if(typeof this._cache[n][ln] == 'undefined') { - return null; - } - return this._cache[n][ln]; - } - - set(n: number, ln: number, val: string): void { - if(typeof this._cache[n] == 'undefined') { - this._cache[n] = []; - } - this._cache[n][ln] = val; - } - }; - - type CachedExEntry = {valContext: (string|number)[], deadContext: text.Deadkey[]}; - /** - * An extended version of cached context storing designed to work with - * `fullContextMatch` and its helper functions. +class RuleDeadkey { + /** Discriminant field - 'd' for Deadkey. */ - class CachedContextEx { - _cache: CachedExEntry[][]; + ['t']: 'd'; - reset(): void { - this._cache = []; + /** + * Value: the deadkey's ID. + */ + ['d']: number; // For 'd'eadkey; also reflects the Deadkey class's 'd' property. +} + +class ContextAny { + /** Discriminant field - 'a' for `any()`. + */ + ['t']: 'a'; + + /** + * Value: the store to search. + */ + ['a']: KeyboardStore; // For 'a'ny statement. + + /** + * If set to true, negates the 'any'. + */ + ['n']: boolean|0|1; +} + +class RuleIndex { + /** Discriminant field - 'i' for `index()`. + */ + ['t']: 'i'; + + /** + * Value: the Store from which to output + */ + ['i']: KeyboardStore; + + /** + * Offset: the offset in context for the corresponding `any()`. + */ + ['o']: number; +} + +class ContextEx { + /** Discriminant field - 'c' for `context()`. + */ + ['t']: 'c'; + + /** + * Value: The offset into the current rule's context to be matched. + */ + ['c']: number; // For 'c'ontext statement. +} + +class ContextNul { + /** Discriminant field - 'n' for `nul` + */ + ['t']: 'n'; +} + +class StoreBeep { + /** Discriminant field - 'b' for `beep` + */ + ['t']: 'b'; +} + +type ContextNonCharEntry = RuleDeadkey | ContextAny | RuleIndex | ContextEx | ContextNul; +type ContextEntry = RuleChar | ContextNonCharEntry; + +type StoreNonCharEntry = RuleDeadkey | StoreBeep; + +/** + * Cache of context storing and retrieving return values from KC + * Must be reset prior to each keystroke and after any text changes + * MCD 3/1/14 + **/ +class CachedContext { + _cache: string[][]; + + reset(): void { + this._cache = []; + } + + get(n: number, ln: number): string { + // return null; // uncomment this line to disable context caching + if(typeof this._cache[n] == 'undefined') { + return null; + } else if(typeof this._cache[n][ln] == 'undefined') { + return null; + } + return this._cache[n][ln]; + } + + set(n: number, ln: number, val: string): void { + if(typeof this._cache[n] == 'undefined') { + this._cache[n] = []; + } + this._cache[n][ln] = val; + } +}; + +type CachedExEntry = {valContext: (string|number)[], deadContext: Deadkey[]}; +/** + * An extended version of cached context storing designed to work with + * `fullContextMatch` and its helper functions. + */ +class CachedContextEx { + _cache: CachedExEntry[][]; + + reset(): void { + this._cache = []; + } + + get(n: number, ln: number): CachedExEntry { + // return null; // uncomment this line to disable context caching + if(typeof this._cache[n] == 'undefined') { + return null; + } else if(typeof this._cache[n][ln] == 'undefined') { + return null; + } + return this._cache[n][ln]; + } + + set(n: number, ln: number, val: CachedExEntry): void { + if(typeof this._cache[n] == 'undefined') { + this._cache[n] = []; + } + this._cache[n][ln] = val; + } + + clone(): CachedContextEx { + let r = new CachedContextEx(); + r._cache = this._cache; + return r; + } +}; + +export enum SystemStoreIDs { + TSS_LAYER = 33, + TSS_PLATFORM = 31, + TSS_NEWLAYER = 42, + TSS_OLDLAYER = 43 +} + +//#endregion + +export default class KeyboardInterface { + static readonly GLOBAL_NAME = 'KeymanWeb'; + + cachedContext: CachedContext = new CachedContext(); + cachedContextEx: CachedContextEx = new CachedContextEx(); + ruleContextEx: CachedContextEx; + + activeTargetOutput: OutputTarget; + ruleBehavior: RuleBehavior; + + systemStores: {[storeID: number]: SystemStore}; + + _AnyIndices: number[] = []; // AnyIndex - array of any/index match indices + + // Must be accessible to some of the keyboard API methods. + activeKeyboard: Keyboard; + activeDevice: DeviceSpec; + + variableStoreSerializer?: VariableStoreSerializer; + + constructor(variableStoreSerializer: VariableStoreSerializer = null) { + this.systemStores = {}; + + this.systemStores[SystemStoreIDs.TSS_PLATFORM] = new PlatformSystemStore(this); + this.systemStores[SystemStoreIDs.TSS_LAYER] = new MutableSystemStore(SystemStoreIDs.TSS_LAYER, 'default'); + this.systemStores[SystemStoreIDs.TSS_NEWLAYER] = new MutableSystemStore(SystemStoreIDs.TSS_NEWLAYER, ''); + this.systemStores[SystemStoreIDs.TSS_OLDLAYER] = new MutableSystemStore(SystemStoreIDs.TSS_OLDLAYER, ''); + + this.variableStoreSerializer = variableStoreSerializer; + } + + /** + * Function KSF + * Scope Public + * + * Saves the document's current focus settings on behalf of the keyboard. Often paired with insertText. + */ + saveFocus(): void { } + + /** + * A text-insertion method used by custom OSKs for helpHTML interaction, like with sil_euro_latin. + * + * This function currently bypasses web-core's standard text handling control path and all predictive text processing. + * It also has DOM-dependencies that help ensure KMW's active OutputTarget retains focus during use. + */ + insertText?: (Ptext: string, PdeadKey: number) => boolean; + + /** + * Function registerKeyboard KR + * Scope Public + * @param {Object} Pk Keyboard object + * Description Registers a keyboard with KeymanWeb once its script has fully loaded. + * + * In web-core, this also activates the keyboard; in other modules, this method + * may be replaced with other implementations. + */ + registerKeyboard(Pk): void { + // NOTE: This implementation is web-core specific and is intentionally replaced, whole-sale, + // by DOM-aware code. + let keyboard = new Keyboard(Pk); + this.activeKeyboard = keyboard; + } + + /** + * Used by DOM-aware KeymanWeb to add keyboard stubs, used by the `KeyboardManager` type + * to optimize resource use. + */ + registerStub?: (Pstub) => number; + + /** + * Get *cached or uncached* keyboard context for a specified range, relative to caret + * + * @param {number} n Number of characters to move back from caret + * @param {number} ln Number of characters to return + * @param {Object} Pelem Element to work with (must be currently focused element) + * @return {string} Context string + * + * Example [abcdef|ghi] as INPUT, with the caret position marked by |: + * KC(2,1,Pelem) == "e" + * KC(3,3,Pelem) == "def" + * KC(10,10,Pelem) == "abcdef" i.e. return as much as possible of the requested string + */ + + context(n: number, ln: number, outputTarget: OutputTarget): string { + var v = this.cachedContext.get(n, ln); + if(v !== null) { + return v; } - get(n: number, ln: number): CachedExEntry { - // return null; // uncomment this line to disable context caching - if(typeof this._cache[n] == 'undefined') { - return null; - } else if(typeof this._cache[n][ln] == 'undefined') { - return null; + var r = this.KC_(n, ln, outputTarget); + this.cachedContext.set(n, ln, r); + return r; + } + + /** + * Get (uncached) keyboard context for a specified range, relative to caret + * + * @param {number} n Number of characters to move back from caret + * @param {number} ln Number of characters to return + * @param {Object} Pelem Element to work with (must be currently focused element) + * @return {string} Context string + * + * Example [abcdef|ghi] as INPUT, with the caret position marked by |: + * KC(2,1,Pelem) == "e" + * KC(3,3,Pelem) == "def" + * KC(10,10,Pelem) == "XXXXabcdef" i.e. return as much as possible of the requested string, where X = \uFFFE + */ + private KC_(n: number, ln: number, outputTarget: OutputTarget): string { + var tempContext = ''; + + // If we have a selection, we have an empty context + tempContext = outputTarget.isSelectionEmpty() ? outputTarget.getTextBeforeCaret() : ""; + + if(tempContext._kmwLength() < n) { + tempContext = Array(n-tempContext._kmwLength()+1).join("\uFFFE") + tempContext; + } + + return tempContext._kmwSubstr(-n)._kmwSubstr(0,ln); + } + + /** + * Function nul KN + * Scope Public + * @param {number} n Length of context to check + * @param {Object} Ptarg Element to work with (must be currently focused element) + * @return {boolean} True if length of context is less than or equal to n + * Description Test length of context, return true if the length of the context is less than or equal to n + * + * Example [abc|def] as INPUT, with the caret position marked by |: + * KN(3,Pelem) == TRUE + * KN(2,Pelem) == FALSE + * KN(4,Pelem) == TRUE + */ + nul(n: number, outputTarget: OutputTarget): boolean { + var cx=this.context(n+1, 1, outputTarget); + + // With #31, the result will be a replacement character if context is empty. + return cx === "\uFFFE"; + } + + /** + * Function contextMatch KCM + * Scope Public + * @param {number} n Number of characters to move back from caret + * @param {Object} Ptarg Focused element + * @param {string} val String to match + * @param {number} ln Number of characters to return + * @return {boolean} True if selected context matches val + * Description Test keyboard context for match + */ + contextMatch(n: number, outputTarget: OutputTarget, val: string, ln: number): boolean { + var cx=this.context(n, ln, outputTarget); + if(cx === val) { + return true; // I3318 + } + outputTarget.deadkeys().resetMatched(); // I3318 + return false; + } + + /** + * Builds the *cached or uncached* keyboard context for a specified range, relative to caret + * + * @param {number} n Number of characters to move back from caret + * @param {number} ln Number of characters to return + * @param {Object} Pelem Element to work with (must be currently focused element) + * @return {Array} Context array (of strings and numbers) + */ + private _BuildExtendedContext(n: number, ln: number, outputTarget: OutputTarget): CachedExEntry { + var cache: CachedExEntry = this.cachedContextEx.get(n, ln); + if(cache !== null) { + return cache; + } else { + // By far the easiest way to correctly build what we want is to start from the right and work to what we need. + // We may have done it for a similar cursor position before. + cache = this.cachedContextEx.get(n, n); + if(cache === null) { + // First, let's make sure we have a cloned, sorted copy of the deadkey array. + let unmatchedDeadkeys = outputTarget.deadkeys().toSortedArray(); // Is reverse-order sorted for us already. + + // Time to build from scratch! + var index = 0; + cache = { valContext: [], deadContext: []}; + while(cache.valContext.length < n) { + // As adapted from `deadkeyMatch`. + var sp = outputTarget.getDeadkeyCaret(); + var deadPos = sp - index; + if(unmatchedDeadkeys.length > 0 && unmatchedDeadkeys[0].p > deadPos) { + // We have deadkeys at the right-hand side of the caret! They don't belong in the context, so pop 'em off. + unmatchedDeadkeys.splice(0, 1); + continue; + } else if(unmatchedDeadkeys.length > 0 && unmatchedDeadkeys[0].p == deadPos) { + // Take the deadkey. + cache.deadContext[n-cache.valContext.length-1] = unmatchedDeadkeys[0]; + cache.valContext = ([unmatchedDeadkeys[0].d] as (string|number)[]).concat(cache.valContext); + unmatchedDeadkeys.splice(0, 1); + } else { + // Take the character. We get "\ufffe" if it doesn't exist. + var kc = this.context(++index, 1, outputTarget); + cache.valContext = ([kc] as (string|number)[]).concat(cache.valContext); + } + } + this.cachedContextEx.set(n, n, cache); } - return this._cache[n][ln]; - } - set(n: number, ln: number, val: CachedExEntry): void { - if(typeof this._cache[n] == 'undefined') { - this._cache[n] = []; + // Now that we have the cache... + var subCache = cache; + subCache.valContext = subCache.valContext.slice(0, ln); + for(var i=0; i < subCache.valContext.length; i++) { + if(subCache[i] == '\ufffe') { + subCache.valContext.splice(0, 1); + subCache.deadContext.splice(0, 1); + } } - this._cache[n][ln] = val; + + if(subCache.valContext.length == 0) { + subCache.valContext = ['\ufffe']; + subCache.deadContext = []; + } + + this.cachedContextEx.set(n, ln, subCache); + + return subCache; + } + } + + /** + * Function fullContextMatch KFCM + * Scope Private + * @param {number} n Number of characters to move back from caret + * @param {Object} Ptarg Focused element + * @param {Array} rule An array of ContextEntries to match. + * @return {boolean} True if the fully-specified rule context matches the current KMW state. + * + * A KMW 10+ function designed to bring KMW closer to Keyman Desktop functionality, + * near-directly modeling (externally) the compiled form of Desktop rules' context section. + */ + fullContextMatch(n: number, outputTarget: OutputTarget, rule: ContextEntry[]): boolean { + // Stage one: build the context index map. + var fullContext = this._BuildExtendedContext(n, rule.length, outputTarget); + this.ruleContextEx = this.cachedContextEx.clone(); + var context = fullContext.valContext; + var deadContext = fullContext.deadContext; + + var mismatch = false; + + // This symbol internally indicates lack of context in a position. (See KC_) + const NUL_CONTEXT = "\uFFFE"; + + var assertNever = function(x: never): never { + // Could be accessed by improperly handwritten calls to `fullContextMatch`. + throw new Error("Unexpected object in fullContextMatch specification: " + x); } - clone(): CachedContextEx { - let r = new CachedContextEx(); - r._cache = this._cache; - return r; + // Stage two: time to match against the rule specified. + for(var i=0; i < rule.length; i++) { + if(typeof rule[i] == 'string') { + var str = rule[i] as string; + if(str !== context[i]) { + mismatch = true; + break; + } + } else { + // TypeScript needs a cast to this intermediate type to do its discriminated union magic. + var r = rule[i] as ContextNonCharEntry; + switch(r.t) { + case 'd': + // We still need to set a flag here; + if(r['d'] !== context[i]) { + mismatch = true; + } else { + deadContext[i].set(); + } + break; + case 'a': + var lookup: KeyboardStoreElement; + + if(typeof context[i] == 'string') { + lookup = context[i] as string; + } else { + lookup = {'t': 'd', 'd': context[i] as number}; + } + + var result = this.any(i, lookup, r.a); + + if(!r.n) { // If it's a standard 'any'... + if(!result) { + mismatch = true; + } else if(deadContext[i] !== undefined) { + // It's a deadkey match, so indicate that. + deadContext[i].set(); + } + // 'n' for 'notany'. + // - if `result === true`, `any` would match: this should thus fail. + // - if `context[i] === NUL_CONTEXT`, `notany` should not match. + } else if(r.n && (result || context[i] === NUL_CONTEXT)) { + mismatch = true; + } + break; + case 'i': + // The context will never hold a 'beep.' + var ch = this._Index(r.i, r.o) as string | RuleDeadkey; + + if(ch !== undefined && (typeof(ch) == 'string' ? ch : ch.d) !== context[i]) { + mismatch = true; + } else if(deadContext[i] !== undefined) { + deadContext[i].set(); + } + break; + case 'c': + if(context[r.c - 1] !== context[i]) { + mismatch = true; + } else if(deadContext[i] !== undefined) { + deadContext[i].set(); + } + break; + case 'n': + // \uFFFE is the internal 'no context here sentinel'. + if(context[i] != NUL_CONTEXT) { + mismatch = true; + } + break; + default: + assertNever(r); + } + } } + + if(mismatch) { + // Reset the matched 'any' indices, if any. + outputTarget.deadkeys().resetMatched(); + this._AnyIndices = []; + } + + return !mismatch; + } + + /** + * Function KIK + * Scope Public + * @param {Object} e keystroke event + * @return {boolean} true if keypress event + * Description Test if event as a keypress event + */ + isKeypress(e: KeyEvent): boolean { + if(this.activeKeyboard.isMnemonic) { // I1380 - support KIK for positional layouts + return !e.LisVirtualKey; // will now return true for U_xxxx keys, but not for T_xxxx keys + } else { + return KeyMapping._USKeyCodeToCharCode(e) ? true : false; // I1380 - support KIK for positional layouts + } + } + + /** + * Maps a KeyEvent's modifiers to their appropriate value for key-rule evaluation + * based on the rule's specified target modifier set. + * + * Mostly used to correct chiral OSK-keys targeting non-chiral rules. + * @param e The source KeyEvent + * @returns + */ + private static matchModifiersToRuleChirality(eventModifiers: number, targetModifierMask: number): number { + const CHIRAL_ALT = Codes.modifierCodes["LALT"] | Codes.modifierCodes["RALT"]; + const CHIRAL_CTRL = Codes.modifierCodes["LCTRL"] | Codes.modifierCodes["RCTRL"]; + + let modifiers = eventModifiers; + + // If the target rule does not use chiral alt... + if(!(targetModifierMask & CHIRAL_ALT)) { + const altIntersection = modifiers & CHIRAL_ALT; + + if(altIntersection) { + // Undo the chiral part and replace with non-chiral. + modifiers ^= altIntersection | Codes.modifierCodes["ALT"]; + } + } + + // If the target rule does not use chiral ctrl... + if(!(targetModifierMask & CHIRAL_CTRL)) { + const ctrlIntersection = modifiers & CHIRAL_CTRL; + + if(ctrlIntersection) { + // Undo the chiral part and replace with non-chiral. + modifiers ^= ctrlIntersection | Codes.modifierCodes["CTRL"]; + } + } + + return modifiers; + } + + /** + * Function keyMatch KKM + * Scope Public + * @param {Object} e keystroke event + * @param {number} Lruleshift + * @param {number} Lrulekey + * @return {boolean} True if key matches rule + * Description Test keystroke with modifiers against rule + */ + keyMatch(e: KeyEvent, Lruleshift:number, Lrulekey:number): boolean { + var retVal = false; // I3318 + var keyCode = (e.Lcode == 173 ? 189 : e.Lcode); //I3555 (Firefox hyphen issue) + + let bitmask = this.activeKeyboard.modifierBitmask; + var modifierBitmask = bitmask & Codes.modifierBitmasks["ALL"]; + var stateBitmask = bitmask & Codes.stateBitmasks["ALL"]; + + const eventModifiers = KeyboardInterface.matchModifiersToRuleChirality(e.Lmodifiers, Lruleshift); + + if(e.vkCode > 255) { + keyCode = e.vkCode; // added to support extended (touch-hold) keys for mnemonic layouts + } + + if(e.LisVirtualKey || keyCode > 255) { + if((Lruleshift & 0x4000) == 0x4000 || (keyCode > 255)) { // added keyCode test to support extended keys + retVal = ((Lrulekey == keyCode) && ((Lruleshift & modifierBitmask) == eventModifiers)); //I3318, I3555 + retVal = retVal && this.stateMatch(e, Lruleshift & stateBitmask); + } + } else if((Lruleshift & 0x4000) == 0) { + retVal = (keyCode == Lrulekey); // I3318, I3555 + } + if(!retVal) { + this.activeTargetOutput.deadkeys().resetMatched(); // I3318 + } + return retVal; // I3318 }; - //#endregion + /** + * Function stateMatch KSM + * Scope Public + * @param {Object} e keystroke event + * @param {number} Lstate + * Description Test keystroke against state key rules + */ + stateMatch(e: KeyEvent, Lstate: number) { + return ((Lstate & e.Lstates) == Lstate); + } - export class KeyboardInterface { - static readonly GLOBAL_NAME = 'KeymanWeb'; + /** + * Function keyInformation KKI + * Scope Public + * @param {Object} e + * @return {Object} Object with event's virtual key flag, key code, and modifiers + * Description Get object with extended key event information + */ + keyInformation(e: KeyEvent): KeyInformation { + var ei = new KeyInformation(); + ei['vk'] = e.LisVirtualKey; + ei['code'] = e.Lcode; + ei['modifiers'] = e.Lmodifiers; + return ei; + }; - cachedContext: CachedContext = new CachedContext(); - cachedContextEx: CachedContextEx = new CachedContextEx(); - ruleContextEx: CachedContextEx; + /** + * Function deadkeyMatch KDM + * Scope Public + * @param {number} n offset from current cursor position + * @param {Object} Ptarg target element + * @param {number} d deadkey + * @return {boolean} True if deadkey found selected context matches val + * Description Match deadkey at current cursor position + */ + deadkeyMatch(n: number, outputTarget: OutputTarget, d: number): boolean { + return outputTarget.hasDeadkeyMatch(n, d); + } - activeTargetOutput: OutputTarget; - ruleBehavior: RuleBehavior; + /** + * Function beep KB + * Scope Public + * @param {Object} Pelem element to flash + * Description Flash body as substitute for audible beep; notify embedded device to vibrate + */ + beep(outputTarget: OutputTarget): void { + this.resetContextCache(); - static readonly TSS_LAYER: number = 33; - static readonly TSS_PLATFORM: number = 31; - static readonly TSS_NEWLAYER: number = 42; - static readonly TSS_OLDLAYER: number = 43; + // Denote as part of the matched rule's behavior. + this.ruleBehavior.beep = true; + } - systemStores: {[storeID: number]: SystemStore}; + _ExplodeStore(store: KeyboardStore): ComplexKeyboardStore { + if(typeof(store) == 'string') { + let cachedStores = this.activeKeyboard.explodedStores; - _AnyIndices: number[] = []; // AnyIndex - array of any/index match indices - - // Must be accessible to some of the keyboard API methods. - activeKeyboard: keyboards.Keyboard; - activeDevice: utils.DeviceSpec; - - variableStoreSerializer?: VariableStoreSerializer; - - constructor(variableStoreSerializer: VariableStoreSerializer = null) { - this.systemStores = {}; - - this.systemStores[KeyboardInterface.TSS_PLATFORM] = new PlatformSystemStore(this); - this.systemStores[KeyboardInterface.TSS_LAYER] = new MutableSystemStore(KeyboardInterface.TSS_LAYER, 'default'); - this.systemStores[KeyboardInterface.TSS_NEWLAYER] = new MutableSystemStore(KeyboardInterface.TSS_NEWLAYER, ''); - this.systemStores[KeyboardInterface.TSS_OLDLAYER] = new MutableSystemStore(KeyboardInterface.TSS_OLDLAYER, ''); - - this.variableStoreSerializer = variableStoreSerializer; - } - - /** - * Function KSF - * Scope Public - * - * Saves the document's current focus settings on behalf of the keyboard. Often paired with insertText. - */ - saveFocus(): void { } - - /** - * A text-insertion method used by custom OSKs for helpHTML interaction, like with sil_euro_latin. - * - * This function currently bypasses web-core's standard text handling control path and all predictive text processing. - * It also has DOM-dependencies that help ensure KMW's active OutputTarget retains focus during use. - */ - insertText?: (Ptext: string, PdeadKey: number) => boolean; - - /** - * Function registerKeyboard KR - * Scope Public - * @param {Object} Pk Keyboard object - * Description Registers a keyboard with KeymanWeb once its script has fully loaded. - * - * In web-core, this also activates the keyboard; in other modules, this method - * may be replaced with other implementations. - */ - registerKeyboard(Pk): void { - // NOTE: This implementation is web-core specific and is intentionally replaced, whole-sale, - // by DOM-aware code. - let keyboard = new keyboards.Keyboard(Pk); - this.activeKeyboard = keyboard; - } - - /** - * Used by DOM-aware KeymanWeb to add keyboard stubs, used by the `KeyboardManager` type - * to optimize resource use. - */ - registerStub?: (Pstub) => number; - - /** - * Get *cached or uncached* keyboard context for a specified range, relative to caret - * - * @param {number} n Number of characters to move back from caret - * @param {number} ln Number of characters to return - * @param {Object} Pelem Element to work with (must be currently focused element) - * @return {string} Context string - * - * Example [abcdef|ghi] as INPUT, with the caret position marked by |: - * KC(2,1,Pelem) == "e" - * KC(3,3,Pelem) == "def" - * KC(10,10,Pelem) == "abcdef" i.e. return as much as possible of the requested string - */ - - context(n: number, ln: number, outputTarget: OutputTarget): string { - var v = this.cachedContext.get(n, ln); - if(v !== null) { - return v; + // Is the result cached? + if(cachedStores[store]) { + return cachedStores[store]; } - var r = this.KC_(n, ln, outputTarget); - this.cachedContext.set(n, ln, r); - return r; - } - - /** - * Get (uncached) keyboard context for a specified range, relative to caret - * - * @param {number} n Number of characters to move back from caret - * @param {number} ln Number of characters to return - * @param {Object} Pelem Element to work with (must be currently focused element) - * @return {string} Context string - * - * Example [abcdef|ghi] as INPUT, with the caret position marked by |: - * KC(2,1,Pelem) == "e" - * KC(3,3,Pelem) == "def" - * KC(10,10,Pelem) == "XXXXabcdef" i.e. return as much as possible of the requested string, where X = \uFFFE - */ - private KC_(n: number, ln: number, outputTarget: OutputTarget): string { - var tempContext = ''; - - // If we have a selection, we have an empty context - tempContext = outputTarget.isSelectionEmpty() ? outputTarget.getTextBeforeCaret() : ""; - - if(tempContext._kmwLength() < n) { - tempContext = Array(n-tempContext._kmwLength()+1).join("\uFFFE") + tempContext; + // Nope, so let's build its cache. + var result: ComplexKeyboardStore = []; + for(var i=0; i < store._kmwLength(); i++) { + result.push(store._kmwCharAt(i)); } - return tempContext._kmwSubstr(-n)._kmwSubstr(0,ln); + // Cache the result for later! + cachedStores[store] = result; + return result; + } else { + return store; } + } - /** - * Function nul KN - * Scope Public - * @param {number} n Length of context to check - * @param {Object} Ptarg Element to work with (must be currently focused element) - * @return {boolean} True if length of context is less than or equal to n - * Description Test length of context, return true if the length of the context is less than or equal to n - * - * Example [abc|def] as INPUT, with the caret position marked by |: - * KN(3,Pelem) == TRUE - * KN(2,Pelem) == FALSE - * KN(4,Pelem) == TRUE - */ - nul(n: number, outputTarget: OutputTarget): boolean { - var cx=this.context(n+1, 1, outputTarget); - - // With #31, the result will be a replacement character if context is empty. - return cx === "\uFFFE"; - } - - /** - * Function contextMatch KCM - * Scope Public - * @param {number} n Number of characters to move back from caret - * @param {Object} Ptarg Focused element - * @param {string} val String to match - * @param {number} ln Number of characters to return - * @return {boolean} True if selected context matches val - * Description Test keyboard context for match - */ - contextMatch(n: number, outputTarget: OutputTarget, val: string, ln: number): boolean { - var cx=this.context(n, ln, outputTarget); - if(cx === val) { - return true; // I3318 - } - outputTarget.deadkeys().resetMatched(); // I3318 + /** + * Function any KA + * Scope Public + * @param {number} n character position (index) + * @param {string} ch character to find in string + * @param {string} s 'any' string + * @return {boolean} True if character found in 'any' string, sets index accordingly + * Description Test for character matching + */ + any(n: number, ch: KeyboardStoreElement, s: KeyboardStore): boolean { + if(ch == '') { return false; } - /** - * Builds the *cached or uncached* keyboard context for a specified range, relative to caret - * - * @param {number} n Number of characters to move back from caret - * @param {number} ln Number of characters to return - * @param {Object} Pelem Element to work with (must be currently focused element) - * @return {Array} Context array (of strings and numbers) - */ - private _BuildExtendedContext(n: number, ln: number, outputTarget: OutputTarget): CachedExEntry { - var cache: CachedExEntry = this.cachedContextEx.get(n, ln); - if(cache !== null) { - return cache; - } else { - // By far the easiest way to correctly build what we want is to start from the right and work to what we need. - // We may have done it for a similar cursor position before. - cache = this.cachedContextEx.get(n, n); - if(cache === null) { - // First, let's make sure we have a cloned, sorted copy of the deadkey array. - let unmatchedDeadkeys = outputTarget.deadkeys().toSortedArray(); // Is reverse-order sorted for us already. - - // Time to build from scratch! - var index = 0; - cache = { valContext: [], deadContext: []}; - while(cache.valContext.length < n) { - // As adapted from `deadkeyMatch`. - var sp = outputTarget.getDeadkeyCaret(); - var deadPos = sp - index; - if(unmatchedDeadkeys.length > 0 && unmatchedDeadkeys[0].p > deadPos) { - // We have deadkeys at the right-hand side of the caret! They don't belong in the context, so pop 'em off. - unmatchedDeadkeys.splice(0, 1); - continue; - } else if(unmatchedDeadkeys.length > 0 && unmatchedDeadkeys[0].p == deadPos) { - // Take the deadkey. - cache.deadContext[n-cache.valContext.length-1] = unmatchedDeadkeys[0]; - cache.valContext = ([unmatchedDeadkeys[0].d] as (string|number)[]).concat(cache.valContext); - unmatchedDeadkeys.splice(0, 1); - } else { - // Take the character. We get "\ufffe" if it doesn't exist. - var kc = this.context(++index, 1, outputTarget); - cache.valContext = ([kc] as (string|number)[]).concat(cache.valContext); - } - } - this.cachedContextEx.set(n, n, cache); - } - - // Now that we have the cache... - var subCache = cache; - subCache.valContext = subCache.valContext.slice(0, ln); - for(var i=0; i < subCache.valContext.length; i++) { - if(subCache[i] == '\ufffe') { - subCache.valContext.splice(0, 1); - subCache.deadContext.splice(0, 1); - } - } - - if(subCache.valContext.length == 0) { - subCache.valContext = ['\ufffe']; - subCache.deadContext = []; - } - - this.cachedContextEx.set(n, ln, subCache); - - return subCache; - } - } - - /** - * Function fullContextMatch KFCM - * Scope Private - * @param {number} n Number of characters to move back from caret - * @param {Object} Ptarg Focused element - * @param {Array} rule An array of ContextEntries to match. - * @return {boolean} True if the fully-specified rule context matches the current KMW state. - * - * A KMW 10+ function designed to bring KMW closer to Keyman Desktop functionality, - * near-directly modeling (externally) the compiled form of Desktop rules' context section. - */ - fullContextMatch(n: number, outputTarget: OutputTarget, rule: ContextEntry[]): boolean { - // Stage one: build the context index map. - var fullContext = this._BuildExtendedContext(n, rule.length, outputTarget); - this.ruleContextEx = this.cachedContextEx.clone(); - var context = fullContext.valContext; - var deadContext = fullContext.deadContext; - - var mismatch = false; - - // This symbol internally indicates lack of context in a position. (See KC_) - const NUL_CONTEXT = "\uFFFE"; - - var assertNever = function(x: never): never { - // Could be accessed by improperly handwritten calls to `fullContextMatch`. - throw new Error("Unexpected object in fullContextMatch specification: " + x); - } - - // Stage two: time to match against the rule specified. - for(var i=0; i < rule.length; i++) { - if(typeof rule[i] == 'string') { - var str = rule[i] as string; - if(str !== context[i]) { - mismatch = true; - break; - } - } else { - // TypeScript needs a cast to this intermediate type to do its discriminated union magic. - var r = rule[i] as ContextNonCharEntry; - switch(r.t) { - case 'd': - // We still need to set a flag here; - if(r['d'] !== context[i]) { - mismatch = true; - } else { - deadContext[i].set(); - } - break; - case 'a': - var lookup: KeyboardStoreElement; - - if(typeof context[i] == 'string') { - lookup = context[i] as string; - } else { - lookup = {'t': 'd', 'd': context[i] as number}; - } - - var result = this.any(i, lookup, r.a); - - if(!r.n) { // If it's a standard 'any'... - if(!result) { - mismatch = true; - } else if(deadContext[i] !== undefined) { - // It's a deadkey match, so indicate that. - deadContext[i].set(); - } - // 'n' for 'notany'. - // - if `result === true`, `any` would match: this should thus fail. - // - if `context[i] === NUL_CONTEXT`, `notany` should not match. - } else if(r.n && (result || context[i] === NUL_CONTEXT)) { - mismatch = true; - } - break; - case 'i': - // The context will never hold a 'beep.' - var ch = this._Index(r.i, r.o) as string | RuleDeadkey; - - if(ch !== undefined && (typeof(ch) == 'string' ? ch : ch.d) !== context[i]) { - mismatch = true; - } else if(deadContext[i] !== undefined) { - deadContext[i].set(); - } - break; - case 'c': - if(context[r.c - 1] !== context[i]) { - mismatch = true; - } else if(deadContext[i] !== undefined) { - deadContext[i].set(); - } - break; - case 'n': - // \uFFFE is the internal 'no context here sentinel'. - if(context[i] != NUL_CONTEXT) { - mismatch = true; - } - break; - default: - assertNever(r); - } - } - } - - if(mismatch) { - // Reset the matched 'any' indices, if any. - outputTarget.deadkeys().resetMatched(); - this._AnyIndices = []; - } - - return !mismatch; - } - - /** - * Function KIK - * Scope Public - * @param {Object} e keystroke event - * @return {boolean} true if keypress event - * Description Test if event as a keypress event - */ - isKeypress(e: KeyEvent): boolean { - if(this.activeKeyboard.isMnemonic) { // I1380 - support KIK for positional layouts - return !e.LisVirtualKey; // will now return true for U_xxxx keys, but not for T_xxxx keys - } else { - return KeyMapping._USKeyCodeToCharCode(e) ? true : false; // I1380 - support KIK for positional layouts - } - } - - /** - * Maps a KeyEvent's modifiers to their appropriate value for key-rule evaluation - * based on the rule's specified target modifier set. - * - * Mostly used to correct chiral OSK-keys targeting non-chiral rules. - * @param e The source KeyEvent - * @returns - */ - private static matchModifiersToRuleChirality(eventModifiers: number, targetModifierMask: number): number { - const CHIRAL_ALT = Codes.modifierCodes["LALT"] | Codes.modifierCodes["RALT"]; - const CHIRAL_CTRL = Codes.modifierCodes["LCTRL"] | Codes.modifierCodes["RCTRL"]; - - let modifiers = eventModifiers; - - // If the target rule does not use chiral alt... - if(!(targetModifierMask & CHIRAL_ALT)) { - const altIntersection = modifiers & CHIRAL_ALT; - - if(altIntersection) { - // Undo the chiral part and replace with non-chiral. - modifiers ^= altIntersection | Codes.modifierCodes["ALT"]; - } - } - - // If the target rule does not use chiral ctrl... - if(!(targetModifierMask & CHIRAL_CTRL)) { - const ctrlIntersection = modifiers & CHIRAL_CTRL; - - if(ctrlIntersection) { - // Undo the chiral part and replace with non-chiral. - modifiers ^= ctrlIntersection | Codes.modifierCodes["CTRL"]; - } - } - - return modifiers; - } - - /** - * Function keyMatch KKM - * Scope Public - * @param {Object} e keystroke event - * @param {number} Lruleshift - * @param {number} Lrulekey - * @return {boolean} True if key matches rule - * Description Test keystroke with modifiers against rule - */ - keyMatch(e: KeyEvent, Lruleshift:number, Lrulekey:number): boolean { - var retVal = false; // I3318 - var keyCode = (e.Lcode == 173 ? 189 : e.Lcode); //I3555 (Firefox hyphen issue) - - let bitmask = this.activeKeyboard.modifierBitmask; - let Codes = com.keyman.text.Codes; - var modifierBitmask = bitmask & Codes.modifierBitmasks["ALL"]; - var stateBitmask = bitmask & Codes.stateBitmasks["ALL"]; - - const eventModifiers = KeyboardInterface.matchModifiersToRuleChirality(e.Lmodifiers, Lruleshift); - - if(e.vkCode > 255) { - keyCode = e.vkCode; // added to support extended (touch-hold) keys for mnemonic layouts - } - - if(e.LisVirtualKey || keyCode > 255) { - if((Lruleshift & 0x4000) == 0x4000 || (keyCode > 255)) { // added keyCode test to support extended keys - retVal = ((Lrulekey == keyCode) && ((Lruleshift & modifierBitmask) == eventModifiers)); //I3318, I3555 - retVal = retVal && this.stateMatch(e, Lruleshift & stateBitmask); - } - } else if((Lruleshift & 0x4000) == 0) { - retVal = (keyCode == Lrulekey); // I3318, I3555 - } - if(!retVal) { - this.activeTargetOutput.deadkeys().resetMatched(); // I3318 - } - return retVal; // I3318 - }; - - /** - * Function stateMatch KSM - * Scope Public - * @param {Object} e keystroke event - * @param {number} Lstate - * Description Test keystroke against state key rules - */ - stateMatch(e: KeyEvent, Lstate: number) { - return ((Lstate & e.Lstates) == Lstate); - } - - /** - * Function keyInformation KKI - * Scope Public - * @param {Object} e - * @return {Object} Object with event's virtual key flag, key code, and modifiers - * Description Get object with extended key event information - */ - keyInformation(e: KeyEvent): KeyInformation { - var ei = new KeyInformation(); - ei['vk'] = e.LisVirtualKey; - ei['code'] = e.Lcode; - ei['modifiers'] = e.Lmodifiers; - return ei; - }; - - /** - * Function deadkeyMatch KDM - * Scope Public - * @param {number} n offset from current cursor position - * @param {Object} Ptarg target element - * @param {number} d deadkey - * @return {boolean} True if deadkey found selected context matches val - * Description Match deadkey at current cursor position - */ - deadkeyMatch(n: number, outputTarget: OutputTarget, d: number): boolean { - return outputTarget.hasDeadkeyMatch(n, d); - } - - /** - * Function beep KB - * Scope Public - * @param {Object} Pelem element to flash - * Description Flash body as substitute for audible beep; notify embedded device to vibrate - */ - beep(outputTarget: OutputTarget): void { - this.resetContextCache(); - - // Denote as part of the matched rule's behavior. - this.ruleBehavior.beep = true; - } - - _ExplodeStore(store: KeyboardStore): ComplexKeyboardStore { - if(typeof(store) == 'string') { - let cachedStores = this.activeKeyboard.explodedStores; - - // Is the result cached? - if(cachedStores[store]) { - return cachedStores[store]; - } - - // Nope, so let's build its cache. - var result: ComplexKeyboardStore = []; - for(var i=0; i < store._kmwLength(); i++) { - result.push(store._kmwCharAt(i)); - } - - // Cache the result for later! - cachedStores[store] = result; - return result; - } else { - return store; - } - } - - /** - * Function any KA - * Scope Public - * @param {number} n character position (index) - * @param {string} ch character to find in string - * @param {string} s 'any' string - * @return {boolean} True if character found in 'any' string, sets index accordingly - * Description Test for character matching - */ - any(n: number, ch: KeyboardStoreElement, s: KeyboardStore): boolean { - if(ch == '') { - return false; - } - - s = this._ExplodeStore(s); - var Lix = -1; - for(var i=0; i < s.length; i++) { - if(typeof(s[i]) == 'string') { - if(s[i] == ch) { - Lix = i; - break; - } - } else if(s[i]['d'] === ch['d']) { + s = this._ExplodeStore(s); + var Lix = -1; + for(var i=0; i < s.length; i++) { + if(typeof(s[i]) == 'string') { + if(s[i] == ch) { Lix = i; break; } - } - this._AnyIndices[n] = Lix; - return Lix >= 0; - } - - /** - * Function _Index - * Scope Public - * @param {string} Ps string - * @param {number} Pn index - * Description Returns the character from a store string according to the offset in the index array - */ - _Index(Ps: KeyboardStore, Pn: number): KeyboardStoreElement { - Ps = this._ExplodeStore(Ps); - - if(this._AnyIndices[Pn-1] < Ps.length) { //I3319 - return Ps[this._AnyIndices[Pn-1]]; - } else { - /* Should not be possible for a compiled keyboard, but may arise - * during the development of handwritten keyboards. - */ - console.warn("Unmatched contextual index() statement detected in rule with index " + Pn + "!"); - return ""; + } else if(s[i]['d'] === ch['d']) { + Lix = i; + break; } } + this._AnyIndices[n] = Lix; + return Lix >= 0; + } - /** - * Function indexOutput KIO - * Scope Public - * @param {number} Pdn no of character to overwrite (delete) - * @param {string} Ps string - * @param {number} Pn index - * @param {Object} Pelem element to output to - * Description Output a character selected from the string according to the offset in the index array - */ - indexOutput(Pdn: number, Ps: KeyboardStore, Pn: number, outputTarget: OutputTarget): void { - this.resetContextCache(); + /** + * Function _Index + * Scope Public + * @param {string} Ps string + * @param {number} Pn index + * Description Returns the character from a store string according to the offset in the index array + */ + _Index(Ps: KeyboardStore, Pn: number): KeyboardStoreElement { + Ps = this._ExplodeStore(Ps); - var assertNever = function(x: never): never { - // Could be accessed by improperly handwritten calls to `fullContextMatch`. - throw new Error("Unexpected object in fullContextMatch specification: " + x); - } - - var indexChar = this._Index(Ps, Pn); - if(indexChar !== "") { - if(typeof indexChar == 'string' ) { - this.output(Pdn, outputTarget, indexChar); //I3319 - } else if(indexChar['t']) { - var storeEntry = indexChar as StoreNonCharEntry; - - switch(storeEntry.t) { - case 'b': // Beep commands may appear within stores. - this.beep(outputTarget); - break; - case 'd': - this.deadkeyOutput(Pdn, outputTarget, indexChar['d']); - break; - default: - assertNever(storeEntry); - } - } else { // For keyboards developed during 10.0's alpha phase - t:'d' was assumed. - this.deadkeyOutput(Pdn, outputTarget, indexChar['d']); - } - } - } - - - /** - * Function deleteContext KDC - * Scope Public - * @param {number} dn number of context entries to overwrite - * @param {Object} Pelem element to output to - * @param {string} s string to output - * Description Keyboard output - */ - deleteContext(dn: number, outputTarget: OutputTarget): void { - var context: CachedExEntry; - - // We want to control exactly which deadkeys get removed. - if(dn > 0) { - context = this._BuildExtendedContext(dn, dn, outputTarget); - let nulCount = 0; - - for(var i=0; i < context.valContext.length; i++) { - var dk = context.deadContext[i]; - - if(dk) { - // Remove deadkey in context. - outputTarget.deadkeys().remove(dk); - - // Reduce our reported context size. - dn--; - } else if(context.valContext[i] == "\uFFFE") { - // Count any `nul` sentinels that would contribute to our deletion count. - nulCount++; - } - } - - // Prevent attempts to delete nul sentinels, as they don't exist in the actual context. - // (Addresses regression from KMW v 12.0 paired with Developer bug through same version) - let contextLength = context.valContext.length - nulCount; - if(dn > contextLength) { - dn = contextLength; - } - } - - // If a matched deadkey hasn't been deleted, we don't WANT to delete it. - outputTarget.deadkeys().resetMatched(); - - // Why reinvent the wheel? Delete the remaining characters by 'inserting a blank string'. - this.output(dn, outputTarget, ''); - } - - /** - * Function output KO - * Scope Public - * @param {number} dn number of characters to overwrite - * @param {Object} Pelem element to output to - * @param {string} s string to output - * Description Keyboard output - */ - output(dn: number, outputTarget: OutputTarget, s:string): void { - this.resetContextCache(); - - outputTarget.saveProperties(); - outputTarget.clearSelection(); - outputTarget.deadkeys().deleteMatched(); // I3318 - if(dn >= 0) { - // Automatically manages affected deadkey positions. Does not delete deadkeys b/c legacy behavior support. - outputTarget.deleteCharsBeforeCaret(dn); - } - // Automatically manages affected deadkey positions. - outputTarget.insertTextBeforeCaret(s); - outputTarget.restoreProperties(); - } - - /** - * `contextExOutput` function emits the character or object at `contextOffset` from the - * current matched rule's context. Introduced in Keyman 14.0, in order to resolve a - * gap between desktop and web core functionality for context(n) matching on notany(). - * See #917 for additional detail. - * @alias KCXO - * @public - * @param {number} Pdn number of characters to delete left of cursor - * @param {OutputTarget} outputTarget target to output to - * @param {number} contextLength length of current rule context to retrieve - * @param {number} contextOffset offset from start of current rule context, 1-based - */ - contextExOutput(Pdn: number, outputTarget: OutputTarget, contextLength: number, contextOffset: number): void { - this.resetContextCache(); - - if(Pdn >= 0) { - this.output(Pdn, outputTarget, ""); - } - - const context = this.ruleContextEx.get(contextLength, contextLength); - const dk = context.deadContext[contextOffset-1], vc = context.valContext[contextOffset-1]; - if(dk) { - outputTarget.insertDeadkeyBeforeCaret(dk.d); - } else if(typeof vc == 'string') { - this.output(-1, outputTarget, vc); - } else { - throw new Error("contextExOutput: should never be a numeric valContext with no corresponding deadContext"); - } - } - - /** - * Function deadkeyOutput KDO - * Scope Public - * @param {number} Pdn no of character to overwrite (delete) - * @param {Object} Pelem element to output to - * @param {number} Pd deadkey id - * Description Record a deadkey at current cursor position, deleting Pdn characters first - */ - deadkeyOutput(Pdn: number, outputTarget: OutputTarget, Pd: number): void { - this.resetContextCache(); - - if(Pdn >= 0) { - this.output(Pdn, outputTarget,""); //I3318 corrected to >= - } - - outputTarget.insertDeadkeyBeforeCaret(Pd); - // _DebugDeadKeys(Pelem, 'KDeadKeyOutput: dn='+Pdn+'; deadKey='+Pd); - } - - /** - * KIFS compares the content of a system store with a string value - * - * @param {number} systemId ID of the system store to test (only TSS_LAYER currently supported) - * @param {string} strValue String value to compare to - * @param {Object} Pelem Currently active element (may be needed by future tests) - * @return {boolean} True if the test succeeds - */ - ifStore(systemId: number, strValue: string, outputTarget: OutputTarget): boolean { - var result=true; - let store = this.systemStores[systemId]; - if(store) { - result = store.matches(strValue); - } - return result; //Moved from previous line, now supports layer selection, Build 350 - } - - /** - * KSETS sets the value of a system store to a string - * - * @param {number} systemId ID of the system store to set (only TSS_LAYER currently supported) - * @param {string} strValue String to set as the system store content - * @param {Object} Pelem Currently active element (may be needed in future tests) - * @return {boolean} True if command succeeds - * (i.e. for TSS_LAYER, if the layer is successfully selected) - * - * Note that option/variable stores are instead set within keyboard script code, as they only - * affect keyboard behavior. - */ - setStore(systemId: number, strValue: string, outputTarget: OutputTarget): boolean { - this.resetContextCache(); - // Unique case: we only allow set(&layer) ops from keyboard rules triggered by touch OSKs. - if(systemId == KeyboardInterface.TSS_LAYER && this.activeDevice.touchable) { - // Denote the changed store as part of the matched rule's behavior. - this.ruleBehavior.setStore[systemId] = strValue; - } else { - return false; - } - } - - /** - * Load an option store value from a cookie or default value - * - * @param {string} kbdName keyboard internal name - * @param {string} storeName store (option) name, embedded in cookie name - * @param {string} dfltValue default value - * @return {string} current or default option value - * - * This will only ever be called when the keyboard is loaded, as it is used by keyboards - * to initialize a store value on the keyboard's script object. - */ - loadStore(kbdName: string, storeName:string, dfltValue:string): string { - this.resetContextCache(); - if(this.variableStoreSerializer) { - let cValue = this.variableStoreSerializer.loadStore(kbdName, storeName); - return cValue[storeName] || dfltValue; - } else { - return dfltValue; - } - } - - /** - * Save an option store value to a cookie - * - * @param {string} storeName store (option) name, embedded in cookie name - * @param {string} optValue option value to save - * @return {boolean} true if save successful - * - * Note that a keyboard will freely manipulate the value of its variable stores on the - * script object within its own code. This function's use is merely to _persist_ that - * value across sessions, providing a custom user default for later uses of the keyboard. - */ - saveStore(storeName:string, optValue:string): boolean { - this.resetContextCache(); - var kbd=this.activeKeyboard; - if(!kbd || typeof kbd.id == 'undefined' || kbd.id == '') { - return false; - } - - // And the lookup under that entry looks for the value under the store name, again. - let valueObj: VariableStore = {}; - valueObj[storeName] = optValue; - - // Null-check in case of invocation during unit-test - if(this.ruleBehavior) { - this.ruleBehavior.saveStore[storeName] = valueObj; - } else { - // We're in a unit-test environment, directly invoking this method from outside of a keyboard. - // In this case, we should immediately commit the change. - this.variableStoreSerializer.saveStore(this.activeKeyboard.id, storeName, valueObj); - } - return true; - } - - resetContextCache(): void { - this.cachedContext.reset(); - this.cachedContextEx.reset(); - } - - defaultBackspace(outputTarget: OutputTarget) { - if(outputTarget.isSelectionEmpty()) { - // Delete the character left of the caret - this.output(1, outputTarget, ""); - } else { - // Delete just the selection - this.output(0, outputTarget, ""); - } - } - - /** - * Function processNewContextEvent - * Scope Private - * @param {Object} outputTarget The target receiving input - * @param {Object} keystroke The input keystroke (with its properties) to be mapped by the keyboard. - * Description Calls the keyboard's `begin newContext` group - * @returns {RuleBehavior} Record of commands and state changes that result from executing `begin NewContext` - */ - processNewContextEvent(outputTarget: OutputTarget, keystroke: KeyEvent): RuleBehavior { - if(!this.activeKeyboard) { - throw "No active keyboard for keystroke processing!"; - } - return this.process(this.activeKeyboard.processNewContextEvent.bind(this.activeKeyboard), outputTarget, keystroke, true); - } - - /** - * Function processPostKeystroke - * Scope Private - * @param {Object} outputTarget The target receiving input - * @param {Object} keystroke The input keystroke with relevant properties to be mapped by the keyboard. - * Description Calls the keyboard's `begin postKeystroke` group - * @returns {RuleBehavior} Record of commands and state changes that result from executing `begin PostKeystroke` - */ - processPostKeystroke(outputTarget: OutputTarget, keystroke: KeyEvent): RuleBehavior { - if(!this.activeKeyboard) { - throw "No active keyboard for keystroke processing!"; - } - return this.process(this.activeKeyboard.processPostKeystroke.bind(this.activeKeyboard), outputTarget, keystroke, true); - } - - /** - * Function processKeystroke - * Scope Private - * @param {Object} outputTarget The target receiving input - * @param {Object} keystroke The input keystroke (with its properties) to be mapped by the keyboard. - * Description Encapsulates calls to keyboard input processing. - * @returns {RuleBehavior} Record of commands and state changes that result from executing `begin Unicode` - */ - processKeystroke(outputTarget: OutputTarget, keystroke: KeyEvent): RuleBehavior { - if(!this.activeKeyboard) { - throw "No active keyboard for keystroke processing!"; - } - return this.process(this.activeKeyboard.process.bind(this.activeKeyboard), outputTarget, keystroke, false); - } - - private process(callee, outputTarget: OutputTarget, keystroke: KeyEvent, readonly: boolean): RuleBehavior { - // Clear internal state tracking data from prior keystrokes. - if(!outputTarget) { - throw "No target specified for keyboard output!"; - } else if(!this.activeKeyboard) { - throw "No active keyboard for keystroke processing!"; - } else if(!callee) { - throw "No callee for keystroke processing!"; - } - - outputTarget.invalidateSelection(); - - outputTarget.deadkeys().resetMatched(); // I3318 - this.resetContextCache(); - - // Capture the initial state of the OutputTarget before any rules are matched. - let preInput = Mock.from(outputTarget, true); - - // Capture the initial state of any variable stores - const cachedVariableStores = this.activeKeyboard.variableStores; - - // Establishes the results object, allowing corresponding commands to set values here as appropriate. - this.ruleBehavior = new RuleBehavior(); - - // Ensure the settings are in place so that KIFS/ifState activates and deactivates - // the appropriate rule(s) for the modeled device. - this.activeDevice = keystroke.device; - - // Calls the start-group of the active keyboard. - this.activeTargetOutput = outputTarget; - var matched = callee(outputTarget, keystroke); - this.activeTargetOutput = null; - - // Finalize the rule's results. - this.ruleBehavior.transcription = outputTarget.buildTranscriptionFrom(preInput, keystroke, readonly); - - // We always backup the changes to variable stores to the RuleBehavior, to - // be applied during finalization, then restore them to the cached initial - // values to avoid side-effects with predictive text mocks. - this.ruleBehavior.variableStores = this.activeKeyboard.variableStores; - this.activeKeyboard.variableStores = cachedVariableStores; - - // `matched` refers to whether or not the FINAL rule (from any group) matched, rather than - // whether or not ANY rule matched. If the final rule doesn't match, we trigger the key's - // default behavior (if appropriate). - // - // See https://github.com/keymanapp/keyman/pull/4350#issuecomment-768753852 - this.ruleBehavior.triggerKeyDefault = !matched; - - // Clear our result-tracking variable to prevent any possible pollution for future processing. - let behavior = this.ruleBehavior; - this.ruleBehavior = null; - - return behavior; - } - - /** - * Applies the dictionary of variable store values to the active keyboard - * - * Has no effect on keyboards compiled with 14.0 or earlier; system store - * names are not exposed unless compiled with Developer 15.0 or later. - * - * @param stores A dictionary of stores which should be found in the - * keyboard - */ - applyVariableStores(stores: com.keyman.keyboards.VariableStoreDictionary): void { - this.activeKeyboard.variableStores = stores; - } - - /** - * Publishes the KeyboardInterface's shorthand API names. As this assigns the current functions - * held by the longform versions, note that this should be called after replacing any of them via - * JS method extension. - * - * DOM-aware KeymanWeb should call this after its domKbdInterface.ts code is loaded, as it replaces - * a few. (This is currently done within its kmwapi.ts.) - */ - static __publishShorthandAPI() { - // Keyboard callbacks - let prototype = this.prototype; - - var exportKBCallback = function(miniName: string, longName: string) { - prototype[miniName] = prototype[longName]; - } - - exportKBCallback('KSF', 'saveFocus'); - exportKBCallback('KBR', 'beepReset'); - exportKBCallback('KT', 'insertText'); - exportKBCallback('KR', 'registerKeyboard'); - exportKBCallback('KRS', 'registerStub'); - exportKBCallback('KC', 'context'); - exportKBCallback('KN', 'nul'); - exportKBCallback('KCM', 'contextMatch'); - exportKBCallback('KFCM', 'fullContextMatch'); - exportKBCallback('KIK', 'isKeypress'); - exportKBCallback('KKM', 'keyMatch'); - exportKBCallback('KSM', 'stateMatch'); - exportKBCallback('KKI', 'keyInformation'); - exportKBCallback('KDM', 'deadkeyMatch'); - exportKBCallback('KB', 'beep'); - exportKBCallback('KA', 'any'); - exportKBCallback('KDC', 'deleteContext'); - exportKBCallback('KO', 'output'); - exportKBCallback('KDO', 'deadkeyOutput'); - exportKBCallback('KCXO', 'contextExOutput'); - exportKBCallback('KIO', 'indexOutput'); - exportKBCallback('KIFS', 'ifStore'); - exportKBCallback('KSETS', 'setStore'); - exportKBCallback('KLOAD', 'loadStore'); - exportKBCallback('KSAVE', 'saveStore'); + if(this._AnyIndices[Pn-1] < Ps.length) { //I3319 + return Ps[this._AnyIndices[Pn-1]]; + } else { + /* Should not be possible for a compiled keyboard, but may arise + * during the development of handwritten keyboards. + */ + console.warn("Unmatched contextual index() statement detected in rule with index " + Pn + "!"); + return ""; } } - (function() { - // This will be the only call within the keyboard-processor module. - KeyboardInterface.__publishShorthandAPI(); - }()); -} \ No newline at end of file + /** + * Function indexOutput KIO + * Scope Public + * @param {number} Pdn no of character to overwrite (delete) + * @param {string} Ps string + * @param {number} Pn index + * @param {Object} Pelem element to output to + * Description Output a character selected from the string according to the offset in the index array + */ + indexOutput(Pdn: number, Ps: KeyboardStore, Pn: number, outputTarget: OutputTarget): void { + this.resetContextCache(); + + var assertNever = function(x: never): never { + // Could be accessed by improperly handwritten calls to `fullContextMatch`. + throw new Error("Unexpected object in fullContextMatch specification: " + x); + } + + var indexChar = this._Index(Ps, Pn); + if(indexChar !== "") { + if(typeof indexChar == 'string' ) { + this.output(Pdn, outputTarget, indexChar); //I3319 + } else if(indexChar['t']) { + var storeEntry = indexChar as StoreNonCharEntry; + + switch(storeEntry.t) { + case 'b': // Beep commands may appear within stores. + this.beep(outputTarget); + break; + case 'd': + this.deadkeyOutput(Pdn, outputTarget, indexChar['d']); + break; + default: + assertNever(storeEntry); + } + } else { // For keyboards developed during 10.0's alpha phase - t:'d' was assumed. + this.deadkeyOutput(Pdn, outputTarget, indexChar['d']); + } + } + } + + + /** + * Function deleteContext KDC + * Scope Public + * @param {number} dn number of context entries to overwrite + * @param {Object} Pelem element to output to + * @param {string} s string to output + * Description Keyboard output + */ + deleteContext(dn: number, outputTarget: OutputTarget): void { + var context: CachedExEntry; + + // We want to control exactly which deadkeys get removed. + if(dn > 0) { + context = this._BuildExtendedContext(dn, dn, outputTarget); + let nulCount = 0; + + for(var i=0; i < context.valContext.length; i++) { + var dk = context.deadContext[i]; + + if(dk) { + // Remove deadkey in context. + outputTarget.deadkeys().remove(dk); + + // Reduce our reported context size. + dn--; + } else if(context.valContext[i] == "\uFFFE") { + // Count any `nul` sentinels that would contribute to our deletion count. + nulCount++; + } + } + + // Prevent attempts to delete nul sentinels, as they don't exist in the actual context. + // (Addresses regression from KMW v 12.0 paired with Developer bug through same version) + let contextLength = context.valContext.length - nulCount; + if(dn > contextLength) { + dn = contextLength; + } + } + + // If a matched deadkey hasn't been deleted, we don't WANT to delete it. + outputTarget.deadkeys().resetMatched(); + + // Why reinvent the wheel? Delete the remaining characters by 'inserting a blank string'. + this.output(dn, outputTarget, ''); + } + + /** + * Function output KO + * Scope Public + * @param {number} dn number of characters to overwrite + * @param {Object} Pelem element to output to + * @param {string} s string to output + * Description Keyboard output + */ + output(dn: number, outputTarget: OutputTarget, s:string): void { + this.resetContextCache(); + + outputTarget.saveProperties(); + outputTarget.clearSelection(); + outputTarget.deadkeys().deleteMatched(); // I3318 + if(dn >= 0) { + // Automatically manages affected deadkey positions. Does not delete deadkeys b/c legacy behavior support. + outputTarget.deleteCharsBeforeCaret(dn); + } + // Automatically manages affected deadkey positions. + outputTarget.insertTextBeforeCaret(s); + outputTarget.restoreProperties(); + } + + /** + * `contextExOutput` function emits the character or object at `contextOffset` from the + * current matched rule's context. Introduced in Keyman 14.0, in order to resolve a + * gap between desktop and web core functionality for context(n) matching on notany(). + * See #917 for additional detail. + * @alias KCXO + * @public + * @param {number} Pdn number of characters to delete left of cursor + * @param {OutputTarget} outputTarget target to output to + * @param {number} contextLength length of current rule context to retrieve + * @param {number} contextOffset offset from start of current rule context, 1-based + */ + contextExOutput(Pdn: number, outputTarget: OutputTarget, contextLength: number, contextOffset: number): void { + this.resetContextCache(); + + if(Pdn >= 0) { + this.output(Pdn, outputTarget, ""); + } + + const context = this.ruleContextEx.get(contextLength, contextLength); + const dk = context.deadContext[contextOffset-1], vc = context.valContext[contextOffset-1]; + if(dk) { + outputTarget.insertDeadkeyBeforeCaret(dk.d); + } else if(typeof vc == 'string') { + this.output(-1, outputTarget, vc); + } else { + throw new Error("contextExOutput: should never be a numeric valContext with no corresponding deadContext"); + } + } + + /** + * Function deadkeyOutput KDO + * Scope Public + * @param {number} Pdn no of character to overwrite (delete) + * @param {Object} Pelem element to output to + * @param {number} Pd deadkey id + * Description Record a deadkey at current cursor position, deleting Pdn characters first + */ + deadkeyOutput(Pdn: number, outputTarget: OutputTarget, Pd: number): void { + this.resetContextCache(); + + if(Pdn >= 0) { + this.output(Pdn, outputTarget,""); //I3318 corrected to >= + } + + outputTarget.insertDeadkeyBeforeCaret(Pd); + // _DebugDeadKeys(Pelem, 'KDeadKeyOutput: dn='+Pdn+'; deadKey='+Pd); + } + + /** + * KIFS compares the content of a system store with a string value + * + * @param {number} systemId ID of the system store to test (only TSS_LAYER currently supported) + * @param {string} strValue String value to compare to + * @param {Object} Pelem Currently active element (may be needed by future tests) + * @return {boolean} True if the test succeeds + */ + ifStore(systemId: number, strValue: string, outputTarget: OutputTarget): boolean { + var result=true; + let store = this.systemStores[systemId]; + if(store) { + result = store.matches(strValue); + } + return result; //Moved from previous line, now supports layer selection, Build 350 + } + + /** + * KSETS sets the value of a system store to a string + * + * @param {number} systemId ID of the system store to set (only TSS_LAYER currently supported) + * @param {string} strValue String to set as the system store content + * @param {Object} Pelem Currently active element (may be needed in future tests) + * @return {boolean} True if command succeeds + * (i.e. for TSS_LAYER, if the layer is successfully selected) + * + * Note that option/variable stores are instead set within keyboard script code, as they only + * affect keyboard behavior. + */ + setStore(systemId: number, strValue: string, outputTarget: OutputTarget): boolean { + this.resetContextCache(); + // Unique case: we only allow set(&layer) ops from keyboard rules triggered by touch OSKs. + if(systemId == SystemStoreIDs.TSS_LAYER && this.activeDevice.touchable) { + // Denote the changed store as part of the matched rule's behavior. + this.ruleBehavior.setStore[systemId] = strValue; + } else { + return false; + } + } + + /** + * Load an option store value from a cookie or default value + * + * @param {string} kbdName keyboard internal name + * @param {string} storeName store (option) name, embedded in cookie name + * @param {string} dfltValue default value + * @return {string} current or default option value + * + * This will only ever be called when the keyboard is loaded, as it is used by keyboards + * to initialize a store value on the keyboard's script object. + */ + loadStore(kbdName: string, storeName:string, dfltValue:string): string { + this.resetContextCache(); + if(this.variableStoreSerializer) { + let cValue = this.variableStoreSerializer.loadStore(kbdName, storeName); + return cValue[storeName] || dfltValue; + } else { + return dfltValue; + } + } + + /** + * Save an option store value to a cookie + * + * @param {string} storeName store (option) name, embedded in cookie name + * @param {string} optValue option value to save + * @return {boolean} true if save successful + * + * Note that a keyboard will freely manipulate the value of its variable stores on the + * script object within its own code. This function's use is merely to _persist_ that + * value across sessions, providing a custom user default for later uses of the keyboard. + */ + saveStore(storeName:string, optValue:string): boolean { + this.resetContextCache(); + var kbd=this.activeKeyboard; + if(!kbd || typeof kbd.id == 'undefined' || kbd.id == '') { + return false; + } + + // And the lookup under that entry looks for the value under the store name, again. + let valueObj: VariableStore = {}; + valueObj[storeName] = optValue; + + // Null-check in case of invocation during unit-test + if(this.ruleBehavior) { + this.ruleBehavior.saveStore[storeName] = valueObj; + } else { + // We're in a unit-test environment, directly invoking this method from outside of a keyboard. + // In this case, we should immediately commit the change. + this.variableStoreSerializer.saveStore(this.activeKeyboard.id, storeName, valueObj); + } + return true; + } + + resetContextCache(): void { + this.cachedContext.reset(); + this.cachedContextEx.reset(); + } + + defaultBackspace(outputTarget: OutputTarget) { + if(outputTarget.isSelectionEmpty()) { + // Delete the character left of the caret + this.output(1, outputTarget, ""); + } else { + // Delete just the selection + this.output(0, outputTarget, ""); + } + } + + /** + * Function processNewContextEvent + * Scope Private + * @param {Object} outputTarget The target receiving input + * @param {Object} keystroke The input keystroke (with its properties) to be mapped by the keyboard. + * Description Calls the keyboard's `begin newContext` group + * @returns {RuleBehavior} Record of commands and state changes that result from executing `begin NewContext` + */ + processNewContextEvent(outputTarget: OutputTarget, keystroke: KeyEvent): RuleBehavior { + if(!this.activeKeyboard) { + throw "No active keyboard for keystroke processing!"; + } + return this.process(this.activeKeyboard.processNewContextEvent.bind(this.activeKeyboard), outputTarget, keystroke, true); + } + + /** + * Function processPostKeystroke + * Scope Private + * @param {Object} outputTarget The target receiving input + * @param {Object} keystroke The input keystroke with relevant properties to be mapped by the keyboard. + * Description Calls the keyboard's `begin postKeystroke` group + * @returns {RuleBehavior} Record of commands and state changes that result from executing `begin PostKeystroke` + */ + processPostKeystroke(outputTarget: OutputTarget, keystroke: KeyEvent): RuleBehavior { + if(!this.activeKeyboard) { + throw "No active keyboard for keystroke processing!"; + } + return this.process(this.activeKeyboard.processPostKeystroke.bind(this.activeKeyboard), outputTarget, keystroke, true); + } + + /** + * Function processKeystroke + * Scope Private + * @param {Object} outputTarget The target receiving input + * @param {Object} keystroke The input keystroke (with its properties) to be mapped by the keyboard. + * Description Encapsulates calls to keyboard input processing. + * @returns {RuleBehavior} Record of commands and state changes that result from executing `begin Unicode` + */ + processKeystroke(outputTarget: OutputTarget, keystroke: KeyEvent): RuleBehavior { + if(!this.activeKeyboard) { + throw "No active keyboard for keystroke processing!"; + } + return this.process(this.activeKeyboard.process.bind(this.activeKeyboard), outputTarget, keystroke, false); + } + + private process(callee, outputTarget: OutputTarget, keystroke: KeyEvent, readonly: boolean): RuleBehavior { + // Clear internal state tracking data from prior keystrokes. + if(!outputTarget) { + throw "No target specified for keyboard output!"; + } else if(!this.activeKeyboard) { + throw "No active keyboard for keystroke processing!"; + } else if(!callee) { + throw "No callee for keystroke processing!"; + } + + outputTarget.invalidateSelection(); + + outputTarget.deadkeys().resetMatched(); // I3318 + this.resetContextCache(); + + // Capture the initial state of the OutputTarget before any rules are matched. + let preInput = Mock.from(outputTarget, true); + + // Capture the initial state of any variable stores + const cachedVariableStores = this.activeKeyboard.variableStores; + + // Establishes the results object, allowing corresponding commands to set values here as appropriate. + this.ruleBehavior = new RuleBehavior(); + + // Ensure the settings are in place so that KIFS/ifState activates and deactivates + // the appropriate rule(s) for the modeled device. + this.activeDevice = keystroke.device; + + // Calls the start-group of the active keyboard. + this.activeTargetOutput = outputTarget; + var matched = callee(outputTarget, keystroke); + this.activeTargetOutput = null; + + // Finalize the rule's results. + this.ruleBehavior.transcription = outputTarget.buildTranscriptionFrom(preInput, keystroke, readonly); + + // We always backup the changes to variable stores to the RuleBehavior, to + // be applied during finalization, then restore them to the cached initial + // values to avoid side-effects with predictive text mocks. + this.ruleBehavior.variableStores = this.activeKeyboard.variableStores; + this.activeKeyboard.variableStores = cachedVariableStores; + + // `matched` refers to whether or not the FINAL rule (from any group) matched, rather than + // whether or not ANY rule matched. If the final rule doesn't match, we trigger the key's + // default behavior (if appropriate). + // + // See https://github.com/keymanapp/keyman/pull/4350#issuecomment-768753852 + this.ruleBehavior.triggerKeyDefault = !matched; + + // Clear our result-tracking variable to prevent any possible pollution for future processing. + let behavior = this.ruleBehavior; + this.ruleBehavior = null; + + return behavior; + } + + /** + * Applies the dictionary of variable store values to the active keyboard + * + * Has no effect on keyboards compiled with 14.0 or earlier; system store + * names are not exposed unless compiled with Developer 15.0 or later. + * + * @param stores A dictionary of stores which should be found in the + * keyboard + */ + applyVariableStores(stores: VariableStoreDictionary): void { + this.activeKeyboard.variableStores = stores; + } + + /** + * Publishes the KeyboardInterface's shorthand API names. As this assigns the current functions + * held by the longform versions, note that this should be called after replacing any of them via + * JS method extension. + * + * DOM-aware KeymanWeb should call this after its domKbdInterface.ts code is loaded, as it replaces + * a few. (This is currently done within its kmwapi.ts.) + */ + static __publishShorthandAPI() { + // Keyboard callbacks + let prototype = this.prototype; + + var exportKBCallback = function(miniName: string, longName: string) { + prototype[miniName] = prototype[longName]; + } + + exportKBCallback('KSF', 'saveFocus'); + exportKBCallback('KBR', 'beepReset'); + exportKBCallback('KT', 'insertText'); + exportKBCallback('KR', 'registerKeyboard'); + exportKBCallback('KRS', 'registerStub'); + exportKBCallback('KC', 'context'); + exportKBCallback('KN', 'nul'); + exportKBCallback('KCM', 'contextMatch'); + exportKBCallback('KFCM', 'fullContextMatch'); + exportKBCallback('KIK', 'isKeypress'); + exportKBCallback('KKM', 'keyMatch'); + exportKBCallback('KSM', 'stateMatch'); + exportKBCallback('KKI', 'keyInformation'); + exportKBCallback('KDM', 'deadkeyMatch'); + exportKBCallback('KB', 'beep'); + exportKBCallback('KA', 'any'); + exportKBCallback('KDC', 'deleteContext'); + exportKBCallback('KO', 'output'); + exportKBCallback('KDO', 'deadkeyOutput'); + exportKBCallback('KCXO', 'contextExOutput'); + exportKBCallback('KIO', 'indexOutput'); + exportKBCallback('KIFS', 'ifStore'); + exportKBCallback('KSETS', 'setStore'); + exportKBCallback('KLOAD', 'loadStore'); + exportKBCallback('KSAVE', 'saveStore'); + } +} + +(function() { + // This will be the only call within the keyboard-processor module. + KeyboardInterface.__publishShorthandAPI(); +}()); \ No newline at end of file diff --git a/common/web/keyboard-processor/src/text/keyEvent.ts b/common/web/keyboard-processor/src/text/keyEvent.ts index 1a16dc94bc..64b722e211 100644 --- a/common/web/keyboard-processor/src/text/keyEvent.ts +++ b/common/web/keyboard-processor/src/text/keyEvent.ts @@ -1,57 +1,56 @@ -/// +import type Keyboard from "../keyboards/keyboard.js"; +import type DeviceSpec from "utils/build/modules/deviceSpec.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; - } - }; -} \ No newline at end of file + public static constructNullKeyEvent(device: DeviceSpec): KeyEvent { + const keyEvent = new KeyEvent(); + keyEvent.Lcode = 0; + keyEvent.kName = ''; + keyEvent.device = device; + return keyEvent; + } +}; \ No newline at end of file diff --git a/common/web/keyboard-processor/src/text/keyMapping.ts b/common/web/keyboard-processor/src/text/keyMapping.ts index ba7cc09868..771abe7bd2 100644 --- a/common/web/keyboard-processor/src/text/keyMapping.ts +++ b/common/web/keyboard-processor/src/text/keyMapping.ts @@ -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~~~~~ ` ~ + 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~~~~~ ` ~ - 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; } } \ No newline at end of file diff --git a/common/web/keyboard-processor/src/text/keyboardProcessor.ts b/common/web/keyboard-processor/src/text/keyboardProcessor.ts index a1ad0699f0..cdd3367f05 100644 --- a/common/web/keyboard-processor/src/text/keyboardProcessor.ts +++ b/common/web/keyboard-processor/src/text/keyboardProcessor.ts @@ -1,780 +1,773 @@ -// Establishes key-code definitions. -/// -// Defines our generalized "KeyEvent" class. -/// -// Defines the RuleBehavior keyboard-processing return object. -/// -// Defines default key handling behaviors. -/// -// Defines the keyboard wrapper object. -/// -// Defines built-in keymapping. -/// +// #region Big ol' list of imports + +import Codes from "./codes.js"; +import type Keyboard from "../keyboards/keyboard.js"; +import KeyEvent from "./keyEvent.js"; +import { Layouts } from "keyboards/defaultLayouts"; +import type { MutableSystemStore } from "./systemStores.js"; + +import DefaultOutput, { EmulationKeystrokes } from "./defaultOutput"; +import type OutputTarget from "./outputTarget.js"; +import { Mock } from "./outputTarget.js"; + +import KeyboardInterface, { SystemStoreIDs, VariableStore } from "./kbdInterface.js"; +import RuleBehavior from "./ruleBehavior.js"; + +import { DeviceSpec, globalObject as getGlobalObject } from "utils/build/modules/index.js"; + +// #endregion // Also relies on @keymanapp/web-utils, which is included via tsconfig.json. -namespace com.keyman.text { - export type BeepHandler = (outputTarget: OutputTarget) => void; - export type LogMessageHandler = (str: string) => void; +export type BeepHandler = (outputTarget: OutputTarget) => void; +export type LogMessageHandler = (str: string) => void; - export interface VariableStoreSerializer { - loadStore(keyboardID: string, storeName: string): VariableStore; - saveStore(keyboardID: string, storeName: string, storeMap: VariableStore); +export interface VariableStoreSerializer { + loadStore(keyboardID: string, storeName: string): VariableStore; + saveStore(keyboardID: string, storeName: string, storeMap: VariableStore); +} + +export interface ProcessorInitOptions { + baseLayout?: string; + variableStoreSerializer?: VariableStoreSerializer; +} + +export class KeyboardProcessor { + public static readonly DEFAULT_OPTIONS: ProcessorInitOptions = { + baseLayout: 'us' } - export interface ProcessorInitOptions { - baseLayout?: string; - variableStoreSerializer?: VariableStoreSerializer; + // Tracks the simulated value for supported state keys, allowing the OSK to mirror a physical keyboard for them. + // Using the exact keyCode name from the Codes definitions will allow for certain optimizations elsewhere in the code. + stateKeys = { + "K_CAPS":false, + "K_NUMLOCK":false, + "K_SCROLL":false + }; + + // Tracks the most recent modifier state information in order to quickly detect changes + // in keyboard state not otherwise captured by the hosting page in the browser. + // Needed for AltGr simulation. + modStateFlags: number = 0; + + keyboardInterface: KeyboardInterface; + + /** + * Indicates the device (platform) to be used for non-keystroke events, + * such as those sent to `begin postkeystroke` and `begin newcontext` + * entry points. + */ + contextDevice: DeviceSpec; + + baseLayout: string; + + // Callbacks for various feedback types + beepHandler?: BeepHandler; + warningLogger?: LogMessageHandler; + errorLogger?: LogMessageHandler; + + constructor(device: DeviceSpec, options?: ProcessorInitOptions) { + if(!options) { + options = KeyboardProcessor.DEFAULT_OPTIONS; + } + + this.contextDevice = device; + + this.baseLayout = options.baseLayout || KeyboardProcessor.DEFAULT_OPTIONS.baseLayout; + this.keyboardInterface = new KeyboardInterface(options.variableStoreSerializer); + this.installInterface(); } - export class KeyboardProcessor { - public static readonly DEFAULT_OPTIONS: ProcessorInitOptions = { - baseLayout: 'us' + private installInterface() { + // We must ensure that the keyboard can find the API functions at the expected place. + let globalThis = getGlobalObject(); + globalThis[KeyboardInterface.GLOBAL_NAME] = this.keyboardInterface; + + // Ensure that the active keyboard is set on the keyboard interface object. + if(this.activeKeyboard) { + this.keyboardInterface.activeKeyboard = this.activeKeyboard; } + } - // Tracks the simulated value for supported state keys, allowing the OSK to mirror a physical keyboard for them. - // Using the exact keyCode name from the Codes definitions will allow for certain optimizations elsewhere in the code. - stateKeys = { - "K_CAPS":false, - "K_NUMLOCK":false, - "K_SCROLL":false - }; + public get activeKeyboard(): Keyboard { + return this.keyboardInterface.activeKeyboard; + } - // Tracks the most recent modifier state information in order to quickly detect changes - // in keyboard state not otherwise captured by the hosting page in the browser. - // Needed for AltGr simulation. - modStateFlags: number = 0; + public set activeKeyboard(keyboard: Keyboard) { + this.keyboardInterface.activeKeyboard = keyboard; - keyboardInterface: KeyboardInterface; + // All old deadkeys and keyboard-specific cache should immediately be invalidated + // on a keyboard change. + this.resetContext(); + } - /** - * Indicates the device (platform) to be used for non-keystroke events, - * such as those sent to `begin postkeystroke` and `begin newcontext` - * entry points. - */ - contextDevice: utils.DeviceSpec; + get layerStore(): MutableSystemStore { + return this.keyboardInterface.systemStores[SystemStoreIDs.TSS_LAYER] as MutableSystemStore; + } - baseLayout: string; + public get newLayerStore(): MutableSystemStore { + return this.keyboardInterface.systemStores[SystemStoreIDs.TSS_NEWLAYER] as MutableSystemStore; + } - // Callbacks for various feedback types - beepHandler?: BeepHandler; - warningLogger?: LogMessageHandler; - errorLogger?: LogMessageHandler; + public get oldLayerStore(): MutableSystemStore { + return this.keyboardInterface.systemStores[SystemStoreIDs.TSS_OLDLAYER] as MutableSystemStore; + } - constructor(device: utils.DeviceSpec, options?: ProcessorInitOptions) { - if(!options) { - options = KeyboardProcessor.DEFAULT_OPTIONS; - } + public get layerId(): string { + return this.layerStore.value; + } - this.contextDevice = device; + // Note: will trigger an 'event' callback designed to notify the OSK of layer changes. + public set layerId(value: string) { + this.layerStore.set(value); + } - this.baseLayout = options.baseLayout || KeyboardProcessor.DEFAULT_OPTIONS.baseLayout; - this.keyboardInterface = new KeyboardInterface(options.variableStoreSerializer); - this.installInterface(); - } + /** + * Get the default RuleBehavior for the specified key, attempting to mimic standard browser defaults + * where and when appropriate. + * + * @param {object} Lkc The pre-analyzed KeyEvent object + * @param {boolean} outputTarget The OutputTarget receiving the KeyEvent + * @return {string} + */ + defaultRuleBehavior(Lkc: KeyEvent, outputTarget: OutputTarget, readonly: boolean): RuleBehavior { + let preInput = Mock.from(outputTarget, readonly); + let ruleBehavior = new RuleBehavior(); - private installInterface() { - // We must ensure that the keyboard can find the API functions at the expected place. - let globalThis = utils.getGlobalObject(); - globalThis[KeyboardInterface.GLOBAL_NAME] = this.keyboardInterface; + let matched = false; + var char = ''; + var special: EmulationKeystrokes; + if(Lkc.isSynthetic || outputTarget.isSynthetic) { + matched = true; // All the conditions below result in matches until the final else, which restores the expected default + // if no match occurs. - // Ensure that the active keyboard is set on the keyboard interface object. - if(this.activeKeyboard) { - this.keyboardInterface.activeKeyboard = this.activeKeyboard; - } - } + if(DefaultOutput.isCommand(Lkc)) { + // Note this in the rule behavior, return successfully. We'll consider applying it later. + ruleBehavior.triggersDefaultCommand = true; - public get activeKeyboard(): keyboards.Keyboard { - return this.keyboardInterface.activeKeyboard; - } - - public set activeKeyboard(keyboard: keyboards.Keyboard) { - this.keyboardInterface.activeKeyboard = keyboard; - - // All old deadkeys and keyboard-specific cache should immediately be invalidated - // on a keyboard change. - this.resetContext(); - } - - get layerStore(): MutableSystemStore { - return this.keyboardInterface.systemStores[KeyboardInterface.TSS_LAYER] as MutableSystemStore; - } - - public get newLayerStore(): MutableSystemStore { - return this.keyboardInterface.systemStores[KeyboardInterface.TSS_NEWLAYER] as MutableSystemStore; - } - - public get oldLayerStore(): MutableSystemStore { - return this.keyboardInterface.systemStores[KeyboardInterface.TSS_OLDLAYER] as MutableSystemStore; - } - - public get layerId(): string { - return this.layerStore.value; - } - - // Note: will trigger an 'event' callback designed to notify the OSK of layer changes. - public set layerId(value: string) { - this.layerStore.set(value); - } - - /** - * Get the default RuleBehavior for the specified key, attempting to mimic standard browser defaults - * where and when appropriate. - * - * @param {object} Lkc The pre-analyzed KeyEvent object - * @param {boolean} outputTarget The OutputTarget receiving the KeyEvent - * @return {string} - */ - defaultRuleBehavior(Lkc: KeyEvent, outputTarget: OutputTarget, readonly: boolean): RuleBehavior { - let preInput = Mock.from(outputTarget, readonly); - let ruleBehavior = new RuleBehavior(); - - let matched = false; - var char = ''; - var special: EmulationKeystrokes; - if(Lkc.isSynthetic || outputTarget.isSynthetic) { - matched = true; // All the conditions below result in matches until the final else, which restores the expected default - // if no match occurs. - - if(DefaultOutput.isCommand(Lkc)) { - // Note this in the rule behavior, return successfully. We'll consider applying it later. - ruleBehavior.triggersDefaultCommand = true; - - // We'd rather let the browser handle these keys, but we're using emulated keystrokes, forcing KMW - // to emulate default behavior here. - } else if((special = DefaultOutput.forSpecialEmulation(Lkc)) != null) { - switch(special) { - case EmulationKeystrokes.Backspace: - this.keyboardInterface.defaultBackspace(outputTarget); - break; - case EmulationKeystrokes.Enter: - outputTarget.handleNewlineAtCaret(); - break; - // case '\u007f': // K_DEL - // // For (possible) future implementation. - // // Would recommend (conceptually) equaling K_RIGHT + K_BKSP, the former of which would technically be a 'command'. - default: - // In case we extend the allowed set, but forget to implement its handling case above. - ruleBehavior.errorLog = "Unexpected 'special emulation' character (\\u" + (special as String).kmwCharCodeAt(0).toString(16) + ") went unhandled!"; - } - } else { - // Back to the standard default, pending normal matching. - matched = false; - } - } - - let isMnemonic = this.activeKeyboard && this.activeKeyboard.isMnemonic; - - if(!matched) { - if((char = DefaultOutput.forAny(Lkc, isMnemonic)) != null) { - special = DefaultOutput.forSpecialEmulation(Lkc) - if(special == EmulationKeystrokes.Backspace) { - // A browser's default backspace may fail to delete both parts of an SMP character. + // We'd rather let the browser handle these keys, but we're using emulated keystrokes, forcing KMW + // to emulate default behavior here. + } else if((special = DefaultOutput.forSpecialEmulation(Lkc)) != null) { + switch(special) { + case EmulationKeystrokes.Backspace: this.keyboardInterface.defaultBackspace(outputTarget); - } else if(special || DefaultOutput.isCommand(Lkc)) { // Filters out 'commands' like TAB. - // We only do the "for special emulation" cases under the condition above... aside from backspace - // Let the browser handle those. - return null; - } else { - this.keyboardInterface.output(0, outputTarget, char); - } - } else { - // No match, no default RuleBehavior. - return null; + break; + case EmulationKeystrokes.Enter: + outputTarget.handleNewlineAtCaret(); + break; + // case '\u007f': // K_DEL + // // For (possible) future implementation. + // // Would recommend (conceptually) equaling K_RIGHT + K_BKSP, the former of which would technically be a 'command'. + default: + // In case we extend the allowed set, but forget to implement its handling case above. + ruleBehavior.errorLog = "Unexpected 'special emulation' character (\\u" + (special as String).kmwCharCodeAt(0).toString(16) + ") went unhandled!"; } + } else { + // Back to the standard default, pending normal matching. + matched = false; } + } - // Shortcut things immediately if there were issues generating this rule behavior. - if(ruleBehavior.errorLog) { - return ruleBehavior; + let isMnemonic = this.activeKeyboard && this.activeKeyboard.isMnemonic; + + if(!matched) { + if((char = DefaultOutput.forAny(Lkc, isMnemonic)) != null) { + special = DefaultOutput.forSpecialEmulation(Lkc) + if(special == EmulationKeystrokes.Backspace) { + // A browser's default backspace may fail to delete both parts of an SMP character. + this.keyboardInterface.defaultBackspace(outputTarget); + } else if(special || DefaultOutput.isCommand(Lkc)) { // Filters out 'commands' like TAB. + // We only do the "for special emulation" cases under the condition above... aside from backspace + // Let the browser handle those. + return null; + } else { + this.keyboardInterface.output(0, outputTarget, char); + } + } else { + // No match, no default RuleBehavior. + return null; } + } - let transcription = outputTarget.buildTranscriptionFrom(preInput, Lkc, readonly); - ruleBehavior.transcription = transcription; - + // Shortcut things immediately if there were issues generating this rule behavior. + if(ruleBehavior.errorLog) { return ruleBehavior; } - setSyntheticEventDefaults(Lkc: text.KeyEvent) { - // Set the flags for the state keys - for desktop devices. For touch - // devices, the only state key in use currently is Caps Lock, which is set - // when the 'caps' layer is active in ActiveKey::constructBaseKeyEvent. - if(!Lkc.device.touchable) { - Lkc.Lstates |= this.stateKeys['K_CAPS'] ? Codes.modifierCodes['CAPS'] : Codes.modifierCodes['NO_CAPS']; - Lkc.Lstates |= this.stateKeys['K_NUMLOCK'] ? Codes.modifierCodes['NUM_LOCK'] : Codes.modifierCodes['NO_NUM_LOCK']; - Lkc.Lstates |= this.stateKeys['K_SCROLL'] ? Codes.modifierCodes['SCROLL_LOCK'] : Codes.modifierCodes['NO_SCROLL_LOCK']; - } + let transcription = outputTarget.buildTranscriptionFrom(preInput, Lkc, readonly); + ruleBehavior.transcription = transcription; - // Set LisVirtualKey to false to ensure that nomatch rule does fire for U_xxxx keys - if(Lkc.kName && Lkc.kName.substr(0,2) == 'U_') { - Lkc.LisVirtualKey=false; - } + return ruleBehavior; + } - // Get code for non-physical keys (T_KOKAI, U_05AB etc) - if(typeof Lkc.Lcode == 'undefined') { - Lkc.Lcode = this.getVKDictionaryCode(Lkc.kName);// Updated for Build 347 - if(!Lkc.Lcode) { - // Special case for U_xxxx keys. This vk code will never be used - // in a keyboard, so we use this to ensure that keystroke processing - // occurs for the key. - Lkc.Lcode = 1; - } - } + setSyntheticEventDefaults(Lkc: KeyEvent) { + // Set the flags for the state keys - for desktop devices. For touch + // devices, the only state key in use currently is Caps Lock, which is set + // when the 'caps' layer is active in ActiveKey::constructBaseKeyEvent. + if(!Lkc.device.touchable) { + Lkc.Lstates |= this.stateKeys['K_CAPS'] ? Codes.modifierCodes['CAPS'] : Codes.modifierCodes['NO_CAPS']; + Lkc.Lstates |= this.stateKeys['K_NUMLOCK'] ? Codes.modifierCodes['NUM_LOCK'] : Codes.modifierCodes['NO_NUM_LOCK']; + Lkc.Lstates |= this.stateKeys['K_SCROLL'] ? Codes.modifierCodes['SCROLL_LOCK'] : Codes.modifierCodes['NO_SCROLL_LOCK']; + } - // Handles modifier states when the OSK is emulating rightalt through the leftctrl-leftalt layer. - if((Lkc.Lmodifiers & Codes.modifierBitmasks['ALT_GR_SIM']) == Codes.modifierBitmasks['ALT_GR_SIM'] && this.activeKeyboard.emulatesAltGr) { - Lkc.Lmodifiers &= ~Codes.modifierBitmasks['ALT_GR_SIM']; - Lkc.Lmodifiers |= Codes.modifierCodes['RALT']; + // Set LisVirtualKey to false to ensure that nomatch rule does fire for U_xxxx keys + if(Lkc.kName && Lkc.kName.substr(0,2) == 'U_') { + Lkc.LisVirtualKey=false; + } + + // Get code for non-physical keys (T_KOKAI, U_05AB etc) + if(typeof Lkc.Lcode == 'undefined') { + Lkc.Lcode = this.getVKDictionaryCode(Lkc.kName);// Updated for Build 347 + if(!Lkc.Lcode) { + // Special case for U_xxxx keys. This vk code will never be used + // in a keyboard, so we use this to ensure that keystroke processing + // occurs for the key. + Lkc.Lcode = 1; } } - constructNullKeyEvent(device: utils.DeviceSpec): KeyEvent { - const keyEvent = KeyEvent.constructNullKeyEvent(device); - this.setSyntheticEventDefaults(keyEvent); - return keyEvent; + // Handles modifier states when the OSK is emulating rightalt through the leftctrl-leftalt layer. + if((Lkc.Lmodifiers & Codes.modifierBitmasks['ALT_GR_SIM']) == Codes.modifierBitmasks['ALT_GR_SIM'] && this.activeKeyboard.emulatesAltGr) { + Lkc.Lmodifiers &= ~Codes.modifierBitmasks['ALT_GR_SIM']; + Lkc.Lmodifiers |= Codes.modifierCodes['RALT']; + } + } + + constructNullKeyEvent(device: DeviceSpec): KeyEvent { + const keyEvent = KeyEvent.constructNullKeyEvent(device); + this.setSyntheticEventDefaults(keyEvent); + return keyEvent; + } + + processNewContextEvent(device: DeviceSpec, outputTarget: OutputTarget): RuleBehavior { + return this.activeKeyboard ? + this.keyboardInterface.processNewContextEvent(outputTarget, this.constructNullKeyEvent(device)) : + null; + } + + processPostKeystroke(device: DeviceSpec, outputTarget: OutputTarget): RuleBehavior { + return this.activeKeyboard ? + this.keyboardInterface.processPostKeystroke(outputTarget, this.constructNullKeyEvent(device)) : + null; + } + + processKeystroke(keyEvent: KeyEvent, outputTarget: OutputTarget): RuleBehavior { + var matchBehavior: RuleBehavior; + + // Pass this key code and state to the keyboard program + if(this.activeKeyboard && keyEvent.Lcode != 0) { + /* + * The `this.installInterface()` call is insurance against something I've seen in unit tests when things break a bit. + * + * Currently, when a KMW shutdown doesn't go through properly or completely, sometimes we end up with parallel + * versions of KMW running, and an old, partially-shutdown one will "snipe" a command meant for the most-recent + * one's test. So, installing here ensures that the active Processor has its matching KeyboardInterface ready, + * even should that occur. + */ + this.installInterface(); + matchBehavior = this.keyboardInterface.processKeystroke(outputTarget, keyEvent); } - processNewContextEvent(device: utils.DeviceSpec, outputTarget: OutputTarget): RuleBehavior { - return this.activeKeyboard ? - this.keyboardInterface.processNewContextEvent(outputTarget, this.constructNullKeyEvent(device)) : - null; - } + if(!matchBehavior || matchBehavior.triggerKeyDefault) { + // Restore the virtual key code if a mnemonic keyboard is being used + // If no vkCode value was stored, maintain the original Lcode value. + keyEvent.Lcode=keyEvent.vkCode || keyEvent.Lcode; - processPostKeystroke(device: utils.DeviceSpec, outputTarget: OutputTarget): RuleBehavior { - return this.activeKeyboard ? - this.keyboardInterface.processPostKeystroke(outputTarget, this.constructNullKeyEvent(device)) : - null; - } + // Handle unmapped keys, including special keys + // The following is physical layout dependent, so should be avoided if possible. All keys should be mapped. + this.keyboardInterface.activeTargetOutput = outputTarget; - processKeystroke(keyEvent: KeyEvent, outputTarget: OutputTarget): RuleBehavior { - var matchBehavior: RuleBehavior; - - // Pass this key code and state to the keyboard program - if(this.activeKeyboard && keyEvent.Lcode != 0) { - /* - * The `this.installInterface()` call is insurance against something I've seen in unit tests when things break a bit. - * - * Currently, when a KMW shutdown doesn't go through properly or completely, sometimes we end up with parallel - * versions of KMW running, and an old, partially-shutdown one will "snipe" a command meant for the most-recent - * one's test. So, installing here ensures that the active Processor has its matching KeyboardInterface ready, - * even should that occur. - */ - this.installInterface(); - matchBehavior = this.keyboardInterface.processKeystroke(outputTarget, keyEvent); - } - - if(!matchBehavior || matchBehavior.triggerKeyDefault) { - // Restore the virtual key code if a mnemonic keyboard is being used - // If no vkCode value was stored, maintain the original Lcode value. - keyEvent.Lcode=keyEvent.vkCode || keyEvent.Lcode; - - // Handle unmapped keys, including special keys - // The following is physical layout dependent, so should be avoided if possible. All keys should be mapped. - this.keyboardInterface.activeTargetOutput = outputTarget; - - // Match against the 'default keyboard' - rules to mimic the default string output when typing in a browser. - // Many keyboards rely upon these 'implied rules'. - let defaultBehavior = this.defaultRuleBehavior(keyEvent, outputTarget, false); - if(defaultBehavior) { - if(!matchBehavior) { - matchBehavior = defaultBehavior; - } else { - matchBehavior.mergeInDefaults(defaultBehavior); - } - matchBehavior.triggerKeyDefault = false; // We've triggered it successfully. - } // If null, we must rely on something else (like the browser, in DOM-aware code) to fulfill the default. - - this.keyboardInterface.activeTargetOutput = null; - } - - return matchBehavior; - } - - // FIXME: makes some bad assumptions. - static setMnemonicCode(Lkc: KeyEvent, shifted: boolean, capsActive: boolean) { - // K_SPACE is not handled by defaultKeyOutput for physical keystrokes unless using touch-aliased elements. - // It's also a "exception required, March 2013" for clickKey, so at least they both have this requirement. - if(Lkc.Lcode != Codes.keyCodes['K_SPACE']) { - // So long as the key name isn't prefixed with 'U_', we'll get a default mapping based on the Lcode value. - // We need to determine the mnemonic base character - for example, SHIFT + K_PERIOD needs to map to '>'. - let mappingEvent: KeyEvent = new KeyEvent(); - for(var key in Lkc) { - mappingEvent[key] = Lkc[key]; - } - - // To facilitate storing relevant commands, we should probably reverse-lookup - // the actual keyname instead. - mappingEvent.kName = 'K_xxxx'; - mappingEvent.Lmodifiers = (shifted ? 0x10 : 0); // mnemonic lookups only exist for default & shift layers. - var mappedChar: string = DefaultOutput.forAny(mappingEvent, true); - - /* First, save a backup of the original code. This one won't needlessly trigger keyboard - * rules, but allows us to replicate/emulate commands after rule processing if needed. - * (Like backspaces) - */ - Lkc.vkCode = Lkc.Lcode; - if(mappedChar) { - // Will return 96 for 'a', which is a keycode corresponding to Codes.keyCodes('K_NP1') - a numpad key. - // That stated, we're in mnemonic mode - this keyboard's rules are based on the char codes. - Lkc.Lcode = mappedChar.charCodeAt(0); + // Match against the 'default keyboard' - rules to mimic the default string output when typing in a browser. + // Many keyboards rely upon these 'implied rules'. + let defaultBehavior = this.defaultRuleBehavior(keyEvent, outputTarget, false); + if(defaultBehavior) { + if(!matchBehavior) { + matchBehavior = defaultBehavior; } else { - // Don't let command-type keys (like K_DEL, which will output '.' otherwise!) - // trigger keyboard rules. - // - // However, DO make sure modifier keys pass through safely. - // (https://github.com/keymanapp/keyman/issues/3744) - if(!KeyboardProcessor.isModifier(Lkc)) { - delete Lkc.Lcode; - } + matchBehavior.mergeInDefaults(defaultBehavior); } - } + matchBehavior.triggerKeyDefault = false; // We've triggered it successfully. + } // If null, we must rely on something else (like the browser, in DOM-aware code) to fulfill the default. - if(capsActive) { - // TODO: Needs fixing - does not properly mirror physical keystrokes, as Lcode range 96-111 corresponds - // to numpad keys! (Physical keyboard section has its own issues here.) - if((Lkc.Lcode >= 65 && Lkc.Lcode <= 90) /* 'A' - 'Z' */ || (Lkc.Lcode >= 97 && Lkc.Lcode <= 122) /* 'a' - 'z' */) { - Lkc.Lmodifiers ^= 0x10; // Flip the 'shifted' bit, so it'll act as the opposite key. - Lkc.Lcode ^= 0x20; // Flips the 'upper' vs 'lower' bit for the base 'a'-'z' ASCII alphabetics. - } - } + this.keyboardInterface.activeTargetOutput = null; } - /** - * Get modifier key state from layer id - * - * @param {string} layerId layer id (e.g. ctrlshift) - * @return {number} modifier key state (desktop keyboards) - */ - static getModifierState(layerId: string): number { - var modifier=0; - if(layerId.indexOf('shift') >= 0) { - modifier |= Codes.modifierCodes['SHIFT']; + return matchBehavior; + } + + // FIXME: makes some bad assumptions. + static setMnemonicCode(Lkc: KeyEvent, shifted: boolean, capsActive: boolean) { + // K_SPACE is not handled by defaultKeyOutput for physical keystrokes unless using touch-aliased elements. + // It's also a "exception required, March 2013" for clickKey, so at least they both have this requirement. + if(Lkc.Lcode != Codes.keyCodes['K_SPACE']) { + // So long as the key name isn't prefixed with 'U_', we'll get a default mapping based on the Lcode value. + // We need to determine the mnemonic base character - for example, SHIFT + K_PERIOD needs to map to '>'. + let mappingEvent: KeyEvent = new KeyEvent(); + for(var key in Lkc) { + mappingEvent[key] = Lkc[key]; } - // The chiral checks must not be directly exclusive due each other to visual OSK feedback. - var ctrlMatched=false; - if(layerId.indexOf('leftctrl') >= 0) { - modifier |= Codes.modifierCodes['LCTRL']; - ctrlMatched=true; - } - if(layerId.indexOf('rightctrl') >= 0) { - modifier |= Codes.modifierCodes['RCTRL']; - ctrlMatched=true; - } - if(layerId.indexOf('ctrl') >= 0 && !ctrlMatched) { - modifier |= Codes.modifierCodes['CTRL']; - } + // To facilitate storing relevant commands, we should probably reverse-lookup + // the actual keyname instead. + mappingEvent.kName = 'K_xxxx'; + mappingEvent.Lmodifiers = (shifted ? 0x10 : 0); // mnemonic lookups only exist for default & shift layers. + var mappedChar: string = DefaultOutput.forAny(mappingEvent, true); - var altMatched=false; - if(layerId.indexOf('leftalt') >= 0) { - modifier |= Codes.modifierCodes['LALT']; - altMatched=true; - } - if(layerId.indexOf('rightalt') >= 0) { - modifier |= Codes.modifierCodes['RALT']; - altMatched=true; - } - if(layerId.indexOf('alt') >= 0 && !altMatched) { - modifier |= Codes.modifierCodes['ALT']; - } - - return modifier; - } - - /** - * Get state key state from layer id - * - * @param {string} layerId layer id (e.g. caps) - * @return {number} modifier key state (desktop keyboards) - */ - static getStateFromLayer(layerId: string): number { - var modifier=0; - - if(layerId.indexOf('caps') >= 0) { - modifier |= Codes.modifierCodes['CAPS']; + /* First, save a backup of the original code. This one won't needlessly trigger keyboard + * rules, but allows us to replicate/emulate commands after rule processing if needed. + * (Like backspaces) + */ + Lkc.vkCode = Lkc.Lcode; + if(mappedChar) { + // Will return 96 for 'a', which is a keycode corresponding to Codes.keyCodes('K_NP1') - a numpad key. + // That stated, we're in mnemonic mode - this keyboard's rules are based on the char codes. + Lkc.Lcode = mappedChar.charCodeAt(0); } else { - modifier |= Codes.modifierCodes['NO_CAPS']; - } - - return modifier; - } - - /** - * @summary Look up a custom virtual key code in the virtual key code dictionary KVKD. On first run, will build the dictionary. - * - * `VKDictionary` is constructed from the keyboard's `KVKD` member. This list is constructed - * at compile-time and is a list of 'additional' virtual key codes, starting at 256 (i.e. - * outside the range of standard virtual key codes). These additional codes are both - * `[T_xxx]` and `[U_xxxx]` custom key codes from the Keyman keyboard language. However, - * `[U_xxxx]` keys only generate an entry in `KVKD` if there is a corresponding rule that - * is associated with them in the keyboard rules. If the `[U_xxxx]` key code is only - * referenced as the id of a key in the touch layout, then it does not get an entry in - * the `KVKD` property. - * - * @private - * @param {string} keyName custom virtual key code to lookup in the dictionary - * @return {number} key code > 255 on success, or 0 if not found - */ - getVKDictionaryCode(keyName: string) { - var activeKeyboard = this.activeKeyboard; - if(!activeKeyboard.scriptObject['VKDictionary']) { - var a=[]; - if(typeof activeKeyboard.scriptObject['KVKD'] == 'string') { - // Build the VK dictionary - // TODO: Move the dictionary build into the compiler -- so compiler generates code such as following. - // Makes the VKDictionary member unnecessary. - // this.KVKD={"K_ABC":256,"K_DEF":257,...}; - var s=activeKeyboard.scriptObject['KVKD'].split(' '); - for(var i=0; i= 65 && Lkc.Lcode <= 90) /* 'A' - 'Z' */ || (Lkc.Lcode >= 97 && Lkc.Lcode <= 122) /* 'a' - 'z' */) { + Lkc.Lmodifiers ^= 0x10; // Flip the 'shifted' bit, so it'll act as the opposite key. + Lkc.Lcode ^= 0x20; // Flips the 'upper' vs 'lower' bit for the base 'a'-'z' ASCII alphabetics. } + } + } - if(Levent.Lcode == 8) { - // I3318 (always clear deadkeys after backspace) - outputTarget.deadkeys().clear(); - } else if(KeyboardProcessor.isModifier(Levent)) { - this.activeKeyboard.notify(Levent.Lcode, outputTarget, isKeyDown ? 1 : 0); - // For eventual integration - we bypass an OSK update for physical keystrokes when in touch mode. - if(!Levent.device.touchable) { - return this._UpdateVKShift(Levent); // I2187 - } else { - return true; + /** + * Get modifier key state from layer id + * + * @param {string} layerId layer id (e.g. ctrlshift) + * @return {number} modifier key state (desktop keyboards) + */ + static getModifierState(layerId: string): number { + var modifier=0; + if(layerId.indexOf('shift') >= 0) { + modifier |= Codes.modifierCodes['SHIFT']; + } + + // The chiral checks must not be directly exclusive due each other to visual OSK feedback. + var ctrlMatched=false; + if(layerId.indexOf('leftctrl') >= 0) { + modifier |= Codes.modifierCodes['LCTRL']; + ctrlMatched=true; + } + if(layerId.indexOf('rightctrl') >= 0) { + modifier |= Codes.modifierCodes['RCTRL']; + ctrlMatched=true; + } + if(layerId.indexOf('ctrl') >= 0 && !ctrlMatched) { + modifier |= Codes.modifierCodes['CTRL']; + } + + var altMatched=false; + if(layerId.indexOf('leftalt') >= 0) { + modifier |= Codes.modifierCodes['LALT']; + altMatched=true; + } + if(layerId.indexOf('rightalt') >= 0) { + modifier |= Codes.modifierCodes['RALT']; + altMatched=true; + } + if(layerId.indexOf('alt') >= 0 && !altMatched) { + modifier |= Codes.modifierCodes['ALT']; + } + + return modifier; + } + + /** + * Get state key state from layer id + * + * @param {string} layerId layer id (e.g. caps) + * @return {number} modifier key state (desktop keyboards) + */ + static getStateFromLayer(layerId: string): number { + var modifier=0; + + if(layerId.indexOf('caps') >= 0) { + modifier |= Codes.modifierCodes['CAPS']; + } else { + modifier |= Codes.modifierCodes['NO_CAPS']; + } + + return modifier; + } + + /** + * @summary Look up a custom virtual key code in the virtual key code dictionary KVKD. On first run, will build the dictionary. + * + * `VKDictionary` is constructed from the keyboard's `KVKD` member. This list is constructed + * at compile-time and is a list of 'additional' virtual key codes, starting at 256 (i.e. + * outside the range of standard virtual key codes). These additional codes are both + * `[T_xxx]` and `[U_xxxx]` custom key codes from the Keyman keyboard language. However, + * `[U_xxxx]` keys only generate an entry in `KVKD` if there is a corresponding rule that + * is associated with them in the keyboard rules. If the `[U_xxxx]` key code is only + * referenced as the id of a key in the touch layout, then it does not get an entry in + * the `KVKD` property. + * + * @private + * @param {string} keyName custom virtual key code to lookup in the dictionary + * @return {number} key code > 255 on success, or 0 if not found + */ + getVKDictionaryCode(keyName: string) { + var activeKeyboard = this.activeKeyboard; + if(!activeKeyboard.scriptObject['VKDictionary']) { + var a=[]; + if(typeof activeKeyboard.scriptObject['KVKD'] == 'string') { + // Build the VK dictionary + // TODO: Move the dictionary build into the compiler -- so compiler generates code such as following. + // Makes the VKDictionary member unnecessary. + // this.KVKD={"K_ABC":256,"K_DEF":257,...}; + var s=activeKeyboard.scriptObject['KVKD'].split(' '); + for(var i=0; i -// Defines the KeyEvent type. -/// /// +// 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; +export type Alternate = ProbabilityMass; - 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. } } \ No newline at end of file diff --git a/common/web/keyboard-processor/src/text/ruleBehavior.ts b/common/web/keyboard-processor/src/text/ruleBehavior.ts index e1bc700cf1..ad03f200d0 100644 --- a/common/web/keyboard-processor/src/text/ruleBehavior.ts +++ b/common/web/keyboard-processor/src/text/ruleBehavior.ts @@ -1,133 +1,139 @@ -namespace com.keyman.text { +/// + +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; + /** + * If predictive text is active, contains a Promise returning predictive Suggestions. + */ + predictionPromise?: Promise; - /** - * 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); + } } \ No newline at end of file diff --git a/common/web/keyboard-processor/src/text/systemStores.ts b/common/web/keyboard-processor/src/text/systemStores.ts index 00be331b72..13184baafe 100644 --- a/common/web/keyboard-processor/src/text/systemStores.ts +++ b/common/web/keyboard-processor/src/text/systemStores.ts @@ -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 Date: Thu, 24 Nov 2022 10:54:06 +0700 Subject: [PATCH 02/23] fix(web): fixes build output pathing --- common/web/keyboard-processor/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/web/keyboard-processor/tsconfig.json b/common/web/keyboard-processor/tsconfig.json index 1967f5fde6..343960082f 100644 --- a/common/web/keyboard-processor/tsconfig.json +++ b/common/web/keyboard-processor/tsconfig.json @@ -14,7 +14,7 @@ "baseUrl": "./", "outDir": "build/modules/", "tsBuildInfoFile": "build/modules/tsconfig.tsbuildinfo", - "rootDir": "./" + "rootDir": "./src" }, "references": [ { "path": "../../models/types" }, From 8a02ddc0f14fe5ef9e5b597da7375edffa291500 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 24 Nov 2022 11:58:45 +0700 Subject: [PATCH 03/23] feat(common/web): keyboard-processor esbuild bundling --- .../web/keyboard-processor/build-bundler.js | 25 +++++++++ common/web/keyboard-processor/src/index.ts | 52 +++++++++++++++++++ .../src/keyboards/activeLayout.ts | 2 +- .../src/text/keyboardProcessor.ts | 2 +- .../src/text/ruleBehavior.ts | 2 +- .../src/tsconfig.bundled.json | 15 ------ .../tests/temp-bundle-test.js | 16 ++++++ 7 files changed, 96 insertions(+), 18 deletions(-) create mode 100644 common/web/keyboard-processor/build-bundler.js create mode 100644 common/web/keyboard-processor/src/index.ts delete mode 100644 common/web/keyboard-processor/src/tsconfig.bundled.json create mode 100644 common/web/keyboard-processor/tests/temp-bundle-test.js diff --git a/common/web/keyboard-processor/build-bundler.js b/common/web/keyboard-processor/build-bundler.js new file mode 100644 index 0000000000..1bf88783ba --- /dev/null +++ b/common/web/keyboard-processor/build-bundler.js @@ -0,0 +1,25 @@ +/* + * 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'; + +esbuild.buildSync({ + entryPoints: ['build/modules/index.js'], + bundle: true, + sourcemap: true, + minify: true, + keepNames: true, + // 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/modules"], + outfile: "build/bundled/index.js", + tsconfig: 'tsconfig.json', + target: "es5" +}); \ No newline at end of file diff --git a/common/web/keyboard-processor/src/index.ts b/common/web/keyboard-processor/src/index.ts new file mode 100644 index 0000000000..3c2791f060 --- /dev/null +++ b/common/web/keyboard-processor/src/index.ts @@ -0,0 +1,52 @@ +// This file exists as a bundling intermediary that attempts to present all of +// keyboard-processor's offerings in the 'old', namespaced format - at least, +// as of the time that this submodule was converted to ES6 module use. + +import * as ActiveLayout from "keyboards/activeLayout.js"; +import * as DefaultLayout from "keyboards/defaultLayouts.js"; +import Keyboard, * as KeyboardContents from "keyboards/keyboard.js"; + +import Codes, * as CodesContents from "text/codes.js"; +import * as Deadkeys from "text/deadkeys.js"; +import DefaultOutput, * as DefaultOutputContents from "text/defaultOutput.js"; +import KbdInterface, * as KbdInterfaceContents from "text/kbdInterface.js"; +import KeyboardProcessor, * as KeyboardProcessorContents from "text/keyboardProcessor.js"; +import KeyEvent from "text/keyEvent.js"; +import KeyMapping from "text/keyMapping.js"; +import OutputTarget, * as OutputTargetContents from "text/outputTarget.js"; +import RuleBehavior from "text/ruleBehavior.js"; +import * as SystemStores from "text/systemStores.js"; + +import * as utils from "utils/build/modules/index.js"; + +export let com = { + keyman: { + keyboards: { + ...ActiveLayout, + ...DefaultLayout, + Keyboard, ...KeyboardContents + }, + text: { + Codes, ...CodesContents, + ...Deadkeys, + DefaultOutput, ...DefaultOutputContents, + KbdInterface, ...KbdInterfaceContents, + KeyboardProcessor, ...KeyboardProcessorContents, + KeyEvent, + KeyMapping, + OutputTarget, ...OutputTargetContents, + RuleBehavior, + ...SystemStores + }, + utils: {... utils} + } +} + +// A consequence of the spread-operator use + the modules with defaults. +delete com.keyman.keyboards.default; +delete com.keyman.text.default; + +// Force-exports it as the global it always was. +utils.globalObject()['com'] = com; + +export default com; diff --git a/common/web/keyboard-processor/src/keyboards/activeLayout.ts b/common/web/keyboard-processor/src/keyboards/activeLayout.ts index 49f2104375..fa68cf1499 100644 --- a/common/web/keyboard-processor/src/keyboards/activeLayout.ts +++ b/common/web/keyboard-processor/src/keyboards/activeLayout.ts @@ -5,7 +5,7 @@ import type { KeyDistribution } from "../text/keyEvent.js"; import type { LayoutKey, LayoutRow, LayoutLayer, LayoutFormFactor, ButtonClass } from "./defaultLayouts.js"; import type Keyboard from "./keyboard.js"; -import { KeyboardProcessor } from "text/keyboardProcessor.js"; +import KeyboardProcessor from "text/keyboardProcessor.js"; import { deepCopy, type DeviceSpec } from "utils/build/modules/index.js"; diff --git a/common/web/keyboard-processor/src/text/keyboardProcessor.ts b/common/web/keyboard-processor/src/text/keyboardProcessor.ts index cdd3367f05..58fd337cfd 100644 --- a/common/web/keyboard-processor/src/text/keyboardProcessor.ts +++ b/common/web/keyboard-processor/src/text/keyboardProcessor.ts @@ -32,7 +32,7 @@ export interface ProcessorInitOptions { variableStoreSerializer?: VariableStoreSerializer; } -export class KeyboardProcessor { +export default class KeyboardProcessor { public static readonly DEFAULT_OPTIONS: ProcessorInitOptions = { baseLayout: 'us' } diff --git a/common/web/keyboard-processor/src/text/ruleBehavior.ts b/common/web/keyboard-processor/src/text/ruleBehavior.ts index ad03f200d0..7c9f704f0b 100644 --- a/common/web/keyboard-processor/src/text/ruleBehavior.ts +++ b/common/web/keyboard-processor/src/text/ruleBehavior.ts @@ -1,7 +1,7 @@ /// import DefaultOutput from "./defaultOutput.js"; -import { KeyboardProcessor } from "./keyboardProcessor.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"; diff --git a/common/web/keyboard-processor/src/tsconfig.bundled.json b/common/web/keyboard-processor/src/tsconfig.bundled.json deleted file mode 100644 index 2555414b66..0000000000 --- a/common/web/keyboard-processor/src/tsconfig.bundled.json +++ /dev/null @@ -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} - ] -} diff --git a/common/web/keyboard-processor/tests/temp-bundle-test.js b/common/web/keyboard-processor/tests/temp-bundle-test.js new file mode 100644 index 0000000000..0e4433f12c --- /dev/null +++ b/common/web/keyboard-processor/tests/temp-bundle-test.js @@ -0,0 +1,16 @@ +/** + * A temporary file to validate that the bundled version really is bundled and is usable in a + * similar manner to its old format. + */ + +// Loads `com` into the global namespace. +import * as _ from '../build/bundled/index.js'; + +console.log(`Int code for ALT: ${com.keyman.text.Codes.modifierCodes['ALT']}`); + +console.log(new com.keyman.keyboards.Keyboard(null)); + +console.log(); + +// make sure we bundled `utils` as well! +console.log(`Verifying proper handling of version 16.0: ${new com.keyman.utils.Version([16, 0]).toString()}`); \ No newline at end of file From 6bc8ab579bedeaddef2dd207d9b2ef1dc25140c8 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 24 Nov 2022 12:32:39 +0700 Subject: [PATCH 04/23] change(common/web): converts common/web/recorder to ES modules --- common/web/recorder/build.sh | 33 +- common/web/recorder/src/index.ts | 1444 +++++++++-------- common/web/recorder/src/nodeProctor.ts | 185 ++- .../recorder/src/nodeProctor.tsconfig.json | 28 - common/web/recorder/src/proctor.ts | 91 +- common/web/recorder/src/tsconfig.json | 27 - common/web/recorder/tsconfig.json | 35 + 7 files changed, 914 insertions(+), 929 deletions(-) delete mode 100644 common/web/recorder/src/nodeProctor.tsconfig.json delete mode 100644 common/web/recorder/src/tsconfig.json create mode 100644 common/web/recorder/tsconfig.json diff --git a/common/web/recorder/build.sh b/common/web/recorder/build.sh index fb566a18c8..3bee5dc024 100755 --- a/common/web/recorder/build.sh +++ b/common/web/recorder/build.sh @@ -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 \ No newline at end of file diff --git a/common/web/recorder/src/index.ts b/common/web/recorder/src/index.ts index d01dcad71b..f845891df9 100644 --- a/common/web/recorder/src/index.ts +++ b/common/web/recorder/src/index.ts @@ -1,774 +1,780 @@ /// -namespace KMWRecorder { - //#region Defines the InputEventSpec set, used to reconstruct DOM-based events for browser-based simulation - export abstract class InputEventSpec { - type: "key" | "osk"; - static fromJSONObject(obj: any): InputEventSpec { - if(obj && obj.type) { - if(obj.type == "key") { - return new PhysicalInputEventSpec(obj); - } else if(obj.type == "osk") { - return new OSKInputEventSpec(obj); - } - } else { - throw new SyntaxError("Error in JSON format corresponding to an InputEventSpec!"); - } - } +import KeyEvent, { KeyDistribution } from "keyboard-processor/build/modules/text/keyEvent.js"; +import type OutputTarget from "keyboard-processor/build/modules/text/outputTarget.js"; +import { Mock } from "keyboard-processor/build/modules/text/outputTarget.js"; - toPrettyJSON(): string { - // We want the default, non-spaced JSON for this class, even when otherwise adding whitespace. - var str = JSON.stringify(this); - return str; +import Proctor from "./proctor.js"; + +import * as utils from "utils/build/modules/index.js"; + +//#region Defines the InputEventSpec set, used to reconstruct DOM-based events for browser-based simulation +export abstract class InputEventSpec { + type: "key" | "osk"; + static fromJSONObject(obj: any): InputEventSpec { + if(obj && obj.type) { + if(obj.type == "key") { + return new PhysicalInputEventSpec(obj); + } else if(obj.type == "osk") { + return new OSKInputEventSpec(obj); + } + } else { + throw new SyntaxError("Error in JSON format corresponding to an InputEventSpec!"); } } - export class PhysicalInputEventSpec extends InputEventSpec { - static readonly modifierCodes: { [mod:string]: number } = { - "Shift":0x0001, - "Control":0x0002, - "Alt":0x0004, - "Meta":0x0008, - "CapsLock":0x0010, - "NumLock":0x0020, - "ScrollLock":0x0040 - }; + toPrettyJSON(): string { + // We want the default, non-spaced JSON for this class, even when otherwise adding whitespace. + var str = JSON.stringify(this); + return str; + } +} - // KeyboardEvent properties - type: "key" = "key"; - key: string; - code: string; - keyCode: number; - modifierSet: number; - location: number; +export class PhysicalInputEventSpec extends InputEventSpec { + static readonly modifierCodes: { [mod:string]: number } = { + "Shift":0x0001, + "Control":0x0002, + "Alt":0x0004, + "Meta":0x0008, + "CapsLock":0x0010, + "NumLock":0x0020, + "ScrollLock":0x0040 + }; - constructor(e?: PhysicalInputEventSpec) { // parameter is used to reconstruct from JSON. - super(); + // KeyboardEvent properties + type: "key" = "key"; + key: string; + code: string; + keyCode: number; + modifierSet: number; + location: number; - if(e) { - this.key = e.key; - this.code = e.code; - this.keyCode = e.keyCode; - this.modifierSet = e.modifierSet; - this.location = e.location; - } - } + constructor(e?: PhysicalInputEventSpec) { // parameter is used to reconstruct from JSON. + super(); - getModifierState(key: string): boolean { - return (PhysicalInputEventSpec.modifierCodes[key] & this.modifierSet) != 0; - } - - generateModifierString(): string { - var list: string = ""; - - for(var key in PhysicalInputEventSpec.modifierCodes) { - if(this.getModifierState(key)) { - list += ((list != "" ? " " : "") + key); - } - } - - return list; + if(e) { + this.key = e.key; + this.code = e.code; + this.keyCode = e.keyCode; + this.modifierSet = e.modifierSet; + this.location = e.location; } } - export class OSKInputEventSpec extends InputEventSpec { - type: "osk" = "osk"; - keyID: string; - - // The parameter may be used to reconstruct the item from raw JSON. - constructor(e?: OSKInputEventSpec) { - super(); - if(e) { - this.keyID = e.keyID; - } - } - } - //#endregion - - export abstract class RecordedKeystroke { - type: "key" | "osk"; - - static fromJSONObject(obj: any): RecordedKeystroke { - if(obj && obj.type) { - if(obj.type == "key") { - return new RecordedPhysicalKeystroke(obj as RecordedPhysicalKeystroke); - } else if(obj && obj.type) { - return new RecordedSyntheticKeystroke(obj as RecordedSyntheticKeystroke); - } - } else { - throw new SyntaxError("Error in JSON format corresponding to a RecordedKeystroke!"); - } - } - - toPrettyJSON(): string { - // We want the default, non-spaced JSON for this class, even when otherwise adding whitespace. - var str = JSON.stringify(this); - return str; - } - - /** - * Returns an InputEventSpec that may be used to simulate the keystroke within a browser-based environment. - */ - abstract get inputEventSpec(): InputEventSpec; + getModifierState(key: string): boolean { + return (PhysicalInputEventSpec.modifierCodes[key] & this.modifierSet) != 0; } - export class RecordedPhysicalKeystroke extends RecordedKeystroke { - // KeyboardEvent properties - type: "key" = "key"; + generateModifierString(): string { + var list: string = ""; - keyCode: number; // may be different from eventSpec's value b/c keymapping - states: number; - modifiers: number; - modifierChanged: boolean; - isVirtualKey: boolean; - vkCode: number; // may be possible to eliminate; differences arise from mnemonics. - - eventSpec: PhysicalInputEventSpec; - - constructor(keystroke: RecordedPhysicalKeystroke) - constructor(keystroke: com.keyman.text.KeyEvent, eventSpec: PhysicalInputEventSpec) - constructor(keystroke: RecordedPhysicalKeystroke|com.keyman.text.KeyEvent, eventSpec?: PhysicalInputEventSpec) { - super(); - - if(keystroke instanceof com.keyman.text.KeyEvent || typeof keystroke.type === 'undefined') { - // Store what is necessary for headless event reconstruction. - keystroke = keystroke as com.keyman.text.KeyEvent; - this.keyCode = keystroke.Lcode; - this.states = keystroke.Lstates; - this.modifiers = keystroke.Lmodifiers; - this.modifierChanged = !!keystroke.LmodifierChange; - this.isVirtualKey = keystroke.LisVirtualKey; - this.vkCode = keystroke.vkCode; - - // Also store the DOM-based event spec for use in integrated testing. - this.eventSpec = eventSpec; - } else { - // It might be a raw object, from JSON. - this.keyCode = keystroke.keyCode; - this.states = keystroke.states; - this.modifiers = keystroke.modifiers; - this.modifierChanged = keystroke.modifierChanged; - this.isVirtualKey = keystroke.isVirtualKey; - this.vkCode = keystroke.vkCode; - - this.eventSpec = new PhysicalInputEventSpec(keystroke.eventSpec); // must also be reconstructed. + for(var key in PhysicalInputEventSpec.modifierCodes) { + if(this.getModifierState(key)) { + list += ((list != "" ? " " : "") + key); } } - get inputEventSpec(): InputEventSpec { - return this.eventSpec; + return list; + } +} + +export class OSKInputEventSpec extends InputEventSpec { + type: "osk" = "osk"; + keyID: string; + + // The parameter may be used to reconstruct the item from raw JSON. + constructor(e?: OSKInputEventSpec) { + super(); + if(e) { + this.keyID = e.keyID; + } + } +} +//#endregion + +export abstract class RecordedKeystroke { + type: "key" | "osk"; + + static fromJSONObject(obj: any): RecordedKeystroke { + if(obj && obj.type) { + if(obj.type == "key") { + return new RecordedPhysicalKeystroke(obj as RecordedPhysicalKeystroke); + } else if(obj && obj.type) { + return new RecordedSyntheticKeystroke(obj as RecordedSyntheticKeystroke); + } + } else { + throw new SyntaxError("Error in JSON format corresponding to a RecordedKeystroke!"); } } - export class RecordedSyntheticKeystroke extends RecordedKeystroke { - // KeyboardEvent properties - type: "osk" = "osk"; - - keyName: string; - layer: string; - - keyDistribution?: com.keyman.text.KeyDistribution; - - constructor(keystroke: RecordedSyntheticKeystroke) - constructor(keystroke: com.keyman.text.KeyEvent) - constructor(keystroke: RecordedSyntheticKeystroke|com.keyman.text.KeyEvent) { - super(); - - if(keystroke instanceof com.keyman.text.KeyEvent || typeof keystroke.type === 'undefined') { - keystroke = keystroke as com.keyman.text.KeyEvent; - // Store what is necessary for headless event reconstruction. - - // Also store the DOM-based event spec for use in integrated testing. - this.layer = keystroke.kbdLayer; - this.keyName = keystroke.kName; - this.keyDistribution = keystroke.keyDistribution; - } else { - // It might be a raw object, from JSON. - this.layer = keystroke.layer; - this.keyName = keystroke.keyName; - this.keyDistribution = keystroke.keyDistribution; - } - } - - get inputEventSpec(): InputEventSpec { - let eventSpec = new OSKInputEventSpec(); - eventSpec.keyID = this.layer + '-' + this.keyName; - - return eventSpec; - } - } - - export abstract class TestSequence { - inputs: KeyRecord[]; - output: string; - msg?: string; - - abstract hasOSKInteraction(): boolean; - - test(proctor: Proctor, target?: com.keyman.text.OutputTarget): {success: boolean, result: string} { - // Start with an empty OutputTarget and a fresh KeyboardProcessor. - if(!target) { - target = new com.keyman.text.Mock(); - } - - proctor.before(); - - let result = proctor.simulateSequence(this, target); - proctor.assertEquals(result, this.output, this.msg); - - return {success: (result == this.output), result: result}; - } - - toPrettyJSON(): string { - var str = "{ "; - if(this.output) { - str += "\"output\": \"" + this.output + "\", "; - } - str += "\"inputs\": [\n"; - for(var i = 0; i < this.inputs.length; i++) { - str += " " + this.inputs[i].toPrettyJSON() + ((i == this.inputs.length-1) ? "\n" : ",\n"); - } - if(this.msg) { - str += "], \"message\": \"" + this.msg + "\" }"; - } else { - str += "]}"; - } - return str; - } - } - - export class InputEventSpecSequence extends TestSequence { - inputs: InputEventSpec[]; - output: string; - msg?: string; - - constructor(ins?: InputEventSpec[] | InputEventSpecSequence, outs?: string, msg?: string) { - super(); - - if(ins) { - if(ins instanceof Array) { - this.inputs = [].concat(ins); - } else { - // We're constructing from existing JSON. - this.inputs = []; - - for(var ie=0; ie < ins.inputs.length; ie++) { - this.inputs.push(InputEventSpec.fromJSONObject(ins.inputs[ie])); - } - - this.output = ins.output; - this.msg = ins.msg; - return; - } - } else { - this.inputs = []; - } - - if(outs) { - this.output = outs; - } - - if(msg) { - this.msg = msg; - } - } - - addInput(event: InputEventSpec, output: string) { - this.inputs.push(event); - this.output = output; - } - - hasOSKInteraction(): boolean { - for(var i=0; i < this.inputs.length; i++) { - if(this.inputs[i] instanceof OSKInputEventSpec) { - return true; - } - } - - return false; - } - } - - export class RecordedKeystrokeSequence extends TestSequence { - inputs: RecordedKeystroke[]; - output: string; - msg?: string; - - constructor(ins?: RecordedKeystroke[], outs?: string, msg?: string) - constructor(sequence: RecordedKeystrokeSequence) - constructor(ins?: RecordedKeystroke[] | RecordedKeystrokeSequence, outs?: string, msg?: string) { - super(); - - if(ins) { - if(ins instanceof Array) { - this.inputs = [].concat(ins); - } else { - // We're constructing from existing JSON. - this.inputs = []; - - for(var ie=0; ie < ins.inputs.length; ie++) { - this.inputs.push(RecordedKeystroke.fromJSONObject(ins.inputs[ie])); - } - - this.output = ins.output; - this.msg = ins.msg; - return; - } - } else { - this.inputs = []; - } - - if(outs) { - this.output = outs; - } - - if(msg) { - this.msg = msg; - } - } - - addInput(event: RecordedKeystroke, output: string) { - this.inputs.push(event); - this.output = output; - } - - hasOSKInteraction(): boolean { - for(var i=0; i < this.inputs.length; i++) { - if(this.inputs[i] instanceof RecordedSyntheticKeystroke) { - return true; - } - } - - return false; - } - } - - class FontStubForLanguage { - family: string; - source: string[]; - - constructor(activeStubEntry: any) { - this.family = activeStubEntry.family; - - var src = activeStubEntry.files; - if(!(src instanceof Array)) { - src = [ src ]; - } - - this.source = []; - for(var i=0; i < src.length; i++) { - this.source.push(activeStubEntry.path + src[i]); - } - } - } - - export class LanguageStubForKeyboard { - id: string; - name: string; - region: string; - font?: FontStubForLanguage; - oskFont?: FontStubForLanguage; - - constructor(activeStub: any) { - if(activeStub.KLC) { - this.id = activeStub.KLC; - this.name = activeStub.KL; - this.region = activeStub.KR; - - // Fonts. - if(activeStub.KFont) { - this.font = new FontStubForLanguage(activeStub.KFont); - } - if(activeStub.KOskFont) { - this.oskFont = new FontStubForLanguage(activeStub.KOskFont); - } - } else { - this.id = activeStub.id; - this.name = activeStub.name; - this.region = activeStub.region; - - // If we end up adding functionality to FontStubForLanguage, we'll need to properly reconstruct these. - this.font = activeStub.font; - this.oskFont = activeStub.oskFont; - } - } - } - - export class KeyboardStub { - id: string; - name: string; - filename: string; - languages: LanguageStubForKeyboard | LanguageStubForKeyboard[]; - - // Constructs a stub usable with KeymanWeb's addKeyboards() API function from - // the internally-tracked ActiveStub value for that keyboard. - constructor(json?: KeyboardStub) { - if(json) { - this.id = json.id; - this.name = json.name; - this.filename = json.filename; - - if(!Array.isArray(json.languages)) { - this.languages = new LanguageStubForKeyboard(json.languages); - } else { - this.languages = []; - for(var i=0; i < json.languages.length; i++) { - this.languages.push(new LanguageStubForKeyboard(json.languages[i])); - } - } - } - } - - getFirstLanguage() { - if(this.languages instanceof LanguageStubForKeyboard) { - return this.languages.id; - } else { - return this.languages[0].id; - } - } - } - - type TARGET = 'hardware'|'desktop'|'phone'|'tablet'; - type OS = 'windows'|'android'|'ios'|'macosx'|'linux'; - type BROWSER = 'chrome'|'firefox'|'safari'|'opera'; // ! no 'edge' detection in KMW! - - export class Constraint { - target: TARGET; - validOSList?: OS[]; - validBrowsers?: BROWSER[]; - - constructor(target: TARGET|Constraint, validOSList?: OS[], validBrowsers?: BROWSER[]) { - if(typeof(target) == 'string') { - this.target = target; - this.validOSList = validOSList; - this.validBrowsers = validBrowsers; - } else { - var json = target; - this.target = json.target; - this.validOSList = json.validOSList; - this.validBrowsers = json.validBrowsers; - } - } - - matchesClient(device: com.keyman.utils.DeviceSpec, usingOSK?: boolean) { - // #1: Platform check. - if(usingOSK === true) { - if(this.target != device.formFactor) { - return false; - } - } else if(usingOSK === false) { - if(this.target != 'hardware') { - return false; - } - } else if(this.target != device.formFactor && this.target != 'hardware') { - return false; - } - - if(this.validOSList) { - if(this.validOSList.indexOf(device.OS as OS) == -1) { - return false; - } - } - - if(this.validBrowsers) { - if(this.validBrowsers.indexOf(device.browser as BROWSER) == -1) { - return false; - } - } - - return true; - } - - // Checks if another Constraint instance is functionally identical to this one. - equals(other: Constraint) { - if(this.target != other.target) { - return false; - } - - var list1 = this.validOSList ? this.validOSList : ['any']; - var list2 = other.validOSList ? other.validOSList : ['any']; - - if(list1.sort().join(',') != list2.sort().join(',')) { - return false; - } - - list1 = this.validBrowsers ? this.validBrowsers : ['web']; - list2 = other.validBrowsers ? other.validBrowsers : ['web']; - - if(list1.sort().join(',') != list2.sort().join(',')) { - return false; - } - - return true; - } - } - - export class TestFailure { - constraint: Constraint; - test: InputEventSpecSequence; - result: string; - - constructor(constraint: Constraint, test: InputEventSpecSequence, output: string) { - this.constraint = constraint; - this.test = test; - this.result = output; - } - } - - export interface TestSet> { - constraint: Constraint; - - addTest(seq: Sequence): void; - isValidForDevice(device: com.keyman.utils.DeviceSpec, usingOSK?: boolean): boolean; - test(proctor: Proctor): TestFailure[]; + toPrettyJSON(): string { + // We want the default, non-spaced JSON for this class, even when otherwise adding whitespace. + var str = JSON.stringify(this); + return str; } /** - * The core constraint-specific test set definition used for testing versions 10.0 to 13.0. + * Returns an InputEventSpec that may be used to simulate the keystroke within a browser-based environment. */ - export class EventSpecTestSet implements TestSet { - constraint: Constraint; - testSet: InputEventSpecSequence[]; + abstract get inputEventSpec(): InputEventSpec; +} - constructor(constraint: Constraint|EventSpecTestSet) { - if("target" in constraint) { - this.constraint = constraint as Constraint; - this.testSet = []; - } else { - var json = constraint as EventSpecTestSet; - this.constraint = new Constraint(json.constraint); - this.testSet = []; +export class RecordedPhysicalKeystroke extends RecordedKeystroke { + // KeyboardEvent properties + type: "key" = "key"; - // Clone each test sequence / reconstruct from methodless JSON object. - for(var i=0; i < json.testSet.length; i++) { - this.testSet.push(new InputEventSpecSequence(json.testSet[i])); - } - } - } + keyCode: number; // may be different from eventSpec's value b/c keymapping + states: number; + modifiers: number; + modifierChanged: boolean; + isVirtualKey: boolean; + vkCode: number; // may be possible to eliminate; differences arise from mnemonics. - addTest(seq: InputEventSpecSequence) { - this.testSet.push(seq); - } + eventSpec: PhysicalInputEventSpec; - // Used to determine if the current EventSpecTestSet is applicable to be run on a device. - isValidForDevice(device: com.keyman.utils.DeviceSpec, usingOSK?: boolean) { - return this.constraint.matchesClient(device, usingOSK); - } + constructor(keystroke: RecordedPhysicalKeystroke) + constructor(keystroke: KeyEvent, eventSpec: PhysicalInputEventSpec) + constructor(keystroke: RecordedPhysicalKeystroke|KeyEvent, eventSpec?: PhysicalInputEventSpec) { + super(); - // Validity should be checked before calling this method. - test(proctor: Proctor): TestFailure[] { - var failures: TestFailure[] = []; - let testSet = this.testSet; + if(keystroke instanceof KeyEvent || typeof keystroke.type === 'undefined') { + // Store what is necessary for headless event reconstruction. + keystroke = keystroke as KeyEvent; + this.keyCode = keystroke.Lcode; + this.states = keystroke.Lstates; + this.modifiers = keystroke.Lmodifiers; + this.modifierChanged = !!keystroke.LmodifierChange; + this.isVirtualKey = keystroke.LisVirtualKey; + this.vkCode = keystroke.vkCode; - for(var i=0; i < testSet.length; i++) { - var testSeq = this[i]; - var simResult = testSet[i].test(proctor); - if(!simResult.success) { - // Failed test! - failures.push(new TestFailure(this.constraint, testSeq, simResult.result)); - } - } + // Also store the DOM-based event spec for use in integrated testing. + this.eventSpec = eventSpec; + } else { + // It might be a raw object, from JSON. + this.keyCode = keystroke.keyCode; + this.states = keystroke.states; + this.modifiers = keystroke.modifiers; + this.modifierChanged = keystroke.modifierChanged; + this.isVirtualKey = keystroke.isVirtualKey; + this.vkCode = keystroke.vkCode; - return failures.length > 0 ? failures : null; + this.eventSpec = new PhysicalInputEventSpec(keystroke.eventSpec); // must also be reconstructed. } } + get inputEventSpec(): InputEventSpec { + return this.eventSpec; + } +} + +export class RecordedSyntheticKeystroke extends RecordedKeystroke { + // KeyboardEvent properties + type: "osk" = "osk"; + + keyName: string; + layer: string; + + keyDistribution?: KeyDistribution; + + constructor(keystroke: RecordedSyntheticKeystroke) + constructor(keystroke: KeyEvent) + constructor(keystroke: RecordedSyntheticKeystroke|KeyEvent) { + super(); + + if(keystroke instanceof KeyEvent || typeof keystroke.type === 'undefined') { + keystroke = keystroke as KeyEvent; + // Store what is necessary for headless event reconstruction. + + // Also store the DOM-based event spec for use in integrated testing. + this.layer = keystroke.kbdLayer; + this.keyName = keystroke.kName; + this.keyDistribution = keystroke.keyDistribution; + } else { + // It might be a raw object, from JSON. + this.layer = keystroke.layer; + this.keyName = keystroke.keyName; + this.keyDistribution = keystroke.keyDistribution; + } + } + + get inputEventSpec(): InputEventSpec { + let eventSpec = new OSKInputEventSpec(); + eventSpec.keyID = this.layer + '-' + this.keyName; + + return eventSpec; + } +} + +export abstract class TestSequence { + inputs: KeyRecord[]; + output: string; + msg?: string; + + abstract hasOSKInteraction(): boolean; + + test(proctor: Proctor, target?: OutputTarget): {success: boolean, result: string} { + // Start with an empty OutputTarget and a fresh KeyboardProcessor. + if(!target) { + target = new Mock(); + } + + proctor.before(); + + let result = proctor.simulateSequence(this, target); + proctor.assertEquals(result, this.output, this.msg); + + return {success: (result == this.output), result: result}; + } + + toPrettyJSON(): string { + var str = "{ "; + if(this.output) { + str += "\"output\": \"" + this.output + "\", "; + } + str += "\"inputs\": [\n"; + for(var i = 0; i < this.inputs.length; i++) { + str += " " + this.inputs[i].toPrettyJSON() + ((i == this.inputs.length-1) ? "\n" : ",\n"); + } + if(this.msg) { + str += "], \"message\": \"" + this.msg + "\" }"; + } else { + str += "]}"; + } + return str; + } +} + +export class InputEventSpecSequence extends TestSequence { + inputs: InputEventSpec[]; + output: string; + msg?: string; + + constructor(ins?: InputEventSpec[] | InputEventSpecSequence, outs?: string, msg?: string) { + super(); + + if(ins) { + if(ins instanceof Array) { + this.inputs = [].concat(ins); + } else { + // We're constructing from existing JSON. + this.inputs = []; + + for(var ie=0; ie < ins.inputs.length; ie++) { + this.inputs.push(InputEventSpec.fromJSONObject(ins.inputs[ie])); + } + + this.output = ins.output; + this.msg = ins.msg; + return; + } + } else { + this.inputs = []; + } + + if(outs) { + this.output = outs; + } + + if(msg) { + this.msg = msg; + } + } + + addInput(event: InputEventSpec, output: string) { + this.inputs.push(event); + this.output = output; + } + + hasOSKInteraction(): boolean { + for(var i=0; i < this.inputs.length; i++) { + if(this.inputs[i] instanceof OSKInputEventSpec) { + return true; + } + } + + return false; + } +} + +export class RecordedKeystrokeSequence extends TestSequence { + inputs: RecordedKeystroke[]; + output: string; + msg?: string; + + constructor(ins?: RecordedKeystroke[], outs?: string, msg?: string) + constructor(sequence: RecordedKeystrokeSequence) + constructor(ins?: RecordedKeystroke[] | RecordedKeystrokeSequence, outs?: string, msg?: string) { + super(); + + if(ins) { + if(ins instanceof Array) { + this.inputs = [].concat(ins); + } else { + // We're constructing from existing JSON. + this.inputs = []; + + for(var ie=0; ie < ins.inputs.length; ie++) { + this.inputs.push(RecordedKeystroke.fromJSONObject(ins.inputs[ie])); + } + + this.output = ins.output; + this.msg = ins.msg; + return; + } + } else { + this.inputs = []; + } + + if(outs) { + this.output = outs; + } + + if(msg) { + this.msg = msg; + } + } + + addInput(event: RecordedKeystroke, output: string) { + this.inputs.push(event); + this.output = output; + } + + hasOSKInteraction(): boolean { + for(var i=0; i < this.inputs.length; i++) { + if(this.inputs[i] instanceof RecordedSyntheticKeystroke) { + return true; + } + } + + return false; + } +} + +class FontStubForLanguage { + family: string; + source: string[]; + + constructor(activeStubEntry: any) { + this.family = activeStubEntry.family; + + var src = activeStubEntry.files; + if(!(src instanceof Array)) { + src = [ src ]; + } + + this.source = []; + for(var i=0; i < src.length; i++) { + this.source.push(activeStubEntry.path + src[i]); + } + } +} + +export class LanguageStubForKeyboard { + id: string; + name: string; + region: string; + font?: FontStubForLanguage; + oskFont?: FontStubForLanguage; + + constructor(activeStub: any) { + if(activeStub.KLC) { + this.id = activeStub.KLC; + this.name = activeStub.KL; + this.region = activeStub.KR; + + // Fonts. + if(activeStub.KFont) { + this.font = new FontStubForLanguage(activeStub.KFont); + } + if(activeStub.KOskFont) { + this.oskFont = new FontStubForLanguage(activeStub.KOskFont); + } + } else { + this.id = activeStub.id; + this.name = activeStub.name; + this.region = activeStub.region; + + // If we end up adding functionality to FontStubForLanguage, we'll need to properly reconstruct these. + this.font = activeStub.font; + this.oskFont = activeStub.oskFont; + } + } +} + +export class KeyboardStub { + id: string; + name: string; + filename: string; + languages: LanguageStubForKeyboard | LanguageStubForKeyboard[]; + + // Constructs a stub usable with KeymanWeb's addKeyboards() API function from + // the internally-tracked ActiveStub value for that keyboard. + constructor(json?: KeyboardStub) { + if(json) { + this.id = json.id; + this.name = json.name; + this.filename = json.filename; + + if(!Array.isArray(json.languages)) { + this.languages = new LanguageStubForKeyboard(json.languages); + } else { + this.languages = []; + for(var i=0; i < json.languages.length; i++) { + this.languages.push(new LanguageStubForKeyboard(json.languages[i])); + } + } + } + } + + getFirstLanguage() { + if(this.languages instanceof LanguageStubForKeyboard) { + return this.languages.id; + } else { + return this.languages[0].id; + } + } +} + +type TARGET = 'hardware'|'desktop'|'phone'|'tablet'; +type OS = 'windows'|'android'|'ios'|'macosx'|'linux'; +type BROWSER = 'chrome'|'firefox'|'safari'|'opera'; // ! no 'edge' detection in KMW! + +export class Constraint { + target: TARGET; + validOSList?: OS[]; + validBrowsers?: BROWSER[]; + + constructor(target: TARGET|Constraint, validOSList?: OS[], validBrowsers?: BROWSER[]) { + if(typeof(target) == 'string') { + this.target = target; + this.validOSList = validOSList; + this.validBrowsers = validBrowsers; + } else { + var json = target; + this.target = json.target; + this.validOSList = json.validOSList; + this.validBrowsers = json.validBrowsers; + } + } + + matchesClient(device: utils.DeviceSpec, usingOSK?: boolean) { + // #1: Platform check. + if(usingOSK === true) { + if(this.target != device.formFactor) { + return false; + } + } else if(usingOSK === false) { + if(this.target != 'hardware') { + return false; + } + } else if(this.target != device.formFactor && this.target != 'hardware') { + return false; + } + + if(this.validOSList) { + if(this.validOSList.indexOf(device.OS as OS) == -1) { + return false; + } + } + + if(this.validBrowsers) { + if(this.validBrowsers.indexOf(device.browser as BROWSER) == -1) { + return false; + } + } + + return true; + } + + // Checks if another Constraint instance is functionally identical to this one. + equals(other: Constraint) { + if(this.target != other.target) { + return false; + } + + var list1 = this.validOSList ? this.validOSList : ['any']; + var list2 = other.validOSList ? other.validOSList : ['any']; + + if(list1.sort().join(',') != list2.sort().join(',')) { + return false; + } + + list1 = this.validBrowsers ? this.validBrowsers : ['web']; + list2 = other.validBrowsers ? other.validBrowsers : ['web']; + + if(list1.sort().join(',') != list2.sort().join(',')) { + return false; + } + + return true; + } +} + +export class TestFailure { + constraint: Constraint; + test: InputEventSpecSequence; + result: string; + + constructor(constraint: Constraint, test: InputEventSpecSequence, output: string) { + this.constraint = constraint; + this.test = test; + this.result = output; + } +} + +export interface TestSet> { + constraint: Constraint; + + addTest(seq: Sequence): void; + isValidForDevice(device: utils.DeviceSpec, usingOSK?: boolean): boolean; + test(proctor: Proctor): TestFailure[]; +} + +/** + * The core constraint-specific test set definition used for testing versions 10.0 to 13.0. + */ +export class EventSpecTestSet implements TestSet { + constraint: Constraint; + testSet: InputEventSpecSequence[]; + + constructor(constraint: Constraint|EventSpecTestSet) { + if("target" in constraint) { + this.constraint = constraint as Constraint; + this.testSet = []; + } else { + var json = constraint as EventSpecTestSet; + this.constraint = new Constraint(json.constraint); + this.testSet = []; + + // Clone each test sequence / reconstruct from methodless JSON object. + for(var i=0; i < json.testSet.length; i++) { + this.testSet.push(new InputEventSpecSequence(json.testSet[i])); + } + } + } + + addTest(seq: InputEventSpecSequence) { + this.testSet.push(seq); + } + + // Used to determine if the current EventSpecTestSet is applicable to be run on a device. + isValidForDevice(device: utils.DeviceSpec, usingOSK?: boolean) { + return this.constraint.matchesClient(device, usingOSK); + } + + // Validity should be checked before calling this method. + test(proctor: Proctor): TestFailure[] { + var failures: TestFailure[] = []; + let testSet = this.testSet; + + for(var i=0; i < testSet.length; i++) { + var testSeq = this[i]; + var simResult = testSet[i].test(proctor); + if(!simResult.success) { + // Failed test! + failures.push(new TestFailure(this.constraint, testSeq, simResult.result)); + } + } + + return failures.length > 0 ? failures : null; + } +} + +/** + * The core constraint-specific test set definition used for testing versions 10.0 to 13.0. + */ +export class RecordedSequenceTestSet implements TestSet { + constraint: Constraint; + testSet: RecordedKeystrokeSequence[]; + + constructor(constraint: Constraint|RecordedSequenceTestSet) { + if("target" in constraint) { + this.constraint = constraint as Constraint; + this.testSet = []; + } else { + var json = constraint as RecordedSequenceTestSet; + this.constraint = new Constraint(json.constraint); + this.testSet = []; + + // Clone each test sequence / reconstruct from methodless JSON object. + for(var i=0; i < json.testSet.length; i++) { + this.testSet.push(new RecordedKeystrokeSequence(json.testSet[i])); + } + } + } + + addTest(seq: RecordedKeystrokeSequence) { + this.testSet.push(seq); + } + + // Used to determine if the current EventSpecTestSet is applicable to be run on a device. + isValidForDevice(device: utils.DeviceSpec, usingOSK?: boolean) { + return this.constraint.matchesClient(device, usingOSK); + } + + // Validity should be checked before calling this method. + test(proctor: Proctor): TestFailure[] { + var failures: TestFailure[] = []; + let testSet = this.testSet; + + for(var i=0; i < testSet.length; i++) { + var testSeq = this[i]; + var simResult = testSet[i].test(proctor); + if(!simResult.success) { + // Failed test! + failures.push(new TestFailure(this.constraint, testSeq, simResult.result)); + } + } + + return failures.length > 0 ? failures : null; + } + + toTestName(): string { + let name = "constraint: for " + this.constraint.target; + + if(this.constraint.target == 'hardware') { + name += " keyboard"; + } else { + name += " OSK"; + } + if(this.constraint.validOSList) { + name += " on OS of " + JSON.stringify(this.constraint.validOSList); + } + if(this.constraint.validBrowsers) { + name += " in browser of " + JSON.stringify(this.constraint.validBrowsers); + } + + return name; + } +} + +export class KeyboardTest { + /** + * Indicates what version of KMW's recorder the spec conforms to. + */ + public specVersion: utils.Version = KeyboardTest.CURRENT_VERSION; + /** - * The core constraint-specific test set definition used for testing versions 10.0 to 13.0. + * The version of KMW in which the Recorder was first written. Worked from 10.0 to 13.0 with + * only backward-compatible changes and minor tweaks to conform to internal API shifts. */ - export class RecordedSequenceTestSet implements TestSet { - constraint: Constraint; - testSet: RecordedKeystrokeSequence[]; + public static readonly FALLBACK_VERSION = new utils.Version("10.0"); + public static readonly CURRENT_VERSION = new utils.Version("14.0"); - constructor(constraint: Constraint|RecordedSequenceTestSet) { - if("target" in constraint) { - this.constraint = constraint as Constraint; - this.testSet = []; - } else { - var json = constraint as RecordedSequenceTestSet; - this.constraint = new Constraint(json.constraint); - this.testSet = []; + /** + * The stub information to be passed into keyman.addKeyboards() in order to run the test. + */ + keyboard: KeyboardStub; - // Clone each test sequence / reconstruct from methodless JSON object. - for(var i=0; i < json.testSet.length; i++) { - this.testSet.push(new RecordedKeystrokeSequence(json.testSet[i])); - } - } - } + /** + * The master array of test sets, each of which specifies constraints a client must fulfill for + * the tests contained therein to be valid. + */ + inputTestSets: TestSet[]; - addTest(seq: RecordedKeystrokeSequence) { - this.testSet.push(seq); - } - - // Used to determine if the current EventSpecTestSet is applicable to be run on a device. - isValidForDevice(device: com.keyman.utils.DeviceSpec, usingOSK?: boolean) { - return this.constraint.matchesClient(device, usingOSK); - } - - // Validity should be checked before calling this method. - test(proctor: Proctor): TestFailure[] { - var failures: TestFailure[] = []; - let testSet = this.testSet; - - for(var i=0; i < testSet.length; i++) { - var testSeq = this[i]; - var simResult = testSet[i].test(proctor); - if(!simResult.success) { - // Failed test! - failures.push(new TestFailure(this.constraint, testSeq, simResult.result)); - } - } - - return failures.length > 0 ? failures : null; - } - - toTestName(): string { - let name = "constraint: for " + this.constraint.target; - - if(this.constraint.target == 'hardware') { - name += " keyboard"; - } else { - name += " OSK"; - } - if(this.constraint.validOSList) { - name += " on OS of " + JSON.stringify(this.constraint.validOSList); - } - if(this.constraint.validBrowsers) { - name += " in browser of " + JSON.stringify(this.constraint.validBrowsers); - } - - return name; - } - } - - export class KeyboardTest { - /** - * Indicates what version of KMW's recorder the spec conforms to. - */ - public specVersion: com.keyman.utils.Version = KeyboardTest.CURRENT_VERSION; - - /** - * The version of KMW in which the Recorder was first written. Worked from 10.0 to 13.0 with - * only backward-compatible changes and minor tweaks to conform to internal API shifts. - */ - public static readonly FALLBACK_VERSION = new com.keyman.utils.Version("10.0"); - public static readonly CURRENT_VERSION = new com.keyman.utils.Version("14.0"); - - /** - * The stub information to be passed into keyman.addKeyboards() in order to run the test. - */ - keyboard: KeyboardStub; - - /** - * The master array of test sets, each of which specifies constraints a client must fulfill for - * the tests contained therein to be valid. - */ - inputTestSets: TestSet[]; - - /** - * Reconstructs a KeyboardTest object from its JSON representation, restoring its methods. - * @param fromJSON - */ - constructor(fromJSON?: string|KeyboardStub|KeyboardTest) { - if(!fromJSON) { - this.keyboard = null; - this.inputTestSets = []; - return; - } else if(typeof(fromJSON) == 'string') { - fromJSON = JSON.parse(fromJSON) as KeyboardTest; - } else if(fromJSON instanceof KeyboardStub) { - this.keyboard = fromJSON; - this.inputTestSets = []; - return; - } - - if(!fromJSON.specVersion) { - fromJSON.specVersion = KeyboardTest.FALLBACK_VERSION; - } else { - // Is serialized to a String when saved. - fromJSON.specVersion = new com.keyman.utils.Version(fromJSON.specVersion as unknown as string); - } - - this.keyboard = new KeyboardStub(fromJSON.keyboard); + /** + * Reconstructs a KeyboardTest object from its JSON representation, restoring its methods. + * @param fromJSON + */ + constructor(fromJSON?: string|KeyboardStub|KeyboardTest) { + if(!fromJSON) { + this.keyboard = null; this.inputTestSets = []; - this.specVersion = fromJSON.specVersion; + return; + } else if(typeof(fromJSON) == 'string') { + fromJSON = JSON.parse(fromJSON) as KeyboardTest; + } else if(fromJSON instanceof KeyboardStub) { + this.keyboard = fromJSON; + this.inputTestSets = []; + return; + } - if(this.specVersion.equals(KeyboardTest.FALLBACK_VERSION)) { - // Top-level test spec: EventSpecTestSet, based entirely on browser events. - for(var i=0; i < fromJSON.inputTestSets.length; i++) { - this.inputTestSets[i] = new EventSpecTestSet(fromJSON.inputTestSets[i] as EventSpecTestSet); - } - } else { - for(var i=0; i < fromJSON.inputTestSets.length; i++) { - this.inputTestSets[i] = new RecordedSequenceTestSet(fromJSON.inputTestSets[i] as RecordedSequenceTestSet); + if(!fromJSON.specVersion) { + fromJSON.specVersion = KeyboardTest.FALLBACK_VERSION; + } else { + // Is serialized to a String when saved. + fromJSON.specVersion = new utils.Version(fromJSON.specVersion as unknown as string); + } + + this.keyboard = new KeyboardStub(fromJSON.keyboard); + this.inputTestSets = []; + this.specVersion = fromJSON.specVersion; + + if(this.specVersion.equals(KeyboardTest.FALLBACK_VERSION)) { + // Top-level test spec: EventSpecTestSet, based entirely on browser events. + for(var i=0; i < fromJSON.inputTestSets.length; i++) { + this.inputTestSets[i] = new EventSpecTestSet(fromJSON.inputTestSets[i] as EventSpecTestSet); + } + } else { + for(var i=0; i < fromJSON.inputTestSets.length; i++) { + this.inputTestSets[i] = new RecordedSequenceTestSet(fromJSON.inputTestSets[i] as RecordedSequenceTestSet); + } + } + } + + addTest(constraint: Constraint, seq: RecordedKeystrokeSequence) { + if(!this.specVersion.equals(KeyboardTest.CURRENT_VERSION)) { + throw new Error("The currently-loaded test was built to an outdated specification and may not be altered."); + } + + for(var i=0; i < this.inputTestSets.length; i++) { + if(this.inputTestSets[i].constraint.equals(constraint)) { + this.inputTestSets[i].addTest(seq); + return; + } + } + + var newSet = new RecordedSequenceTestSet(new Constraint(constraint)); + this.inputTestSets.push(newSet); + newSet.addTest(seq); + } + + test(proctor: Proctor) { + var setHasRun = false; + var failures: TestFailure[] = []; + + proctor.beforeAll(); + + // The original test spec requires a browser environment and thus requires its own `.run` implementation. + if(!(proctor.compatibleWithSuite(this))) { + throw Error("Cannot perform version " + KeyboardTest.FALLBACK_VERSION + "-based testing outside of browser-based environments."); + } + + // Otherwise, the test spec instances will know how to run in any currently-supported environment. + for(var i = 0; i < this.inputTestSets.length; i++) { + var testSet = this.inputTestSets[i]; + + if(proctor.matchesTestSet(testSet)) { + var testFailures = testSet.test(proctor); + if(testFailures) { + failures = failures.concat(testFailures); } + setHasRun = true; } } - addTest(constraint: Constraint, seq: RecordedKeystrokeSequence) { - if(!this.specVersion.equals(KeyboardTest.CURRENT_VERSION)) { - throw new Error("The currently-loaded test was built to an outdated specification and may not be altered."); - } - - for(var i=0; i < this.inputTestSets.length; i++) { - if(this.inputTestSets[i].constraint.equals(constraint)) { - this.inputTestSets[i].addTest(seq); - return; - } - } - - var newSet = new RecordedSequenceTestSet(new Constraint(constraint)); - this.inputTestSets.push(newSet); - newSet.addTest(seq); + if(!setHasRun) { + // The sets CAN be empty, allowing silent failure if/when we actually want that. + console.warn("No test sets for this keyboard were applicable for this device!"); } - test(proctor: Proctor) { - var setHasRun = false; - var failures: TestFailure[] = []; - - proctor.beforeAll(); - - // The original test spec requires a browser environment and thus requires its own `.run` implementation. - if(!(proctor.compatibleWithSuite(this))) { - throw Error("Cannot perform version " + KeyboardTest.FALLBACK_VERSION + "-based testing outside of browser-based environments."); - } - - // Otherwise, the test spec instances will know how to run in any currently-supported environment. - for(var i = 0; i < this.inputTestSets.length; i++) { - var testSet = this.inputTestSets[i]; - - if(proctor.matchesTestSet(testSet)) { - var testFailures = testSet.test(proctor); - if(testFailures) { - failures = failures.concat(testFailures); - } - setHasRun = true; - } - } - - if(!setHasRun) { - // The sets CAN be empty, allowing silent failure if/when we actually want that. - console.warn("No test sets for this keyboard were applicable for this device!"); - } - - // Allow the method's caller to trigger a 'fail'. - if(failures.length > 0) { - return failures; - } else { - return null; - } + // Allow the method's caller to trigger a 'fail'. + if(failures.length > 0) { + return failures; + } else { + return null; } + } - isEmpty() { - return this.inputTestSets.length == 0; - } + isEmpty() { + return this.inputTestSets.length == 0; + } - toPrettyJSON() { - return JSON.stringify(this, null, ' '); - } + toPrettyJSON() { + return JSON.stringify(this, null, ' '); + } - get isLegacy(): boolean { - return !this.specVersion.equals(KeyboardTest.CURRENT_VERSION); - } + get isLegacy(): boolean { + return !this.specVersion.equals(KeyboardTest.CURRENT_VERSION); } } \ No newline at end of file diff --git a/common/web/recorder/src/nodeProctor.ts b/common/web/recorder/src/nodeProctor.ts index 0ae4b5adcc..2031e450e4 100644 --- a/common/web/recorder/src/nodeProctor.ts +++ b/common/web/recorder/src/nodeProctor.ts @@ -1,94 +1,105 @@ +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 from "keyboard-processor/build/modules/keyboards/keyboard.js"; +import type KeyEvent from "keyboard-processor/build/modules/text/keyEvent.js"; +import KeyboardProcessor from "keyboard-processor/build/modules/text/keyboardProcessor.js"; +import type OutputTarget from "keyboard-processor/build/modules/text/outputTarget.js"; +import { Mock } from "keyboard-processor/build/modules/text/outputTarget.js"; - constructor(keyboard: com.keyman.keyboards.Keyboard, device: com.keyman.utils.DeviceSpec, assert: AssertCallback) { - super(device, assert); +import DeviceSpec from "utils/build/modules/deviceSpec.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) { - // KeyboardProcessor is abstract enough to run tests aimed at any platform. - return true; - } - - simulateSequence(sequence: TestSequence, 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; \ No newline at end of file + 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) { + // KeyboardProcessor is abstract enough to run tests aimed at any platform. + return true; + } + + simulateSequence(sequence: TestSequence, 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(); + } +} \ No newline at end of file diff --git a/common/web/recorder/src/nodeProctor.tsconfig.json b/common/web/recorder/src/nodeProctor.tsconfig.json deleted file mode 100644 index 5b80993dc6..0000000000 --- a/common/web/recorder/src/nodeProctor.tsconfig.json +++ /dev/null @@ -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" } - ] -} diff --git a/common/web/recorder/src/proctor.ts b/common/web/recorder/src/proctor.ts index 38ffd9997a..bedc461237 100644 --- a/common/web/recorder/src/proctor.ts +++ b/common/web/recorder/src/proctor.ts @@ -1,50 +1,53 @@ -namespace KMWRecorder { - export type AssertCallback = (s1: any, s2: any, msg?: string) => void; +import { type DeviceSpec } from "utils/build/modules/index.js"; +import type OutputTarget from "keyboard-processor/build/modules/text/outputTarget.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); - 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); - - /** - * Simulates the specified test sequence for use in testing. - * @param sequence The recorded sequence, generally provided by a test set. - */ - abstract simulateSequence(sequence: TestSequence, 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, target?: OutputTarget); } \ No newline at end of file diff --git a/common/web/recorder/src/tsconfig.json b/common/web/recorder/src/tsconfig.json deleted file mode 100644 index 00cb00a482..0000000000 --- a/common/web/recorder/src/tsconfig.json +++ /dev/null @@ -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" } - ] -} diff --git a/common/web/recorder/tsconfig.json b/common/web/recorder/tsconfig.json new file mode 100644 index 0000000000..c490c2280a --- /dev/null +++ b/common/web/recorder/tsconfig.json @@ -0,0 +1,35 @@ +{ + "extends": "../../../tsconfig-base.json", + + "compilerOptions": { + "allowJs": true, + "module": "es6", + "declaration": true, + "inlineSources": true, + "inlineSourceMap": true, + "target": "es5", + "types": ["node"], + "lib": ["es6"], + "baseUrl": "./", + "outDir": "build/modules/", + "tsBuildInfoFile": "build/modules/tsconfig.tsbuildinfo", + "rootDir": "./src" + }, + + "include": [ + "src/**/*.ts" + ], + + "references": [ + { "path": "../keyman-version" }, + { "path": "../utils/" }, + { "path": "../keyboard-processor/" }, + { "path": "../lm-message-types" } + ], + + "paths": { + "keyboard-processor": ["../keyboard-processor/build" ], + "keyman-version": ["../keyman-version/build" ], + "utils": ["../utils/build" ] + } +} From 14b40a4301dfc97b8271391f1a065a04d6596d8b Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 24 Nov 2022 13:55:54 +0700 Subject: [PATCH 05/23] fix(common/web): in-place Node module path resolution --- common/web/keyboard-processor/src/index.ts | 28 +++++++++---------- .../src/keyboards/activeLayout.ts | 4 +-- .../src/keyboards/defaultLayouts.ts | 2 +- .../src/keyboards/keyboard.ts | 2 +- .../src/text/kbdInterface.ts | 2 +- .../keyboard-processor/src/text/keyEvent.ts | 2 +- .../src/text/keyboardProcessor.ts | 6 ++-- common/web/keyboard-processor/tsconfig.json | 1 + 8 files changed, 24 insertions(+), 23 deletions(-) diff --git a/common/web/keyboard-processor/src/index.ts b/common/web/keyboard-processor/src/index.ts index 3c2791f060..7a8c06e115 100644 --- a/common/web/keyboard-processor/src/index.ts +++ b/common/web/keyboard-processor/src/index.ts @@ -2,22 +2,22 @@ // keyboard-processor's offerings in the 'old', namespaced format - at least, // as of the time that this submodule was converted to ES6 module use. -import * as ActiveLayout from "keyboards/activeLayout.js"; -import * as DefaultLayout from "keyboards/defaultLayouts.js"; -import Keyboard, * as KeyboardContents from "keyboards/keyboard.js"; +import * as ActiveLayout from "./keyboards/activeLayout.js"; +import * as DefaultLayout from "./keyboards/defaultLayouts.js"; +import Keyboard, * as KeyboardContents from "./keyboards/keyboard.js"; -import Codes, * as CodesContents from "text/codes.js"; -import * as Deadkeys from "text/deadkeys.js"; -import DefaultOutput, * as DefaultOutputContents from "text/defaultOutput.js"; -import KbdInterface, * as KbdInterfaceContents from "text/kbdInterface.js"; -import KeyboardProcessor, * as KeyboardProcessorContents from "text/keyboardProcessor.js"; -import KeyEvent from "text/keyEvent.js"; -import KeyMapping from "text/keyMapping.js"; -import OutputTarget, * as OutputTargetContents from "text/outputTarget.js"; -import RuleBehavior from "text/ruleBehavior.js"; -import * as SystemStores from "text/systemStores.js"; +import Codes, * as CodesContents from "./text/codes.js"; +import * as Deadkeys from "./text/deadkeys.js"; +import DefaultOutput, * as DefaultOutputContents from "./text/defaultOutput.js"; +import KbdInterface, * as KbdInterfaceContents from "./text/kbdInterface.js"; +import KeyboardProcessor, * as KeyboardProcessorContents from "./text/keyboardProcessor.js"; +import KeyEvent from "./text/keyEvent.js"; +import KeyMapping from "./text/keyMapping.js"; +import OutputTarget, * as OutputTargetContents from "./text/outputTarget.js"; +import RuleBehavior from "./text/ruleBehavior.js"; +import * as SystemStores from "./text/systemStores.js"; -import * as utils from "utils/build/modules/index.js"; +import * as utils from "@keymanapp/web-utils/build/modules/index.js"; export let com = { keyman: { diff --git a/common/web/keyboard-processor/src/keyboards/activeLayout.ts b/common/web/keyboard-processor/src/keyboards/activeLayout.ts index fa68cf1499..ee053200d8 100644 --- a/common/web/keyboard-processor/src/keyboards/activeLayout.ts +++ b/common/web/keyboard-processor/src/keyboards/activeLayout.ts @@ -5,9 +5,9 @@ import type { KeyDistribution } from "../text/keyEvent.js"; import type { LayoutKey, LayoutRow, LayoutLayer, LayoutFormFactor, ButtonClass } from "./defaultLayouts.js"; import type Keyboard from "./keyboard.js"; -import KeyboardProcessor from "text/keyboardProcessor.js"; +import KeyboardProcessor from "../text/keyboardProcessor.js"; -import { deepCopy, type DeviceSpec } from "utils/build/modules/index.js"; +import { deepCopy, type DeviceSpec } from "@keymanapp/web-utils/build/modules/index.js"; // TS 3.9 changed behavior of getters to make them // non-enumerable by default. This broke our 'polyfill' diff --git a/common/web/keyboard-processor/src/keyboards/defaultLayouts.ts b/common/web/keyboard-processor/src/keyboards/defaultLayouts.ts index 503dc8ba9b..cafd937d33 100644 --- a/common/web/keyboard-processor/src/keyboards/defaultLayouts.ts +++ b/common/web/keyboard-processor/src/keyboards/defaultLayouts.ts @@ -5,7 +5,7 @@ import Codes from "../text/codes.js"; import type Keyboard from "./keyboard.js"; -import { Version, deepCopy } from "utils/build/modules/index.js"; +import { Version, deepCopy } from "@keymanapp/web-utils/build/modules/index.js"; export type KLS = {[layerName: string]: string[]}; diff --git a/common/web/keyboard-processor/src/keyboards/keyboard.ts b/common/web/keyboard-processor/src/keyboards/keyboard.ts index a64ee92ba8..fb9e0f7d4f 100644 --- a/common/web/keyboard-processor/src/keyboards/keyboard.ts +++ b/common/web/keyboard-processor/src/keyboards/keyboard.ts @@ -6,7 +6,7 @@ import type OutputTarget from "../text/outputTarget.js"; import type { ComplexKeyboardStore } from "../text/kbdInterface.js"; -import { Version, DeviceSpec } from "utils/build/modules/index.js"; +import { Version, DeviceSpec } from "@keymanapp/web-utils/build/modules/index.js"; /** * Stores preprocessed properties of a keyboard for quick retrieval later. diff --git a/common/web/keyboard-processor/src/text/kbdInterface.ts b/common/web/keyboard-processor/src/text/kbdInterface.ts index 249c8ad8fd..85c4023f4d 100644 --- a/common/web/keyboard-processor/src/text/kbdInterface.ts +++ b/common/web/keyboard-processor/src/text/kbdInterface.ts @@ -18,7 +18,7 @@ import { Mock } from "./outputTarget.js"; import RuleBehavior from "./ruleBehavior.js"; import Keyboard, { VariableStoreDictionary } from "../keyboards/keyboard.js"; -import { type DeviceSpec } from "utils/build/modules/index.js"; +import { type DeviceSpec } from "@keymanapp/web-utils/build/modules/index.js"; //#endregion diff --git a/common/web/keyboard-processor/src/text/keyEvent.ts b/common/web/keyboard-processor/src/text/keyEvent.ts index 64b722e211..4ea291a910 100644 --- a/common/web/keyboard-processor/src/text/keyEvent.ts +++ b/common/web/keyboard-processor/src/text/keyEvent.ts @@ -1,5 +1,5 @@ import type Keyboard from "../keyboards/keyboard.js"; -import type DeviceSpec from "utils/build/modules/deviceSpec.js"; +import type DeviceSpec from "@keymanapp/web-utils/build/modules/deviceSpec.js"; // Represents a probability distribution over a keyboard's keys. // Defined here to avoid compilation issues. diff --git a/common/web/keyboard-processor/src/text/keyboardProcessor.ts b/common/web/keyboard-processor/src/text/keyboardProcessor.ts index 58fd337cfd..5508ad907b 100644 --- a/common/web/keyboard-processor/src/text/keyboardProcessor.ts +++ b/common/web/keyboard-processor/src/text/keyboardProcessor.ts @@ -3,17 +3,17 @@ import Codes from "./codes.js"; import type Keyboard from "../keyboards/keyboard.js"; import KeyEvent from "./keyEvent.js"; -import { Layouts } from "keyboards/defaultLayouts"; +import { Layouts } from "../keyboards/defaultLayouts.js"; import type { MutableSystemStore } from "./systemStores.js"; -import DefaultOutput, { EmulationKeystrokes } from "./defaultOutput"; +import DefaultOutput, { EmulationKeystrokes } from "./defaultOutput.js"; import type OutputTarget from "./outputTarget.js"; import { Mock } from "./outputTarget.js"; import KeyboardInterface, { SystemStoreIDs, VariableStore } from "./kbdInterface.js"; import RuleBehavior from "./ruleBehavior.js"; -import { DeviceSpec, globalObject as getGlobalObject } from "utils/build/modules/index.js"; +import { DeviceSpec, globalObject as getGlobalObject } from "@keymanapp/web-utils/build/modules/index.js"; // #endregion diff --git a/common/web/keyboard-processor/tsconfig.json b/common/web/keyboard-processor/tsconfig.json index 343960082f..9c714c1699 100644 --- a/common/web/keyboard-processor/tsconfig.json +++ b/common/web/keyboard-processor/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "allowJs": true, "module": "es6", + "moduleResolution": "Node", "declaration": true, "inlineSources": true, "sourceMap": true, From 7fe069a8f3bc77eb2b56a3f9a3ba4d335e21a6ee Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 24 Nov 2022 13:56:54 +0700 Subject: [PATCH 06/23] chore(common/web): minor cleanup --- common/web/keyboard-processor/tsconfig.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/common/web/keyboard-processor/tsconfig.json b/common/web/keyboard-processor/tsconfig.json index 9c714c1699..96826b11eb 100644 --- a/common/web/keyboard-processor/tsconfig.json +++ b/common/web/keyboard-processor/tsconfig.json @@ -22,9 +22,5 @@ { "path": "../keyman-version/" }, { "path": "../utils/" } ], - "include": ["./src/**/*.ts"], - "paths": { - "keyman-version": ["../keyman-version/build" ], - "utils": ["../utils/build" ] - } + "include": ["./src/**/*.ts"] } From 63b074a0103608ad6ddfa4c456b963fc258ad2e5 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 24 Nov 2022 13:58:52 +0700 Subject: [PATCH 07/23] fix(common/web): matching updates for recorder testing submodule --- common/web/recorder/src/index.ts | 8 ++++---- common/web/recorder/src/nodeProctor.ts | 12 ++++++------ common/web/recorder/src/proctor.ts | 4 ++-- common/web/recorder/tsconfig.json | 7 +------ 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/common/web/recorder/src/index.ts b/common/web/recorder/src/index.ts index f845891df9..7b9821c83f 100644 --- a/common/web/recorder/src/index.ts +++ b/common/web/recorder/src/index.ts @@ -1,12 +1,12 @@ /// -import KeyEvent, { KeyDistribution } from "keyboard-processor/build/modules/text/keyEvent.js"; -import type OutputTarget from "keyboard-processor/build/modules/text/outputTarget.js"; -import { Mock } from "keyboard-processor/build/modules/text/outputTarget.js"; +import KeyEvent, { KeyDistribution } from "@keymanapp/keyboard-processor/build/modules/text/keyEvent.js"; +import type OutputTarget from "@keymanapp/keyboard-processor/build/modules/text/outputTarget.js"; +import { Mock } from "@keymanapp/keyboard-processor/build/modules/text/outputTarget.js"; import Proctor from "./proctor.js"; -import * as utils from "utils/build/modules/index.js"; +import * as utils from "@keymanapp/web-utils/build/modules/index.js"; //#region Defines the InputEventSpec set, used to reconstruct DOM-based events for browser-based simulation export abstract class InputEventSpec { diff --git a/common/web/recorder/src/nodeProctor.ts b/common/web/recorder/src/nodeProctor.ts index 2031e450e4..24d920592b 100644 --- a/common/web/recorder/src/nodeProctor.ts +++ b/common/web/recorder/src/nodeProctor.ts @@ -8,13 +8,13 @@ import { RecordedSyntheticKeystroke } from "./index.js"; -import Keyboard from "keyboard-processor/build/modules/keyboards/keyboard.js"; -import type KeyEvent from "keyboard-processor/build/modules/text/keyEvent.js"; -import KeyboardProcessor from "keyboard-processor/build/modules/text/keyboardProcessor.js"; -import type OutputTarget from "keyboard-processor/build/modules/text/outputTarget.js"; -import { Mock } from "keyboard-processor/build/modules/text/outputTarget.js"; +import Keyboard from "@keymanapp/keyboard-processor/build/modules/keyboards/keyboard.js"; +import type KeyEvent from "@keymanapp/keyboard-processor/build/modules/text/keyEvent.js"; +import KeyboardProcessor from "@keymanapp/keyboard-processor/build/modules/text/keyboardProcessor.js"; +import type OutputTarget from "@keymanapp/keyboard-processor/build/modules/text/outputTarget.js"; +import { Mock } from "@keymanapp/keyboard-processor/build/modules/text/outputTarget.js"; -import DeviceSpec from "utils/build/modules/deviceSpec.js"; +import DeviceSpec from "@keymanapp/web-utils/build/modules/deviceSpec.js"; export default class NodeProctor extends Proctor { private keyboard: Keyboard; diff --git a/common/web/recorder/src/proctor.ts b/common/web/recorder/src/proctor.ts index bedc461237..026bef56c1 100644 --- a/common/web/recorder/src/proctor.ts +++ b/common/web/recorder/src/proctor.ts @@ -1,5 +1,5 @@ -import { type DeviceSpec } from "utils/build/modules/index.js"; -import type OutputTarget from "keyboard-processor/build/modules/text/outputTarget.js"; +import { type DeviceSpec } from "@keymanapp/web-utils/build/modules/index.js"; +import type OutputTarget from "@keymanapp/keyboard-processor/build/modules/text/outputTarget.js"; import type { KeyboardTest, TestSet, TestSequence } from "./index.js"; diff --git a/common/web/recorder/tsconfig.json b/common/web/recorder/tsconfig.json index c490c2280a..22cbba849b 100644 --- a/common/web/recorder/tsconfig.json +++ b/common/web/recorder/tsconfig.json @@ -4,6 +4,7 @@ "compilerOptions": { "allowJs": true, "module": "es6", + "moduleResolution": "Node", "declaration": true, "inlineSources": true, "inlineSourceMap": true, @@ -26,10 +27,4 @@ { "path": "../keyboard-processor/" }, { "path": "../lm-message-types" } ], - - "paths": { - "keyboard-processor": ["../keyboard-processor/build" ], - "keyman-version": ["../keyman-version/build" ], - "utils": ["../utils/build" ] - } } From 918a24c37a05d1b37896ebc75d852145b4505272 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 24 Nov 2022 14:41:22 +0700 Subject: [PATCH 08/23] chore(common/web): converts keyboard-processor unit tests --- common/web/keyboard-processor/build.sh | 2 -- .../tests/cases/basic-engine.js | 22 +++++++-------- .../tests/cases/basic-init.js | 10 +++---- .../tests/cases/chirality.js | 23 ++++++++-------- .../tests/cases/deadkeys.js | 20 +++++++------- .../tests/cases/engine/context.js | 27 +++++++++---------- .../tests/cases/engine/notany_context.js | 25 ++++++++--------- .../tests/cases/engine/stores.js | 14 +++++----- .../cases/engine/unmatched_final_group.js | 18 ++++++------- .../tests/cases/transcriptions.js | 25 ++++------------- .../tests/cases/versions.js | 22 +++++++-------- common/web/recorder/package.json | 1 + 12 files changed, 91 insertions(+), 118 deletions(-) diff --git a/common/web/keyboard-processor/build.sh b/common/web/keyboard-processor/build.sh index e93e8b64fa..50f5822335 100755 --- a/common/web/keyboard-processor/build.sh +++ b/common/web/keyboard-processor/build.sh @@ -54,8 +54,6 @@ if builder_start_action build; then 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= diff --git a/common/web/keyboard-processor/tests/cases/basic-engine.js b/common/web/keyboard-processor/tests/cases/basic-engine.js index 4b886d1184..063b9268e9 100644 --- a/common/web/keyboard-processor/tests/cases/basic-engine.js +++ b/common/web/keyboard-processor/tests/cases/basic-engine.js @@ -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/modules/text/keyboardProcessor.js'; +import { KeyboardTest } from '@keymanapp/recorder-core/build/modules/index.js'; +import NodeProctor from '@keymanapp/recorder-core/build/modules/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); }); } diff --git a/common/web/keyboard-processor/tests/cases/basic-init.js b/common/web/keyboard-processor/tests/cases/basic-init.js index 9fb576cd90..d3182c45a4 100644 --- a/common/web/keyboard-processor/tests/cases/basic-init.js +++ b/common/web/keyboard-processor/tests/cases/basic-init.js @@ -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/modules/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. diff --git a/common/web/keyboard-processor/tests/cases/chirality.js b/common/web/keyboard-processor/tests/cases/chirality.js index 7d7436cf2c..11e59f7ad3 100644 --- a/common/web/keyboard-processor/tests/cases/chirality.js +++ b/common/web/keyboard-processor/tests/cases/chirality.js @@ -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/modules/text/codes.js'; +import KeyboardInterface from '@keymanapp/keyboard-processor/build/modules/text/kbdInterface.js'; +import KeyboardProcessor from '@keymanapp/keyboard-processor/build/modules/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/modules/index.js'; +import NodeProctor from '@keymanapp/recorder-core/build/modules/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 { diff --git a/common/web/keyboard-processor/tests/cases/deadkeys.js b/common/web/keyboard-processor/tests/cases/deadkeys.js index 0510c60ea2..adfe9ec193 100644 --- a/common/web/keyboard-processor/tests/cases/deadkeys.js +++ b/common/web/keyboard-processor/tests/cases/deadkeys.js @@ -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/modules/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/modules/index.js'; +import NodeProctor from '@keymanapp/recorder-core/build/modules/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); }); } diff --git a/common/web/keyboard-processor/tests/cases/engine/context.js b/common/web/keyboard-processor/tests/cases/engine/context.js index 8b8fb34e9e..4b54b18859 100644 --- a/common/web/keyboard-processor/tests/cases/engine/context.js +++ b/common/web/keyboard-processor/tests/cases/engine/context.js @@ -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/modules/text/keyboardProcessor.js'; +import { Mock } from '@keymanapp/keyboard-processor/build/modules/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/modules/index.js'; +import NodeProctor from '@keymanapp/recorder-core/build/modules/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! diff --git a/common/web/keyboard-processor/tests/cases/engine/notany_context.js b/common/web/keyboard-processor/tests/cases/engine/notany_context.js index 2c0ba3585d..f2a1231ec3 100644 --- a/common/web/keyboard-processor/tests/cases/engine/notany_context.js +++ b/common/web/keyboard-processor/tests/cases/engine/notany_context.js @@ -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/modules/text/keyboardProcessor.js'; +import { Mock } from '@keymanapp/keyboard-processor/build/modules/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/modules/index.js'; +import NodeProctor from '@keymanapp/recorder-core/build/modules/nodeProctor.js'; -let KMWRecorder = require('../../../../recorder/build/nodeProctor'); +import extendString from '@keymanapp/web-utils/build/modules/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); } } diff --git a/common/web/keyboard-processor/tests/cases/engine/stores.js b/common/web/keyboard-processor/tests/cases/engine/stores.js index 530ed0c931..2f0eb5eaa5 100644 --- a/common/web/keyboard-processor/tests/cases/engine/stores.js +++ b/common/web/keyboard-processor/tests/cases/engine/stores.js @@ -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/modules/keyboards/keyboard.js'; +import KeyboardProcessor from '@keymanapp/keyboard-processor/build/modules/text/keyboardProcessor.js'; -// Required initialization setup. -global.com = KeyboardProcessor.com; // exports all keyboard-processor namespacing. +import extendString from '@keymanapp/web-utils/build/modules/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; diff --git a/common/web/keyboard-processor/tests/cases/engine/unmatched_final_group.js b/common/web/keyboard-processor/tests/cases/engine/unmatched_final_group.js index 68a04c2ee9..7e449d1117 100644 --- a/common/web/keyboard-processor/tests/cases/engine/unmatched_final_group.js +++ b/common/web/keyboard-processor/tests/cases/engine/unmatched_final_group.js @@ -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/modules/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/modules/index.js'; +import NodeProctor from '@keymanapp/recorder-core/build/modules/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); }); }); \ No newline at end of file diff --git a/common/web/keyboard-processor/tests/cases/transcriptions.js b/common/web/keyboard-processor/tests/cases/transcriptions.js index 1161955720..74bbaf4a9f 100644 --- a/common/web/keyboard-processor/tests/cases/transcriptions.js +++ b/common/web/keyboard-processor/tests/cases/transcriptions.js @@ -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/modules/text/outputTarget.js'; +import extendString from '@keymanapp/web-utils/build/modules/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. diff --git a/common/web/keyboard-processor/tests/cases/versions.js b/common/web/keyboard-processor/tests/cases/versions.js index 2e7f6ae142..949de1e726 100644 --- a/common/web/keyboard-processor/tests/cases/versions.js +++ b/common/web/keyboard-processor/tests/cases/versions.js @@ -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/modules/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); diff --git a/common/web/recorder/package.json b/common/web/recorder/package.json index fe645fb57b..5fb8b9ba85 100644 --- a/common/web/recorder/package.json +++ b/common/web/recorder/package.json @@ -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" From 0eb722ac212041d43a07690531c5f4d5e1bae720 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 24 Nov 2022 14:58:18 +0700 Subject: [PATCH 09/23] fix(common/web): test breakage from loss of former Codes access point --- common/test/resources/keyboards/khmer_angkor.js | 15 ++++++++++++--- common/test/resources/keyboards/test_deadkeys.js | 15 ++++++++++++--- .../test/resources/keyboards/web_context_tests.js | 13 +++++++++++-- .../keyboard-processor/src/text/kbdInterface.ts | 3 +++ 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/common/test/resources/keyboards/khmer_angkor.js b/common/test/resources/keyboards/khmer_angkor.js index 7b797e7e69..204d3e90c3 100644 --- a/common/test/resources/keyboards/khmer_angkor.js +++ b/common/test/resources/keyboards/khmer_angkor.js @@ -6,8 +6,17 @@ KeymanWeb.KR(new Keyboard_khmer_angkor()); } function Keyboard_khmer_angkor() { - var modCodes = com.keyman.text.Codes.modifierCodes; - var keyCodes = com.keyman.text.Codes.keyCodes; + var Codes, modCodes, keyCodes; + + if(KeymanWeb.Codes) { + // ES Module attachment point + Codes = KeymanWeb.Codes; + } else if (typeof com != 'undefined' && com.keyman && com.keyman.text && com.keyman.text.Codes) { + // Pre-modularized attachment point + Codes = com.keyman.text.Codes; + } + var modCodes = Codes.modifierCodes; + var keyCodes = Codes.keyCodes; this.KI="Keyboard_khmer_angkor"; this.KN="Khmer Angkor"; @@ -2804,7 +2813,7 @@ function Keyboard_khmer_angkor() k.KO(-1,t,"»"); } if(m) { - + k.KDC(-1,t); r=this.g_normalise(t,e); } diff --git a/common/test/resources/keyboards/test_deadkeys.js b/common/test/resources/keyboards/test_deadkeys.js index c55165b8f6..a8937ada32 100644 --- a/common/test/resources/keyboards/test_deadkeys.js +++ b/common/test/resources/keyboards/test_deadkeys.js @@ -6,8 +6,17 @@ KeymanWeb.KR(new Keyboard_test_deadkeys()); } function Keyboard_test_deadkeys() { - var modCodes = com.keyman.text.Codes.modifierCodes; - var keyCodes = com.keyman.text.Codes.keyCodes; + var Codes, modCodes, keyCodes; + + if(KeymanWeb.Codes) { + // ES Module attachment point + Codes = KeymanWeb.Codes; + } else if (typeof com != 'undefined' && com.keyman && com.keyman.text && com.keyman.text.Codes) { + // Pre-modularized attachment point + Codes = com.keyman.text.Codes; + } + var modCodes = Codes.modifierCodes; + var keyCodes = Codes.keyCodes; this.KI="Keyboard_test_deadkeys"; this.KN="Keyman Deadkey Stress-Tester"; @@ -325,7 +334,7 @@ function Keyboard_test_deadkeys() k.KDO(-1,t,17); } if(m) { - + k.KDC(-1,t); r=this.g_dead_reorder(t,e); } diff --git a/common/test/resources/keyboards/web_context_tests.js b/common/test/resources/keyboards/web_context_tests.js index 2c1b93ec8a..aa48bd2791 100644 --- a/common/test/resources/keyboards/web_context_tests.js +++ b/common/test/resources/keyboards/web_context_tests.js @@ -6,8 +6,17 @@ KeymanWeb.KR(new Keyboard_web_context_tests()); } function Keyboard_web_context_tests() { - var modCodes = keyman.osk.modifierCodes; - var keyCodes = keyman.osk.keyCodes; + var Codes, modCodes, keyCodes; + + if(KeymanWeb.Codes) { + // ES Module attachment point + Codes = KeymanWeb.Codes; + } else if (typeof com != 'undefined' && com.keyman && com.keyman.text && com.keyman.text.Codes) { + // Pre-modularized attachment point + Codes = com.keyman.text.Codes; + } + var modCodes = Codes.modifierCodes; + var keyCodes = Codes.keyCodes; this._v=(typeof keyman!="undefined"&&typeof keyman.version=="string")?parseInt(keyman.version,10):9; this.KI="Keyboard_web_context_tests"; diff --git a/common/web/keyboard-processor/src/text/kbdInterface.ts b/common/web/keyboard-processor/src/text/kbdInterface.ts index 85c4023f4d..dee0b335e0 100644 --- a/common/web/keyboard-processor/src/text/kbdInterface.ts +++ b/common/web/keyboard-processor/src/text/kbdInterface.ts @@ -212,6 +212,9 @@ export default class KeyboardInterface { variableStoreSerializer?: VariableStoreSerializer; + // A 'reference point' that debug keyboards may use to access KMW's code constants. + public readonly Codes = Codes; + constructor(variableStoreSerializer: VariableStoreSerializer = null) { this.systemStores = {}; From 45e09d043cb9459d2ec8cedcee55d9da5ec781e4 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 24 Nov 2022 15:00:54 +0700 Subject: [PATCH 10/23] fix(common/web): unit test breakage from missing 'let' --- common/web/keyboard-processor/tests/cases/chirality.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/web/keyboard-processor/tests/cases/chirality.js b/common/web/keyboard-processor/tests/cases/chirality.js index 11e59f7ad3..d1c17cf0e7 100644 --- a/common/web/keyboard-processor/tests/cases/chirality.js +++ b/common/web/keyboard-processor/tests/cases/chirality.js @@ -91,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); @@ -110,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; @@ -249,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); }); }); From ec6c90aad2750247683616ee04d312107951186a Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 24 Nov 2022 15:08:41 +0700 Subject: [PATCH 11/23] change(common/web): new Codes access point now fully readonly --- common/web/keyboard-processor/src/text/kbdInterface.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/web/keyboard-processor/src/text/kbdInterface.ts b/common/web/keyboard-processor/src/text/kbdInterface.ts index dee0b335e0..dbf4a10c7a 100644 --- a/common/web/keyboard-processor/src/text/kbdInterface.ts +++ b/common/web/keyboard-processor/src/text/kbdInterface.ts @@ -213,7 +213,9 @@ export default class KeyboardInterface { variableStoreSerializer?: VariableStoreSerializer; // A 'reference point' that debug keyboards may use to access KMW's code constants. - public readonly Codes = Codes; + public get Codes(): typeof Codes { + return Codes; + } constructor(variableStoreSerializer: VariableStoreSerializer = null) { this.systemStores = {}; From a7873504d4eb54b13fbae7d0fceaaeb9aa4b4aca Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 25 Nov 2022 11:48:36 +0700 Subject: [PATCH 12/23] change(common/web): build output path updates --- common/web/keyboard-processor/build-bundler.js | 6 +++--- common/web/keyboard-processor/build.sh | 1 + common/web/keyboard-processor/src/index.ts | 2 +- .../keyboard-processor/src/keyboards/activeLayout.ts | 2 +- .../src/keyboards/defaultLayouts.ts | 2 +- .../web/keyboard-processor/src/keyboards/keyboard.ts | 2 +- .../web/keyboard-processor/src/text/kbdInterface.ts | 2 +- common/web/keyboard-processor/src/text/keyEvent.ts | 2 +- .../keyboard-processor/src/text/keyboardProcessor.ts | 2 +- .../keyboard-processor/tests/cases/basic-engine.js | 6 +++--- .../web/keyboard-processor/tests/cases/basic-init.js | 2 +- .../web/keyboard-processor/tests/cases/chirality.js | 10 +++++----- .../web/keyboard-processor/tests/cases/deadkeys.js | 6 +++--- .../keyboard-processor/tests/cases/engine/context.js | 8 ++++---- .../tests/cases/engine/notany_context.js | 10 +++++----- .../keyboard-processor/tests/cases/engine/stores.js | 6 +++--- .../tests/cases/engine/unmatched_final_group.js | 6 +++--- .../keyboard-processor/tests/cases/transcriptions.js | 4 ++-- .../web/keyboard-processor/tests/cases/versions.js | 2 +- common/web/keyboard-processor/tsconfig.json | 4 ++-- common/web/recorder/src/index.ts | 8 ++++---- common/web/recorder/src/nodeProctor.ts | 12 ++++++------ common/web/recorder/src/proctor.ts | 4 ++-- common/web/recorder/tsconfig.json | 4 ++-- 24 files changed, 57 insertions(+), 56 deletions(-) diff --git a/common/web/keyboard-processor/build-bundler.js b/common/web/keyboard-processor/build-bundler.js index 1bf88783ba..c0fe739a84 100644 --- a/common/web/keyboard-processor/build-bundler.js +++ b/common/web/keyboard-processor/build-bundler.js @@ -8,7 +8,7 @@ import esbuild from 'esbuild'; esbuild.buildSync({ - entryPoints: ['build/modules/index.js'], + entryPoints: ['build/obj/index.js'], bundle: true, sourcemap: true, minify: true, @@ -18,8 +18,8 @@ esbuild.buildSync({ // // 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/modules"], - outfile: "build/bundled/index.js", + nodePaths: ['..', "build/obj"], + outfile: "build/lib/index.js", tsconfig: 'tsconfig.json', target: "es5" }); \ No newline at end of file diff --git a/common/web/keyboard-processor/build.sh b/common/web/keyboard-processor/build.sh index 50f5822335..bc6b0ac617 100755 --- a/common/web/keyboard-processor/build.sh +++ b/common/web/keyboard-processor/build.sh @@ -50,6 +50,7 @@ fi if builder_start_action build; then npm run tsc -- --build "$THIS_SCRIPT_PATH/tsconfig.json" + node ./build-bundler.js builder_finish_action success build fi diff --git a/common/web/keyboard-processor/src/index.ts b/common/web/keyboard-processor/src/index.ts index 7a8c06e115..40f3292e33 100644 --- a/common/web/keyboard-processor/src/index.ts +++ b/common/web/keyboard-processor/src/index.ts @@ -17,7 +17,7 @@ import OutputTarget, * as OutputTargetContents from "./text/outputTarget.js"; import RuleBehavior from "./text/ruleBehavior.js"; import * as SystemStores from "./text/systemStores.js"; -import * as utils from "@keymanapp/web-utils/build/modules/index.js"; +import * as utils from "@keymanapp/web-utils/build/obj/index.js"; export let com = { keyman: { diff --git a/common/web/keyboard-processor/src/keyboards/activeLayout.ts b/common/web/keyboard-processor/src/keyboards/activeLayout.ts index ee053200d8..9498da88ef 100644 --- a/common/web/keyboard-processor/src/keyboards/activeLayout.ts +++ b/common/web/keyboard-processor/src/keyboards/activeLayout.ts @@ -7,7 +7,7 @@ import type Keyboard from "./keyboard.js"; import KeyboardProcessor from "../text/keyboardProcessor.js"; -import { deepCopy, type DeviceSpec } from "@keymanapp/web-utils/build/modules/index.js"; +import { deepCopy, type DeviceSpec } from "@keymanapp/web-utils/build/obj/index.js"; // TS 3.9 changed behavior of getters to make them // non-enumerable by default. This broke our 'polyfill' diff --git a/common/web/keyboard-processor/src/keyboards/defaultLayouts.ts b/common/web/keyboard-processor/src/keyboards/defaultLayouts.ts index cafd937d33..012362526b 100644 --- a/common/web/keyboard-processor/src/keyboards/defaultLayouts.ts +++ b/common/web/keyboard-processor/src/keyboards/defaultLayouts.ts @@ -5,7 +5,7 @@ import Codes from "../text/codes.js"; import type Keyboard from "./keyboard.js"; -import { Version, deepCopy } from "@keymanapp/web-utils/build/modules/index.js"; +import { Version, deepCopy } from "@keymanapp/web-utils/build/obj/index.js"; export type KLS = {[layerName: string]: string[]}; diff --git a/common/web/keyboard-processor/src/keyboards/keyboard.ts b/common/web/keyboard-processor/src/keyboards/keyboard.ts index fb9e0f7d4f..81d0c09dc2 100644 --- a/common/web/keyboard-processor/src/keyboards/keyboard.ts +++ b/common/web/keyboard-processor/src/keyboards/keyboard.ts @@ -6,7 +6,7 @@ import type OutputTarget from "../text/outputTarget.js"; import type { ComplexKeyboardStore } from "../text/kbdInterface.js"; -import { Version, DeviceSpec } from "@keymanapp/web-utils/build/modules/index.js"; +import { Version, DeviceSpec } from "@keymanapp/web-utils/build/obj/index.js"; /** * Stores preprocessed properties of a keyboard for quick retrieval later. diff --git a/common/web/keyboard-processor/src/text/kbdInterface.ts b/common/web/keyboard-processor/src/text/kbdInterface.ts index dbf4a10c7a..1a618c5b0b 100644 --- a/common/web/keyboard-processor/src/text/kbdInterface.ts +++ b/common/web/keyboard-processor/src/text/kbdInterface.ts @@ -18,7 +18,7 @@ import { Mock } from "./outputTarget.js"; import RuleBehavior from "./ruleBehavior.js"; import Keyboard, { VariableStoreDictionary } from "../keyboards/keyboard.js"; -import { type DeviceSpec } from "@keymanapp/web-utils/build/modules/index.js"; +import { type DeviceSpec } from "@keymanapp/web-utils/build/obj/index.js"; //#endregion diff --git a/common/web/keyboard-processor/src/text/keyEvent.ts b/common/web/keyboard-processor/src/text/keyEvent.ts index 4ea291a910..4d4387d723 100644 --- a/common/web/keyboard-processor/src/text/keyEvent.ts +++ b/common/web/keyboard-processor/src/text/keyEvent.ts @@ -1,5 +1,5 @@ import type Keyboard from "../keyboards/keyboard.js"; -import type DeviceSpec from "@keymanapp/web-utils/build/modules/deviceSpec.js"; +import type DeviceSpec from "@keymanapp/web-utils/build/obj/deviceSpec.js"; // Represents a probability distribution over a keyboard's keys. // Defined here to avoid compilation issues. diff --git a/common/web/keyboard-processor/src/text/keyboardProcessor.ts b/common/web/keyboard-processor/src/text/keyboardProcessor.ts index 5508ad907b..897661a94a 100644 --- a/common/web/keyboard-processor/src/text/keyboardProcessor.ts +++ b/common/web/keyboard-processor/src/text/keyboardProcessor.ts @@ -13,7 +13,7 @@ import { Mock } from "./outputTarget.js"; import KeyboardInterface, { SystemStoreIDs, VariableStore } from "./kbdInterface.js"; import RuleBehavior from "./ruleBehavior.js"; -import { DeviceSpec, globalObject as getGlobalObject } from "@keymanapp/web-utils/build/modules/index.js"; +import { DeviceSpec, globalObject as getGlobalObject } from "@keymanapp/web-utils/build/obj/index.js"; // #endregion diff --git a/common/web/keyboard-processor/tests/cases/basic-engine.js b/common/web/keyboard-processor/tests/cases/basic-engine.js index 063b9268e9..ae02753b45 100644 --- a/common/web/keyboard-processor/tests/cases/basic-engine.js +++ b/common/web/keyboard-processor/tests/cases/basic-engine.js @@ -2,9 +2,9 @@ import { assert } from 'chai'; import fs from 'fs'; import vm from 'vm'; -import KeyboardProcessor from '@keymanapp/keyboard-processor/build/modules/text/keyboardProcessor.js'; -import { KeyboardTest } from '@keymanapp/recorder-core/build/modules/index.js'; -import NodeProctor from '@keymanapp/recorder-core/build/modules/nodeProctor.js'; +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'); diff --git a/common/web/keyboard-processor/tests/cases/basic-init.js b/common/web/keyboard-processor/tests/cases/basic-init.js index d3182c45a4..11b5024998 100644 --- a/common/web/keyboard-processor/tests/cases/basic-init.js +++ b/common/web/keyboard-processor/tests/cases/basic-init.js @@ -2,7 +2,7 @@ import { assert } from 'chai'; import fs from 'fs'; import vm from 'vm'; -import KeyboardProcessor from '@keymanapp/keyboard-processor/build/modules/text/keyboardProcessor.js'; +import KeyboardProcessor from '@keymanapp/keyboard-processor/build/obj/text/keyboardProcessor.js'; global.keyman = {}; // So that keyboard-based checks against the global `keyman` succeed. // 10.0+ dependent keyboards, like khmer_angkor, will otherwise fail to load. diff --git a/common/web/keyboard-processor/tests/cases/chirality.js b/common/web/keyboard-processor/tests/cases/chirality.js index d1c17cf0e7..3bb9d6eeba 100644 --- a/common/web/keyboard-processor/tests/cases/chirality.js +++ b/common/web/keyboard-processor/tests/cases/chirality.js @@ -2,12 +2,12 @@ import { assert } from 'chai'; import fs from 'fs'; import vm from 'vm'; -import Codes from '@keymanapp/keyboard-processor/build/modules/text/codes.js'; -import KeyboardInterface from '@keymanapp/keyboard-processor/build/modules/text/kbdInterface.js'; -import KeyboardProcessor from '@keymanapp/keyboard-processor/build/modules/text/keyboardProcessor.js'; +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'; -import { KeyboardTest } from '@keymanapp/recorder-core/build/modules/index.js'; -import NodeProctor from '@keymanapp/recorder-core/build/modules/nodeProctor.js'; +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'); diff --git a/common/web/keyboard-processor/tests/cases/deadkeys.js b/common/web/keyboard-processor/tests/cases/deadkeys.js index adfe9ec193..bc45e77642 100644 --- a/common/web/keyboard-processor/tests/cases/deadkeys.js +++ b/common/web/keyboard-processor/tests/cases/deadkeys.js @@ -2,10 +2,10 @@ import { assert } from 'chai'; import fs from 'fs'; import vm from 'vm'; -import KeyboardProcessor from '@keymanapp/keyboard-processor/build/modules/text/keyboardProcessor.js'; +import KeyboardProcessor from '@keymanapp/keyboard-processor/build/obj/text/keyboardProcessor.js'; -import { KeyboardTest } from '@keymanapp/recorder-core/build/modules/index.js'; -import NodeProctor from '@keymanapp/recorder-core/build/modules/nodeProctor.js'; +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'); diff --git a/common/web/keyboard-processor/tests/cases/engine/context.js b/common/web/keyboard-processor/tests/cases/engine/context.js index 4b54b18859..15091eaa88 100644 --- a/common/web/keyboard-processor/tests/cases/engine/context.js +++ b/common/web/keyboard-processor/tests/cases/engine/context.js @@ -2,11 +2,11 @@ import { assert } from 'chai'; import fs from 'fs'; import vm from 'vm'; -import KeyboardProcessor from '@keymanapp/keyboard-processor/build/modules/text/keyboardProcessor.js'; -import { Mock } from '@keymanapp/keyboard-processor/build/modules/text/outputTarget.js'; +import KeyboardProcessor from '@keymanapp/keyboard-processor/build/obj/text/keyboardProcessor.js'; +import { Mock } from '@keymanapp/keyboard-processor/build/obj/text/outputTarget.js'; -import { RecordedKeystrokeSequence } from '@keymanapp/recorder-core/build/modules/index.js'; -import NodeProctor from '@keymanapp/recorder-core/build/modules/nodeProctor.js'; +import { RecordedKeystrokeSequence } from '@keymanapp/recorder-core/build/obj/index.js'; +import NodeProctor from '@keymanapp/recorder-core/build/obj/nodeProctor.js'; /* * ABOUT THIS TEST SUITE diff --git a/common/web/keyboard-processor/tests/cases/engine/notany_context.js b/common/web/keyboard-processor/tests/cases/engine/notany_context.js index f2a1231ec3..e587174a86 100644 --- a/common/web/keyboard-processor/tests/cases/engine/notany_context.js +++ b/common/web/keyboard-processor/tests/cases/engine/notany_context.js @@ -2,13 +2,13 @@ import { assert } from 'chai'; import fs from 'fs'; import vm from 'vm'; -import KeyboardProcessor from '@keymanapp/keyboard-processor/build/modules/text/keyboardProcessor.js'; -import { Mock } from '@keymanapp/keyboard-processor/build/modules/text/outputTarget.js'; +import KeyboardProcessor from '@keymanapp/keyboard-processor/build/obj/text/keyboardProcessor.js'; +import { Mock } from '@keymanapp/keyboard-processor/build/obj/text/outputTarget.js'; -import { RecordedKeystrokeSequence } from '@keymanapp/recorder-core/build/modules/index.js'; -import NodeProctor from '@keymanapp/recorder-core/build/modules/nodeProctor.js'; +import { RecordedKeystrokeSequence } from '@keymanapp/recorder-core/build/obj/index.js'; +import NodeProctor from '@keymanapp/recorder-core/build/obj/nodeProctor.js'; -import extendString from '@keymanapp/web-utils/build/modules/kmwstring.js' +import extendString from '@keymanapp/web-utils/build/obj/kmwstring.js' extendString(); // Ensure KMW's string-extension functionality is available. diff --git a/common/web/keyboard-processor/tests/cases/engine/stores.js b/common/web/keyboard-processor/tests/cases/engine/stores.js index 2f0eb5eaa5..3a450b7135 100644 --- a/common/web/keyboard-processor/tests/cases/engine/stores.js +++ b/common/web/keyboard-processor/tests/cases/engine/stores.js @@ -1,9 +1,9 @@ import { assert } from 'chai'; -import Keyboard from '@keymanapp/keyboard-processor/build/modules/keyboards/keyboard.js'; -import KeyboardProcessor from '@keymanapp/keyboard-processor/build/modules/text/keyboardProcessor.js'; +import Keyboard from '@keymanapp/keyboard-processor/build/obj/keyboards/keyboard.js'; +import KeyboardProcessor from '@keymanapp/keyboard-processor/build/obj/text/keyboardProcessor.js'; -import extendString from '@keymanapp/web-utils/build/modules/kmwstring.js' +import extendString from '@keymanapp/web-utils/build/obj/kmwstring.js' extendString(); diff --git a/common/web/keyboard-processor/tests/cases/engine/unmatched_final_group.js b/common/web/keyboard-processor/tests/cases/engine/unmatched_final_group.js index 7e449d1117..a083aced29 100644 --- a/common/web/keyboard-processor/tests/cases/engine/unmatched_final_group.js +++ b/common/web/keyboard-processor/tests/cases/engine/unmatched_final_group.js @@ -2,10 +2,10 @@ import { assert } from 'chai'; import fs from 'fs'; import vm from 'vm'; -import KeyboardProcessor from '@keymanapp/keyboard-processor/build/modules/text/keyboardProcessor.js'; +import KeyboardProcessor from '@keymanapp/keyboard-processor/build/obj/text/keyboardProcessor.js'; -import { KeyboardTest } from '@keymanapp/recorder-core/build/modules/index.js'; -import NodeProctor from '@keymanapp/recorder-core/build/modules/nodeProctor.js'; +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'); diff --git a/common/web/keyboard-processor/tests/cases/transcriptions.js b/common/web/keyboard-processor/tests/cases/transcriptions.js index 74bbaf4a9f..0ffb90a97f 100644 --- a/common/web/keyboard-processor/tests/cases/transcriptions.js +++ b/common/web/keyboard-processor/tests/cases/transcriptions.js @@ -1,7 +1,7 @@ import { assert } from 'chai'; -import { Mock } from '@keymanapp/keyboard-processor/build/modules/text/outputTarget.js'; -import extendString from '@keymanapp/web-utils/build/modules/kmwstring.js' +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. diff --git a/common/web/keyboard-processor/tests/cases/versions.js b/common/web/keyboard-processor/tests/cases/versions.js index 949de1e726..378d7f3163 100644 --- a/common/web/keyboard-processor/tests/cases/versions.js +++ b/common/web/keyboard-processor/tests/cases/versions.js @@ -1,6 +1,6 @@ import { assert } from 'chai'; -import Version from '@keymanapp/web-utils/build/modules/version.js'; +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() { diff --git a/common/web/keyboard-processor/tsconfig.json b/common/web/keyboard-processor/tsconfig.json index 96826b11eb..7db9736205 100644 --- a/common/web/keyboard-processor/tsconfig.json +++ b/common/web/keyboard-processor/tsconfig.json @@ -13,8 +13,8 @@ "lib": ["es6"], "experimentalDecorators": true, "baseUrl": "./", - "outDir": "build/modules/", - "tsBuildInfoFile": "build/modules/tsconfig.tsbuildinfo", + "outDir": "build/obj/", + "tsBuildInfoFile": "build/obj/tsconfig.tsbuildinfo", "rootDir": "./src" }, "references": [ diff --git a/common/web/recorder/src/index.ts b/common/web/recorder/src/index.ts index 7b9821c83f..ac6d8f7b70 100644 --- a/common/web/recorder/src/index.ts +++ b/common/web/recorder/src/index.ts @@ -1,12 +1,12 @@ /// -import KeyEvent, { KeyDistribution } from "@keymanapp/keyboard-processor/build/modules/text/keyEvent.js"; -import type OutputTarget from "@keymanapp/keyboard-processor/build/modules/text/outputTarget.js"; -import { Mock } from "@keymanapp/keyboard-processor/build/modules/text/outputTarget.js"; +import KeyEvent, { KeyDistribution } from "@keymanapp/keyboard-processor/build/obj/text/keyEvent.js"; +import type OutputTarget from "@keymanapp/keyboard-processor/build/obj/text/outputTarget.js"; +import { Mock } from "@keymanapp/keyboard-processor/build/obj/text/outputTarget.js"; import Proctor from "./proctor.js"; -import * as utils from "@keymanapp/web-utils/build/modules/index.js"; +import * as utils from "@keymanapp/web-utils/build/obj/index.js"; //#region Defines the InputEventSpec set, used to reconstruct DOM-based events for browser-based simulation export abstract class InputEventSpec { diff --git a/common/web/recorder/src/nodeProctor.ts b/common/web/recorder/src/nodeProctor.ts index 24d920592b..6650f48d61 100644 --- a/common/web/recorder/src/nodeProctor.ts +++ b/common/web/recorder/src/nodeProctor.ts @@ -8,13 +8,13 @@ import { RecordedSyntheticKeystroke } from "./index.js"; -import Keyboard from "@keymanapp/keyboard-processor/build/modules/keyboards/keyboard.js"; -import type KeyEvent from "@keymanapp/keyboard-processor/build/modules/text/keyEvent.js"; -import KeyboardProcessor from "@keymanapp/keyboard-processor/build/modules/text/keyboardProcessor.js"; -import type OutputTarget from "@keymanapp/keyboard-processor/build/modules/text/outputTarget.js"; -import { Mock } from "@keymanapp/keyboard-processor/build/modules/text/outputTarget.js"; +import Keyboard from "@keymanapp/keyboard-processor/build/obj/keyboards/keyboard.js"; +import type KeyEvent from "@keymanapp/keyboard-processor/build/obj/text/keyEvent.js"; +import KeyboardProcessor from "@keymanapp/keyboard-processor/build/obj/text/keyboardProcessor.js"; +import type OutputTarget from "@keymanapp/keyboard-processor/build/obj/text/outputTarget.js"; +import { Mock } from "@keymanapp/keyboard-processor/build/obj/text/outputTarget.js"; -import DeviceSpec from "@keymanapp/web-utils/build/modules/deviceSpec.js"; +import DeviceSpec from "@keymanapp/web-utils/build/obj/deviceSpec.js"; export default class NodeProctor extends Proctor { private keyboard: Keyboard; diff --git a/common/web/recorder/src/proctor.ts b/common/web/recorder/src/proctor.ts index 026bef56c1..347271fb0a 100644 --- a/common/web/recorder/src/proctor.ts +++ b/common/web/recorder/src/proctor.ts @@ -1,5 +1,5 @@ -import { type DeviceSpec } from "@keymanapp/web-utils/build/modules/index.js"; -import type OutputTarget from "@keymanapp/keyboard-processor/build/modules/text/outputTarget.js"; +import { type DeviceSpec } from "@keymanapp/web-utils/build/obj/index.js"; +import type OutputTarget from "@keymanapp/keyboard-processor/build/obj/text/outputTarget.js"; import type { KeyboardTest, TestSet, TestSequence } from "./index.js"; diff --git a/common/web/recorder/tsconfig.json b/common/web/recorder/tsconfig.json index 22cbba849b..6bd3ce6364 100644 --- a/common/web/recorder/tsconfig.json +++ b/common/web/recorder/tsconfig.json @@ -12,8 +12,8 @@ "types": ["node"], "lib": ["es6"], "baseUrl": "./", - "outDir": "build/modules/", - "tsBuildInfoFile": "build/modules/tsconfig.tsbuildinfo", + "outDir": "build/obj/", + "tsBuildInfoFile": "build/obj/tsconfig.tsbuildinfo", "rootDir": "./src" }, From 17d3f40bec3744d6e9deae5a5cb443579f0d3cb7 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 25 Nov 2022 13:50:32 +0700 Subject: [PATCH 13/23] feat(common/web): first pass a a bundled declaration for the namespace-style bundle --- .../web/keyboard-processor/build-bundler.js | 68 +++++++++++++++++- .../src/index-namespaced.ts | 56 +++++++++++++++ common/web/keyboard-processor/src/index.ts | 70 ++++++------------- 3 files changed, 142 insertions(+), 52 deletions(-) create mode 100644 common/web/keyboard-processor/src/index-namespaced.ts diff --git a/common/web/keyboard-processor/build-bundler.js b/common/web/keyboard-processor/build-bundler.js index c0fe739a84..522cb9333a 100644 --- a/common/web/keyboard-processor/build-bundler.js +++ b/common/web/keyboard-processor/build-bundler.js @@ -6,12 +6,15 @@ */ import esbuild from 'esbuild'; +import { spawn } from 'child_process'; +// Browser / namespace-targeted bundle esbuild.buildSync({ - entryPoints: ['build/obj/index.js'], + entryPoints: ['build/obj/index-namespaced.js'], bundle: true, sourcemap: true, minify: true, + format: "iife", keepNames: true, // Sets 'common/web' as a root folder for module resolution; // this allows the keyman-version and utils imports to resolve. @@ -19,7 +22,68 @@ esbuild.buildSync({ // 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.js", + outfile: "build/lib/index.namespaced.js", tsconfig: 'tsconfig.json', target: "es5" +}); + +// 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" +}); + +const dtsBundleCommand = spawn('npx dts-bundle-generator --project tsconfig.json -o build/lib/index.d.ts src/index.ts', { + shell: true +}); + +dtsBundleCommand.stdout.on('data', data => console.log(data.toString())); +dtsBundleCommand.stderr.on('data', data => console.error(data.toString())); + +// Forces synchronicity; done mostly so that the logs don't get jumbled up. +dtsBundleCommand.on('exit', () => { + if(dtsBundleCommand.exitCode != 0) { + process.exit(dtsBundleCommand.exitCode); + } + + const namespacedDtsBundleCmd = spawn('npx dts-bundle-generator --project tsconfig.json -o build/lib/index.namespaced.d.ts src/index-namespaced.ts', { + shell: true + }); + + namespacedDtsBundleCmd.stdout.on('data', data => console.log(data.toString())); + namespacedDtsBundleCmd.stderr.on('data', data => console.error(data.toString())); + + namespacedDtsBundleCmd.on('exit', () => { + if(namespacedDtsBundleCmd.exitCode != 0) { + process.exit(namespacedDtsBundleCmd.exitCode); + } + }) }); \ No newline at end of file diff --git a/common/web/keyboard-processor/src/index-namespaced.ts b/common/web/keyboard-processor/src/index-namespaced.ts new file mode 100644 index 0000000000..9d90b38ad6 --- /dev/null +++ b/common/web/keyboard-processor/src/index-namespaced.ts @@ -0,0 +1,56 @@ +// This file exists as a bundling intermediary that attempts to present all of +// keyboard-processor's offerings in the 'old', namespaced format - at least, +// as of the time that this submodule was converted to ES6 module use. + +// Unfortunately, the declaration-bundling tool that works well for the modules... +// struggles a bit here. + +import { ActiveKey, ActiveRow, ActiveLayer, ActiveLayout } from "./keyboards/activeLayout.js"; +import { Layouts } from "./keyboards/defaultLayouts.js"; +import Keyboard, { LayoutState } from "./keyboards/keyboard.js"; + +import Codes from "./text/codes.js"; +import { Deadkey, DeadkeyTracker} from "./text/deadkeys.js"; +import DefaultOutput, { EmulationKeystrokes } from "./text/defaultOutput.js"; +import KeyboardInterface, { KeyInformation, SystemStoreIDs } from "./text/kbdInterface.js"; +import KeyboardProcessor from "./text/keyboardProcessor.js"; +import KeyEvent from "./text/keyEvent.js"; +import KeyMapping from "./text/keyMapping.js"; +import OutputTarget, { TextTransform, Transcription, Mock } from "./text/outputTarget.js"; +import RuleBehavior from "./text/ruleBehavior.js"; +import { SystemStore, MutableSystemStore, PlatformSystemStore } from "./text/systemStores.js"; + +import { deepCopy, DeviceSpec, extendString, globalObject as getGlobalObject, Version } from "@keymanapp/web-utils/build/obj/index.js"; + +// DeviceSpec's merged declaration style isn't well-handled by the declaration bundler without this. +export { DeviceSpec } from "@keymanapp/web-utils/build/obj/index.js"; + +export let com = { + keyman: { + keyboards: { + ActiveKey, ActiveRow, ActiveLayer, ActiveLayout, // keyboards/activeLayouts.ts + Layouts, // keyboards/defaultLayouts.ts + Keyboard, LayoutState // keyboards/keyboard.ts + }, + text: { + Codes, + Deadkey, DeadkeyTracker, // text/deadkeys.ts + DefaultOutput, EmulationKeystrokes, // text/defaultOutput.ts + KeyboardInterface, KeyInformation, SystemStoreIDs, // text/kbdInterface.ts + KeyboardProcessor, + KeyEvent, + KeyMapping, + OutputTarget, TextTransform, Transcription, Mock, // text/outputTarget.ts + RuleBehavior, + SystemStore, MutableSystemStore, PlatformSystemStore // text/systemStores.ts + }, + utils: { + deepCopy, DeviceSpec, extendString, getGlobalObject, Version + } + } +} + +// Force-exports it as the global it always was. +getGlobalObject()['com'] = com; + +export default com; diff --git a/common/web/keyboard-processor/src/index.ts b/common/web/keyboard-processor/src/index.ts index 40f3292e33..7672682ab1 100644 --- a/common/web/keyboard-processor/src/index.ts +++ b/common/web/keyboard-processor/src/index.ts @@ -1,52 +1,22 @@ -// This file exists as a bundling intermediary that attempts to present all of -// keyboard-processor's offerings in the 'old', namespaced format - at least, -// as of the time that this submodule was converted to ES6 module use. +export * from "./keyboards/activeLayout.js"; +export * from "./keyboards/defaultLayouts.js"; +export { default as Keyboard } from "./keyboards/keyboard.js"; +export * from "./keyboards/keyboard.js"; -import * as ActiveLayout from "./keyboards/activeLayout.js"; -import * as DefaultLayout from "./keyboards/defaultLayouts.js"; -import Keyboard, * as KeyboardContents 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 { 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"; -import Codes, * as CodesContents from "./text/codes.js"; -import * as Deadkeys from "./text/deadkeys.js"; -import DefaultOutput, * as DefaultOutputContents from "./text/defaultOutput.js"; -import KbdInterface, * as KbdInterfaceContents from "./text/kbdInterface.js"; -import KeyboardProcessor, * as KeyboardProcessorContents from "./text/keyboardProcessor.js"; -import KeyEvent from "./text/keyEvent.js"; -import KeyMapping from "./text/keyMapping.js"; -import OutputTarget, * as OutputTargetContents from "./text/outputTarget.js"; -import RuleBehavior from "./text/ruleBehavior.js"; -import * as SystemStores from "./text/systemStores.js"; - -import * as utils from "@keymanapp/web-utils/build/obj/index.js"; - -export let com = { - keyman: { - keyboards: { - ...ActiveLayout, - ...DefaultLayout, - Keyboard, ...KeyboardContents - }, - text: { - Codes, ...CodesContents, - ...Deadkeys, - DefaultOutput, ...DefaultOutputContents, - KbdInterface, ...KbdInterfaceContents, - KeyboardProcessor, ...KeyboardProcessorContents, - KeyEvent, - KeyMapping, - OutputTarget, ...OutputTargetContents, - RuleBehavior, - ...SystemStores - }, - utils: {... utils} - } -} - -// A consequence of the spread-operator use + the modules with defaults. -delete com.keyman.keyboards.default; -delete com.keyman.text.default; - -// Force-exports it as the global it always was. -utils.globalObject()['com'] = com; - -export default com; +export * from "@keymanapp/web-utils/build/obj/index.js"; \ No newline at end of file From 43cdd783bb29625c6b1dd9e3f4158e6d4b076f48 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 25 Nov 2022 14:35:51 +0700 Subject: [PATCH 14/23] feat(common/web): bundled-module canary unit test --- .../tests/cases/bundled-module.js | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 common/web/keyboard-processor/tests/cases/bundled-module.js diff --git a/common/web/keyboard-processor/tests/cases/bundled-module.js b/common/web/keyboard-processor/tests/cases/bundled-module.js new file mode 100644 index 0000000000..3d6896ce3f --- /dev/null +++ b/common/web/keyboard-processor/tests/cases/bundled-module.js @@ -0,0 +1,20 @@ +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. + +describe('Bundled ES Module', function() { + describe('KeyboardProcessor', function () { + it('should initialize without errors', function () { + let kp = new Package.KeyboardProcessor(); + assert.isNotNull(kp); + }); + }); + + 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"); + }); + }) +}); \ No newline at end of file From 7c0908cebd539f83f4b83dc99fabf5465a2ab45b Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 25 Nov 2022 14:47:52 +0700 Subject: [PATCH 15/23] change(common/web): adds one extra module-bundle unit test --- .../web/keyboard-processor/tests/cases/bundled-module.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/common/web/keyboard-processor/tests/cases/bundled-module.js b/common/web/keyboard-processor/tests/cases/bundled-module.js index 3d6896ce3f..4dcd7dc1eb 100644 --- a/common/web/keyboard-processor/tests/cases/bundled-module.js +++ b/common/web/keyboard-processor/tests/cases/bundled-module.js @@ -11,6 +11,14 @@ describe('Bundled ES Module', function() { }); }); + describe('Mock', () => { + it('basic functionality test', () => { + let target = new Package.Mock("aple", 2); + target.insertTextBeforeCaret('p'); + assert.equal(target.getText(), "apple"); + }); + }); + describe("Imported `utils`", function() { it("should include `utils` package's Version class", () => { let v16 = new Package.Version([16, 1]); From 52c4ba404f29f714283ef98039500ef835bb92f2 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 25 Nov 2022 14:53:59 +0700 Subject: [PATCH 16/23] feat(common/web): tests for side-effect auto-extension of String class for smp by modules --- .../tests/cases/bundled-module.js | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/common/web/keyboard-processor/tests/cases/bundled-module.js b/common/web/keyboard-processor/tests/cases/bundled-module.js index 4dcd7dc1eb..bfff937b62 100644 --- a/common/web/keyboard-processor/tests/cases/bundled-module.js +++ b/common/web/keyboard-processor/tests/cases/bundled-module.js @@ -3,6 +3,15 @@ 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 () { @@ -13,10 +22,23 @@ describe('Bundled ES Module', function() { describe('Mock', () => { it('basic functionality test', () => { - let target = new Package.Mock("aple", 2); + 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() { From e48db510e1641988998a6ab47e730b9719642fd0 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Wed, 30 Nov 2022 15:28:17 +0700 Subject: [PATCH 17/23] change(common/web): different namespace-like export attempt --- common/web/input-processor/src/tsconfig.json | 2 +- .../web/keyboard-processor/src/com-index.ts | 1 + .../src/index-namespaced.ts | 50 ++----------------- .../keyboard-processor/src/keyboards/index.ts | 23 +++++++++ .../keyboard-processor/src/keyman-index.ts | 3 ++ .../web/keyboard-processor/src/text/index.ts | 10 ++++ .../web/keyboard-processor/src/utils-index.ts | 1 + 7 files changed, 43 insertions(+), 47 deletions(-) create mode 100644 common/web/keyboard-processor/src/com-index.ts create mode 100644 common/web/keyboard-processor/src/keyboards/index.ts create mode 100644 common/web/keyboard-processor/src/keyman-index.ts create mode 100644 common/web/keyboard-processor/src/text/index.ts create mode 100644 common/web/keyboard-processor/src/utils-index.ts diff --git a/common/web/input-processor/src/tsconfig.json b/common/web/input-processor/src/tsconfig.json index 230233a2d1..ed68d6cb35 100644 --- a/common/web/input-processor/src/tsconfig.json +++ b/common/web/input-processor/src/tsconfig.json @@ -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"], diff --git a/common/web/keyboard-processor/src/com-index.ts b/common/web/keyboard-processor/src/com-index.ts new file mode 100644 index 0000000000..179f865420 --- /dev/null +++ b/common/web/keyboard-processor/src/com-index.ts @@ -0,0 +1 @@ +export * as keyman from './keyman-index.js'; \ No newline at end of file diff --git a/common/web/keyboard-processor/src/index-namespaced.ts b/common/web/keyboard-processor/src/index-namespaced.ts index 9d90b38ad6..c41cc9c0ac 100644 --- a/common/web/keyboard-processor/src/index-namespaced.ts +++ b/common/web/keyboard-processor/src/index-namespaced.ts @@ -5,52 +5,10 @@ // Unfortunately, the declaration-bundling tool that works well for the modules... // struggles a bit here. -import { ActiveKey, ActiveRow, ActiveLayer, ActiveLayout } from "./keyboards/activeLayout.js"; -import { Layouts } from "./keyboards/defaultLayouts.js"; -import Keyboard, { LayoutState } from "./keyboards/keyboard.js"; +import * as com from "./com-index.js"; -import Codes from "./text/codes.js"; -import { Deadkey, DeadkeyTracker} from "./text/deadkeys.js"; -import DefaultOutput, { EmulationKeystrokes } from "./text/defaultOutput.js"; -import KeyboardInterface, { KeyInformation, SystemStoreIDs } from "./text/kbdInterface.js"; -import KeyboardProcessor from "./text/keyboardProcessor.js"; -import KeyEvent from "./text/keyEvent.js"; -import KeyMapping from "./text/keyMapping.js"; -import OutputTarget, { TextTransform, Transcription, Mock } from "./text/outputTarget.js"; -import RuleBehavior from "./text/ruleBehavior.js"; -import { SystemStore, MutableSystemStore, PlatformSystemStore } from "./text/systemStores.js"; - -import { deepCopy, DeviceSpec, extendString, globalObject as getGlobalObject, Version } from "@keymanapp/web-utils/build/obj/index.js"; - -// DeviceSpec's merged declaration style isn't well-handled by the declaration bundler without this. -export { DeviceSpec } from "@keymanapp/web-utils/build/obj/index.js"; - -export let com = { - keyman: { - keyboards: { - ActiveKey, ActiveRow, ActiveLayer, ActiveLayout, // keyboards/activeLayouts.ts - Layouts, // keyboards/defaultLayouts.ts - Keyboard, LayoutState // keyboards/keyboard.ts - }, - text: { - Codes, - Deadkey, DeadkeyTracker, // text/deadkeys.ts - DefaultOutput, EmulationKeystrokes, // text/defaultOutput.ts - KeyboardInterface, KeyInformation, SystemStoreIDs, // text/kbdInterface.ts - KeyboardProcessor, - KeyEvent, - KeyMapping, - OutputTarget, TextTransform, Transcription, Mock, // text/outputTarget.ts - RuleBehavior, - SystemStore, MutableSystemStore, PlatformSystemStore // text/systemStores.ts - }, - utils: { - deepCopy, DeviceSpec, extendString, getGlobalObject, Version - } - } -} +// Make sure the declaration-merger code pays attention. +export * as com from "./com-index.js"; // Force-exports it as the global it always was. -getGlobalObject()['com'] = com; - -export default com; +com.keyman.utils.getGlobalObject()['com'] = com; diff --git a/common/web/keyboard-processor/src/keyboards/index.ts b/common/web/keyboard-processor/src/keyboards/index.ts new file mode 100644 index 0000000000..bf3e3e6e47 --- /dev/null +++ b/common/web/keyboard-processor/src/keyboards/index.ts @@ -0,0 +1,23 @@ +// This file exists as a bundling intermediary that attempts to present all of +// keyboard-processor's offerings in the 'old', namespaced format - at least, +// as of the time that this submodule was converted to ES6 module use. + +// Unfortunately, the declaration-bundling tool that works well for the modules... +// struggles a bit here. + +export { + ActiveKey, + ActiveRow, + ActiveLayer, + ActiveLayout +} from "./activeLayout.js"; + +export { + Layouts +} from "./defaultLayouts.js"; + +export { default as Keyboard} from "./keyboard.js"; +export { + LayoutState +} from "./keyboard.js"; + diff --git a/common/web/keyboard-processor/src/keyman-index.ts b/common/web/keyboard-processor/src/keyman-index.ts new file mode 100644 index 0000000000..e56fa94019 --- /dev/null +++ b/common/web/keyboard-processor/src/keyman-index.ts @@ -0,0 +1,3 @@ +export * as keyboards from './keyboards/index.js'; +export * as text from './text/index.js'; +export * as utils from './utils-index.js'; \ No newline at end of file diff --git a/common/web/keyboard-processor/src/text/index.ts b/common/web/keyboard-processor/src/text/index.ts new file mode 100644 index 0000000000..d4276cfed1 --- /dev/null +++ b/common/web/keyboard-processor/src/text/index.ts @@ -0,0 +1,10 @@ +export { default as Codes } from "./codes.js"; +export { Deadkey, DeadkeyTracker } from "./deadkeys.js"; +export { default as DefaultOutput, EmulationKeystrokes } from "./defaultOutput.js"; +export { default as KeyboardInterface, KeyInformation, SystemStoreIDs } from "./kbdInterface.js"; +export { default as KeyboardProcessor } from "./keyboardProcessor.js"; +export { default as KeyEvent } from "./keyEvent.js"; +export { default as KeyMapping } from "./keyMapping.js"; +export { default as OutputTarget, TextTransform, Transcription, Mock } from "./outputTarget.js"; +export { default as RuleBehavior } from "./ruleBehavior.js"; +export { SystemStore, MutableSystemStore, PlatformSystemStore } from "./systemStores.js"; \ No newline at end of file diff --git a/common/web/keyboard-processor/src/utils-index.ts b/common/web/keyboard-processor/src/utils-index.ts new file mode 100644 index 0000000000..0b6995ee90 --- /dev/null +++ b/common/web/keyboard-processor/src/utils-index.ts @@ -0,0 +1 @@ +export { deepCopy, DeviceSpec, extendString, globalObject as getGlobalObject, Version } from "@keymanapp/web-utils/build/obj/index.js"; \ No newline at end of file From b171ff342e7ee71a1456f8323a830e3a2e8693db Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 27 Jan 2023 11:45:30 +0700 Subject: [PATCH 18/23] chore(common/web): cleans up cross-module imports, drops namespaced export setup --- .../web/keyboard-processor/build-bundler.js | 44 +++---------------- .../web/keyboard-processor/src/com-index.ts | 1 - .../src/index-namespaced.ts | 14 ------ common/web/keyboard-processor/src/index.ts | 9 +++- .../keyboard-processor/src/keyboards/index.ts | 23 ---------- .../keyboard-processor/src/keyman-index.ts | 3 -- .../web/keyboard-processor/src/text/index.ts | 10 ----- .../src/text/kbdInterface.ts | 5 +-- .../keyboard-processor/src/text/keyEvent.ts | 2 +- .../web/keyboard-processor/src/utils-index.ts | 1 - .../tests/temp-bundle-test.js | 16 ------- common/web/recorder/src/index.ts | 5 +-- common/web/recorder/src/nodeProctor.ts | 8 +--- common/web/recorder/src/proctor.ts | 2 +- 14 files changed, 22 insertions(+), 121 deletions(-) delete mode 100644 common/web/keyboard-processor/src/com-index.ts delete mode 100644 common/web/keyboard-processor/src/index-namespaced.ts delete mode 100644 common/web/keyboard-processor/src/keyboards/index.ts delete mode 100644 common/web/keyboard-processor/src/keyman-index.ts delete mode 100644 common/web/keyboard-processor/src/text/index.ts delete mode 100644 common/web/keyboard-processor/src/utils-index.ts delete mode 100644 common/web/keyboard-processor/tests/temp-bundle-test.js diff --git a/common/web/keyboard-processor/build-bundler.js b/common/web/keyboard-processor/build-bundler.js index 522cb9333a..1d1b44508e 100644 --- a/common/web/keyboard-processor/build-bundler.js +++ b/common/web/keyboard-processor/build-bundler.js @@ -8,25 +8,6 @@ import esbuild from 'esbuild'; import { spawn } from 'child_process'; -// Browser / namespace-targeted bundle -esbuild.buildSync({ - entryPoints: ['build/obj/index-namespaced.js'], - bundle: true, - sourcemap: true, - minify: true, - format: "iife", - keepNames: true, - // 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.namespaced.js", - tsconfig: 'tsconfig.json', - target: "es5" -}); - // Bundled ES module version esbuild.buildSync({ entryPoints: ['build/obj/index.js'], @@ -68,22 +49,9 @@ const dtsBundleCommand = spawn('npx dts-bundle-generator --project tsconfig.json dtsBundleCommand.stdout.on('data', data => console.log(data.toString())); dtsBundleCommand.stderr.on('data', data => console.error(data.toString())); -// Forces synchronicity; done mostly so that the logs don't get jumbled up. -dtsBundleCommand.on('exit', () => { - if(dtsBundleCommand.exitCode != 0) { - process.exit(dtsBundleCommand.exitCode); - } - - const namespacedDtsBundleCmd = spawn('npx dts-bundle-generator --project tsconfig.json -o build/lib/index.namespaced.d.ts src/index-namespaced.ts', { - shell: true - }); - - namespacedDtsBundleCmd.stdout.on('data', data => console.log(data.toString())); - namespacedDtsBundleCmd.stderr.on('data', data => console.error(data.toString())); - - namespacedDtsBundleCmd.on('exit', () => { - if(namespacedDtsBundleCmd.exitCode != 0) { - process.exit(namespacedDtsBundleCmd.exitCode); - } - }) -}); \ No newline at end of file +// // Forces synchronicity; done mostly so that the logs don't get jumbled up. +// dtsBundleCommand.on('exit', () => { +// if(dtsBundleCommand.exitCode != 0) { +// process.exit(dtsBundleCommand.exitCode); +// } +// }); \ No newline at end of file diff --git a/common/web/keyboard-processor/src/com-index.ts b/common/web/keyboard-processor/src/com-index.ts deleted file mode 100644 index 179f865420..0000000000 --- a/common/web/keyboard-processor/src/com-index.ts +++ /dev/null @@ -1 +0,0 @@ -export * as keyman from './keyman-index.js'; \ No newline at end of file diff --git a/common/web/keyboard-processor/src/index-namespaced.ts b/common/web/keyboard-processor/src/index-namespaced.ts deleted file mode 100644 index c41cc9c0ac..0000000000 --- a/common/web/keyboard-processor/src/index-namespaced.ts +++ /dev/null @@ -1,14 +0,0 @@ -// This file exists as a bundling intermediary that attempts to present all of -// keyboard-processor's offerings in the 'old', namespaced format - at least, -// as of the time that this submodule was converted to ES6 module use. - -// Unfortunately, the declaration-bundling tool that works well for the modules... -// struggles a bit here. - -import * as com from "./com-index.js"; - -// Make sure the declaration-merger code pays attention. -export * as com from "./com-index.js"; - -// Force-exports it as the global it always was. -com.keyman.utils.getGlobalObject()['com'] = com; diff --git a/common/web/keyboard-processor/src/index.ts b/common/web/keyboard-processor/src/index.ts index 7672682ab1..11ca9a1f78 100644 --- a/common/web/keyboard-processor/src/index.ts +++ b/common/web/keyboard-processor/src/index.ts @@ -13,10 +13,17 @@ 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"; \ No newline at end of file +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; \ No newline at end of file diff --git a/common/web/keyboard-processor/src/keyboards/index.ts b/common/web/keyboard-processor/src/keyboards/index.ts deleted file mode 100644 index bf3e3e6e47..0000000000 --- a/common/web/keyboard-processor/src/keyboards/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -// This file exists as a bundling intermediary that attempts to present all of -// keyboard-processor's offerings in the 'old', namespaced format - at least, -// as of the time that this submodule was converted to ES6 module use. - -// Unfortunately, the declaration-bundling tool that works well for the modules... -// struggles a bit here. - -export { - ActiveKey, - ActiveRow, - ActiveLayer, - ActiveLayout -} from "./activeLayout.js"; - -export { - Layouts -} from "./defaultLayouts.js"; - -export { default as Keyboard} from "./keyboard.js"; -export { - LayoutState -} from "./keyboard.js"; - diff --git a/common/web/keyboard-processor/src/keyman-index.ts b/common/web/keyboard-processor/src/keyman-index.ts deleted file mode 100644 index e56fa94019..0000000000 --- a/common/web/keyboard-processor/src/keyman-index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * as keyboards from './keyboards/index.js'; -export * as text from './text/index.js'; -export * as utils from './utils-index.js'; \ No newline at end of file diff --git a/common/web/keyboard-processor/src/text/index.ts b/common/web/keyboard-processor/src/text/index.ts deleted file mode 100644 index d4276cfed1..0000000000 --- a/common/web/keyboard-processor/src/text/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export { default as Codes } from "./codes.js"; -export { Deadkey, DeadkeyTracker } from "./deadkeys.js"; -export { default as DefaultOutput, EmulationKeystrokes } from "./defaultOutput.js"; -export { default as KeyboardInterface, KeyInformation, SystemStoreIDs } from "./kbdInterface.js"; -export { default as KeyboardProcessor } from "./keyboardProcessor.js"; -export { default as KeyEvent } from "./keyEvent.js"; -export { default as KeyMapping } from "./keyMapping.js"; -export { default as OutputTarget, TextTransform, Transcription, Mock } from "./outputTarget.js"; -export { default as RuleBehavior } from "./ruleBehavior.js"; -export { SystemStore, MutableSystemStore, PlatformSystemStore } from "./systemStores.js"; \ No newline at end of file diff --git a/common/web/keyboard-processor/src/text/kbdInterface.ts b/common/web/keyboard-processor/src/text/kbdInterface.ts index 1a618c5b0b..1dc1d54e60 100644 --- a/common/web/keyboard-processor/src/text/kbdInterface.ts +++ b/common/web/keyboard-processor/src/text/kbdInterface.ts @@ -5,20 +5,19 @@ //#region Imports +import { type DeviceSpec } from "@keymanapp/web-utils/build/obj/index.js"; + import Codes from "./codes.js"; import type KeyEvent from "./keyEvent.js"; import type { Deadkey } from "./deadkeys.js"; import KeyMapping from "./keyMapping.js"; import { SystemStore, MutableSystemStore, PlatformSystemStore } from "./systemStores.js"; import type { VariableStoreSerializer } from "./keyboardProcessor.js"; - import type OutputTarget from "./outputTarget.js"; import { Mock } from "./outputTarget.js"; - import RuleBehavior from "./ruleBehavior.js"; import Keyboard, { VariableStoreDictionary } from "../keyboards/keyboard.js"; -import { type DeviceSpec } from "@keymanapp/web-utils/build/obj/index.js"; //#endregion diff --git a/common/web/keyboard-processor/src/text/keyEvent.ts b/common/web/keyboard-processor/src/text/keyEvent.ts index 4d4387d723..bc29dd1c83 100644 --- a/common/web/keyboard-processor/src/text/keyEvent.ts +++ b/common/web/keyboard-processor/src/text/keyEvent.ts @@ -1,5 +1,5 @@ import type Keyboard from "../keyboards/keyboard.js"; -import type DeviceSpec from "@keymanapp/web-utils/build/obj/deviceSpec.js"; +import {type DeviceSpec} from "@keymanapp/web-utils/build/obj/index.js"; // Represents a probability distribution over a keyboard's keys. // Defined here to avoid compilation issues. diff --git a/common/web/keyboard-processor/src/utils-index.ts b/common/web/keyboard-processor/src/utils-index.ts deleted file mode 100644 index 0b6995ee90..0000000000 --- a/common/web/keyboard-processor/src/utils-index.ts +++ /dev/null @@ -1 +0,0 @@ -export { deepCopy, DeviceSpec, extendString, globalObject as getGlobalObject, Version } from "@keymanapp/web-utils/build/obj/index.js"; \ No newline at end of file diff --git a/common/web/keyboard-processor/tests/temp-bundle-test.js b/common/web/keyboard-processor/tests/temp-bundle-test.js deleted file mode 100644 index 0e4433f12c..0000000000 --- a/common/web/keyboard-processor/tests/temp-bundle-test.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * A temporary file to validate that the bundled version really is bundled and is usable in a - * similar manner to its old format. - */ - -// Loads `com` into the global namespace. -import * as _ from '../build/bundled/index.js'; - -console.log(`Int code for ALT: ${com.keyman.text.Codes.modifierCodes['ALT']}`); - -console.log(new com.keyman.keyboards.Keyboard(null)); - -console.log(); - -// make sure we bundled `utils` as well! -console.log(`Verifying proper handling of version 16.0: ${new com.keyman.utils.Version([16, 0]).toString()}`); \ No newline at end of file diff --git a/common/web/recorder/src/index.ts b/common/web/recorder/src/index.ts index ac6d8f7b70..5943c99fc5 100644 --- a/common/web/recorder/src/index.ts +++ b/common/web/recorder/src/index.ts @@ -1,8 +1,7 @@ /// -import KeyEvent, { KeyDistribution } from "@keymanapp/keyboard-processor/build/obj/text/keyEvent.js"; -import type OutputTarget from "@keymanapp/keyboard-processor/build/obj/text/outputTarget.js"; -import { Mock } from "@keymanapp/keyboard-processor/build/obj/text/outputTarget.js"; +import { type OutputTarget } from "@keymanapp/keyboard-processor/build/obj/index.js"; +import { KeyDistribution, KeyEvent, Mock } from "@keymanapp/keyboard-processor/build/obj/index.js"; import Proctor from "./proctor.js"; diff --git a/common/web/recorder/src/nodeProctor.ts b/common/web/recorder/src/nodeProctor.ts index 6650f48d61..6a759eb4bb 100644 --- a/common/web/recorder/src/nodeProctor.ts +++ b/common/web/recorder/src/nodeProctor.ts @@ -8,13 +8,9 @@ import { RecordedSyntheticKeystroke } from "./index.js"; -import Keyboard from "@keymanapp/keyboard-processor/build/obj/keyboards/keyboard.js"; -import type KeyEvent from "@keymanapp/keyboard-processor/build/obj/text/keyEvent.js"; -import KeyboardProcessor from "@keymanapp/keyboard-processor/build/obj/text/keyboardProcessor.js"; -import type OutputTarget from "@keymanapp/keyboard-processor/build/obj/text/outputTarget.js"; -import { Mock } from "@keymanapp/keyboard-processor/build/obj/text/outputTarget.js"; +import { Keyboard, type KeyEvent, KeyboardProcessor, Mock, type OutputTarget } from "@keymanapp/keyboard-processor/build/obj/index.js"; -import DeviceSpec from "@keymanapp/web-utils/build/obj/deviceSpec.js"; +import { DeviceSpec } from "@keymanapp/web-utils/build/obj/index.js"; export default class NodeProctor extends Proctor { private keyboard: Keyboard; diff --git a/common/web/recorder/src/proctor.ts b/common/web/recorder/src/proctor.ts index 347271fb0a..412e0909c1 100644 --- a/common/web/recorder/src/proctor.ts +++ b/common/web/recorder/src/proctor.ts @@ -1,5 +1,5 @@ import { type DeviceSpec } from "@keymanapp/web-utils/build/obj/index.js"; -import type OutputTarget from "@keymanapp/keyboard-processor/build/obj/text/outputTarget.js"; +import { type OutputTarget } from "@keymanapp/keyboard-processor/build/obj/index.js"; import type { KeyboardTest, TestSet, TestSequence } from "./index.js"; From 2a40534d3ea12d10e5d12f75454d2be25ab6b367 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Tue, 31 Jan 2023 11:54:49 +0700 Subject: [PATCH 19/23] change(common/web): better dev-mode declaration bundling --- common/web/keyboard-processor/build-bundler.js | 16 +--------------- common/web/keyboard-processor/build.sh | 4 ++++ tsconfig-base.json | 2 +- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/common/web/keyboard-processor/build-bundler.js b/common/web/keyboard-processor/build-bundler.js index 1d1b44508e..c08372c92e 100644 --- a/common/web/keyboard-processor/build-bundler.js +++ b/common/web/keyboard-processor/build-bundler.js @@ -40,18 +40,4 @@ esbuild.buildSync({ outfile: "build/lib/index.cjs", tsconfig: 'tsconfig.json', target: "es5" -}); - -const dtsBundleCommand = spawn('npx dts-bundle-generator --project tsconfig.json -o build/lib/index.d.ts src/index.ts', { - shell: true -}); - -dtsBundleCommand.stdout.on('data', data => console.log(data.toString())); -dtsBundleCommand.stderr.on('data', data => console.error(data.toString())); - -// // Forces synchronicity; done mostly so that the logs don't get jumbled up. -// dtsBundleCommand.on('exit', () => { -// if(dtsBundleCommand.exitCode != 0) { -// process.exit(dtsBundleCommand.exitCode); -// } -// }); \ No newline at end of file +}); \ No newline at end of file diff --git a/common/web/keyboard-processor/build.sh b/common/web/keyboard-processor/build.sh index bc6b0ac617..0fffa4cd47 100755 --- a/common/web/keyboard-processor/build.sh +++ b/common/web/keyboard-processor/build.sh @@ -51,6 +51,10 @@ fi if builder_start_action build; then 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 diff --git a/tsconfig-base.json b/tsconfig-base.json index 8fb2142411..1b00981346 100644 --- a/tsconfig-base.json +++ b/tsconfig-base.json @@ -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"], } From b0504394dd9e6fc64fcea332eee2fbc40a79c517 Mon Sep 17 00:00:00 2001 From: Joshua Horton Date: Thu, 2 Feb 2023 13:24:25 +0700 Subject: [PATCH 20/23] chore(common/models): Apply suggestions from code review Co-authored-by: Marc Durdin --- common/test/resources/keyboards/khmer_angkor.js | 13 ++----------- common/test/resources/keyboards/test_deadkeys.js | 13 ++----------- .../test/resources/keyboards/web_context_tests.js | 13 ++----------- common/web/keyboard-processor/src/text/codes.ts | 4 ++-- 4 files changed, 8 insertions(+), 35 deletions(-) diff --git a/common/test/resources/keyboards/khmer_angkor.js b/common/test/resources/keyboards/khmer_angkor.js index 204d3e90c3..ce268a7675 100644 --- a/common/test/resources/keyboards/khmer_angkor.js +++ b/common/test/resources/keyboards/khmer_angkor.js @@ -6,17 +6,8 @@ KeymanWeb.KR(new Keyboard_khmer_angkor()); } function Keyboard_khmer_angkor() { - var Codes, modCodes, keyCodes; - - if(KeymanWeb.Codes) { - // ES Module attachment point - Codes = KeymanWeb.Codes; - } else if (typeof com != 'undefined' && com.keyman && com.keyman.text && com.keyman.text.Codes) { - // Pre-modularized attachment point - Codes = com.keyman.text.Codes; - } - var modCodes = Codes.modifierCodes; - var keyCodes = Codes.keyCodes; + var modCodes = KeymanWeb.Codes.modifierCodes; + var keyCodes = KeymanWeb.Codes.keyCodes; this.KI="Keyboard_khmer_angkor"; this.KN="Khmer Angkor"; diff --git a/common/test/resources/keyboards/test_deadkeys.js b/common/test/resources/keyboards/test_deadkeys.js index a8937ada32..0aa738e7b6 100644 --- a/common/test/resources/keyboards/test_deadkeys.js +++ b/common/test/resources/keyboards/test_deadkeys.js @@ -6,17 +6,8 @@ KeymanWeb.KR(new Keyboard_test_deadkeys()); } function Keyboard_test_deadkeys() { - var Codes, modCodes, keyCodes; - - if(KeymanWeb.Codes) { - // ES Module attachment point - Codes = KeymanWeb.Codes; - } else if (typeof com != 'undefined' && com.keyman && com.keyman.text && com.keyman.text.Codes) { - // Pre-modularized attachment point - Codes = com.keyman.text.Codes; - } - var modCodes = Codes.modifierCodes; - var keyCodes = Codes.keyCodes; + var modCodes = KeymanWeb.Codes.modifierCodes; + var keyCodes = KeymanWeb.Codes.keyCodes; this.KI="Keyboard_test_deadkeys"; this.KN="Keyman Deadkey Stress-Tester"; diff --git a/common/test/resources/keyboards/web_context_tests.js b/common/test/resources/keyboards/web_context_tests.js index aa48bd2791..9f5bc015e0 100644 --- a/common/test/resources/keyboards/web_context_tests.js +++ b/common/test/resources/keyboards/web_context_tests.js @@ -6,17 +6,8 @@ KeymanWeb.KR(new Keyboard_web_context_tests()); } function Keyboard_web_context_tests() { - var Codes, modCodes, keyCodes; - - if(KeymanWeb.Codes) { - // ES Module attachment point - Codes = KeymanWeb.Codes; - } else if (typeof com != 'undefined' && com.keyman && com.keyman.text && com.keyman.text.Codes) { - // Pre-modularized attachment point - Codes = com.keyman.text.Codes; - } - var modCodes = Codes.modifierCodes; - var keyCodes = Codes.keyCodes; + var modCodes = KeymanWeb.Codes.modifierCodes; + var keyCodes = KeymanWeb.Codes.keyCodes; this._v=(typeof keyman!="undefined"&&typeof keyman.version=="string")?parseInt(keyman.version,10):9; this.KI="Keyboard_web_context_tests"; diff --git a/common/web/keyboard-processor/src/text/codes.ts b/common/web/keyboard-processor/src/text/codes.ts index c778400cf1..adac5ec32a 100644 --- a/common/web/keyboard-processor/src/text/codes.ts +++ b/common/web/keyboard-processor/src/text/codes.ts @@ -1,4 +1,4 @@ -let Codes = { +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: { @@ -90,7 +90,7 @@ let Codes = { // 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 = Codes[keyID]; + const code = Codes[keyID]; if(code > 50000 && code < 50011) { return true; } From 857bc80e4ad7ba2b6712f92527bea27ac7337db5 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 2 Feb 2023 13:36:36 +0700 Subject: [PATCH 21/23] fix(common/web): debug endpoints to com.keyman.text.Codes --- common/test/resources/keyboards/khmer_angkor.js | 4 ++-- common/test/resources/keyboards/test_deadkeys.js | 4 ++-- common/test/resources/keyboards/web_context_tests.js | 4 ++-- .../web/keyboard-processor/src/text/keyboardProcessor.ts | 8 ++++++++ 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/common/test/resources/keyboards/khmer_angkor.js b/common/test/resources/keyboards/khmer_angkor.js index ce268a7675..83b2eaffa0 100644 --- a/common/test/resources/keyboards/khmer_angkor.js +++ b/common/test/resources/keyboards/khmer_angkor.js @@ -6,8 +6,8 @@ KeymanWeb.KR(new Keyboard_khmer_angkor()); } function Keyboard_khmer_angkor() { - var modCodes = KeymanWeb.Codes.modifierCodes; - var keyCodes = KeymanWeb.Codes.keyCodes; + var modCodes = com.keyman.text.Codes.modifierCodes; + var keyCodes = com.keyman.text.Codes.keyCodes; this.KI="Keyboard_khmer_angkor"; this.KN="Khmer Angkor"; diff --git a/common/test/resources/keyboards/test_deadkeys.js b/common/test/resources/keyboards/test_deadkeys.js index 0aa738e7b6..b37320fb07 100644 --- a/common/test/resources/keyboards/test_deadkeys.js +++ b/common/test/resources/keyboards/test_deadkeys.js @@ -6,8 +6,8 @@ KeymanWeb.KR(new Keyboard_test_deadkeys()); } function Keyboard_test_deadkeys() { - var modCodes = KeymanWeb.Codes.modifierCodes; - var keyCodes = KeymanWeb.Codes.keyCodes; + var modCodes = com.keyman.text.Codes.modifierCodes; + var keyCodes = com.keyman.text.Codes.keyCodes; this.KI="Keyboard_test_deadkeys"; this.KN="Keyman Deadkey Stress-Tester"; diff --git a/common/test/resources/keyboards/web_context_tests.js b/common/test/resources/keyboards/web_context_tests.js index 9f5bc015e0..ab8ae076b0 100644 --- a/common/test/resources/keyboards/web_context_tests.js +++ b/common/test/resources/keyboards/web_context_tests.js @@ -6,8 +6,8 @@ KeymanWeb.KR(new Keyboard_web_context_tests()); } function Keyboard_web_context_tests() { - var modCodes = KeymanWeb.Codes.modifierCodes; - var keyCodes = KeymanWeb.Codes.keyCodes; + var modCodes = com.keyman.text.Codes.modifierCodes; + var keyCodes = com.keyman.text.Codes.keyCodes; this._v=(typeof keyman!="undefined"&&typeof keyman.version=="string")?parseInt(keyman.version,10):9; this.KI="Keyboard_web_context_tests"; diff --git a/common/web/keyboard-processor/src/text/keyboardProcessor.ts b/common/web/keyboard-processor/src/text/keyboardProcessor.ts index 897661a94a..dd86929938 100644 --- a/common/web/keyboard-processor/src/text/keyboardProcessor.ts +++ b/common/web/keyboard-processor/src/text/keyboardProcessor.ts @@ -83,6 +83,14 @@ export default class KeyboardProcessor { let globalThis = getGlobalObject(); globalThis[KeyboardInterface.GLOBAL_NAME] = this.keyboardInterface; + // Maintains debug definitions - debug keyboard compilations refer to these code definitions. + // + // Note: this is targeted for deprecation and is only included for legacy precompiled keyboards. + const com = globalThis['com'] = globalThis['com'] || {}; + const keyman = com['keyman'] = com['keyman'] || {}; + const text = keyman['text'] = keyman['text'] || {}; + text['Codes'] = Codes; + // Ensure that the active keyboard is set on the keyboard interface object. if(this.activeKeyboard) { this.keyboardInterface.activeKeyboard = this.activeKeyboard; From 9e45b4936f170ec4bc24a99b453f48b5778e289f Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 2 Feb 2023 13:41:24 +0700 Subject: [PATCH 22/23] fix(web): oh yeah, old keyman.osk endpoint for Codes properties --- common/test/resources/keyboards/web_context_tests.js | 4 ++-- .../web/keyboard-processor/src/text/keyboardProcessor.ts | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/common/test/resources/keyboards/web_context_tests.js b/common/test/resources/keyboards/web_context_tests.js index ab8ae076b0..2c1b93ec8a 100644 --- a/common/test/resources/keyboards/web_context_tests.js +++ b/common/test/resources/keyboards/web_context_tests.js @@ -6,8 +6,8 @@ KeymanWeb.KR(new Keyboard_web_context_tests()); } function Keyboard_web_context_tests() { - var modCodes = com.keyman.text.Codes.modifierCodes; - var keyCodes = com.keyman.text.Codes.keyCodes; + var modCodes = keyman.osk.modifierCodes; + var keyCodes = keyman.osk.keyCodes; this._v=(typeof keyman!="undefined"&&typeof keyman.version=="string")?parseInt(keyman.version,10):9; this.KI="Keyboard_web_context_tests"; diff --git a/common/web/keyboard-processor/src/text/keyboardProcessor.ts b/common/web/keyboard-processor/src/text/keyboardProcessor.ts index dd86929938..273769b3b7 100644 --- a/common/web/keyboard-processor/src/text/keyboardProcessor.ts +++ b/common/web/keyboard-processor/src/text/keyboardProcessor.ts @@ -87,10 +87,15 @@ export default class KeyboardProcessor { // // Note: this is targeted for deprecation and is only included for legacy precompiled keyboards. const com = globalThis['com'] = globalThis['com'] || {}; - const keyman = com['keyman'] = com['keyman'] || {}; + let keyman = com['keyman'] = com['keyman'] || {}; const text = keyman['text'] = keyman['text'] || {}; text['Codes'] = Codes; + keyman = globalThis['keyman'] = globalThis['keyman'] || {}; + const osk = keyman['osk'] || keyman['osk'] || {}; + osk['modifierCodes'] = Codes.modifierCodes; + osk['keyCodes'] = Codes.keyCodes; + // Ensure that the active keyboard is set on the keyboard interface object. if(this.activeKeyboard) { this.keyboardInterface.activeKeyboard = this.activeKeyboard; From fab0dd5820039fc9bcd67e6c579b8412b00055bf Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 2 Feb 2023 14:01:03 +0700 Subject: [PATCH 23/23] change(common/web): another legacy endpoint tweak --- .../src/text/keyboardProcessor.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/common/web/keyboard-processor/src/text/keyboardProcessor.ts b/common/web/keyboard-processor/src/text/keyboardProcessor.ts index 273769b3b7..10e63f5a66 100644 --- a/common/web/keyboard-processor/src/text/keyboardProcessor.ts +++ b/common/web/keyboard-processor/src/text/keyboardProcessor.ts @@ -85,14 +85,12 @@ export default class KeyboardProcessor { // Maintains debug definitions - debug keyboard compilations refer to these code definitions. // - // Note: this is targeted for deprecation and is only included for legacy precompiled keyboards. - const com = globalThis['com'] = globalThis['com'] || {}; - let keyman = com['keyman'] = com['keyman'] || {}; - const text = keyman['text'] = keyman['text'] || {}; - text['Codes'] = Codes; - - keyman = globalThis['keyman'] = globalThis['keyman'] || {}; - const osk = keyman['osk'] || keyman['osk'] || {}; + // Note: these are targeted for deprecation and is only included for legacy precompiled keyboards. + // + // Refer to C:\keymanapp\keyman\developer\src\tike\compile\CompileKeymanWeb.pas, + // TCompileKeymanWeb.JavaScript_SetupDebug. + const keyman = globalThis['keyman'] = globalThis['keyman'] || {}; + const osk = keyman['osk'] || keyman['osk'] || {}; // does not otherwise exist when headless or detached from OSKs. osk['modifierCodes'] = Codes.modifierCodes; osk['keyCodes'] = Codes.keyCodes;