/// /// // Defines classes for handling system stores /// /*** KeymanWeb 11.0 Copyright 2019 SIL International ***/ namespace com.keyman.text { //#region Helper type definitions export class KeyInformation { vk: boolean; code: number; modifiers: number; } /* * 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; export type KeyboardStoreElement = (string|StoreNonCharEntry); export type ComplexKeyboardStore = KeyboardStoreElement[]; type KeyboardStore = PlainKeyboardStore | ComplexKeyboardStore; export type VariableStore = {[name: string]: string}; type RuleChar = string; class RuleDeadkey { /** Discriminant field - 'd' for Deadkey. */ ['t']: 'd'; /** * 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: text.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; } }; //#endregion export class KeyboardInterface { static readonly GLOBAL_NAME = 'KeymanWeb'; cachedContext: CachedContext = new CachedContext(); cachedContextEx: CachedContextEx = new CachedContextEx(); activeTargetOutput: OutputTarget; ruleBehavior: RuleBehavior; static readonly TSS_LAYER: number = 33; static readonly TSS_PLATFORM: number = 31; systemStores: {[storeID: number]: SystemStore}; _AnyIndices: number[] = []; // AnyIndex - array of any/index match indices // Must be accessible to some of the keyboard API methods. activeKeyboard: any; activeDevice: EngineDeviceSpec; 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.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; } 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 = ''; tempContext = 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); } // 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); 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 we actually match or if we have nul context (\uFFFE), notany fails. } 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 } } /** * 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"]; 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) == e.Lmodifiers)); //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']) { 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 ""; } } /** * 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(); } /** * 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(); if(systemId == KeyboardInterface.TSS_LAYER) { // 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) { this.output(1, outputTarget, ""); } /** * Function processKeystroke * Scope Private * @param {Object} element The page element 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 {number} 0 if no match is made, otherwise 1. */ processKeystroke(outputTarget: OutputTarget, keystroke: KeyEvent): 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!"; } 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); // 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 = this.activeKeyboard.process(outputTarget, keystroke); this.activeTargetOutput = null; if(!matched) { return null; } // Finalize the rule's results. this.ruleBehavior.transcription = outputTarget.buildTranscriptionFrom(preInput, keystroke); // Clear our result-tracking variable to prevent any possible pollution for future processing. let behavior = this.ruleBehavior; this.ruleBehavior = null; return behavior; } } }