Skip to content

Commit 1b14f5c

Browse files
refactor(web): move RuleBehavior to engine/keyboard
This requires to move some other files around. Also move `finalize` and `mergeInDefaults` methods to processor and rename them to `finalizeProcessorAction` and `mergeInOtherProcessorAction` (decided to directly use the term `ProcessorAction`, to which `RuleBehavior` will be renamed to in a following PR). Co-authored-by: Marc Durdin <[email protected]>
1 parent 657585e commit 1b14f5c

25 files changed

+303
-247
lines changed

web/src/app/browser/src/configuration.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { EngineConfiguration, InitOptionSpec, InitOptionDefaults } from "keyman/engine/main";
22

33
import { OutputTargetElementWrapper as DOMOutputTarget } from 'keyman/engine/element-wrappers';
4-
import { OutputTargetInterface } from 'keyman/engine/keyboard';
5-
import { isEmptyTransform, RuleBehavior } from 'keyman/engine/js-processor';
4+
import { OutputTargetInterface, RuleBehavior } from 'keyman/engine/keyboard';
5+
import { isEmptyTransform } from 'keyman/engine/js-processor';
66
import { AlertHost } from "./utils/alertHost.js";
77
import { whenDocumentReady } from "./utils/documentReady.js";
88

web/src/app/webview/src/contextManager.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { JSKeyboard, Keyboard, OutputTargetInterface } from 'keyman/engine/keyboard';
1+
import { JSKeyboard, Keyboard, OutputTargetInterface, Transcription, TextTransform } from 'keyman/engine/keyboard';
22
// TODO-web-core: remove usage of OutputTargetBase, use OutputTargetInterface instead
3-
import { Mock, Transcription, findCommonSubstringEndIndex, isEmptyTransform, TextTransform, OutputTargetBase } from 'keyman/engine/js-processor';
3+
import { Mock, findCommonSubstringEndIndex, isEmptyTransform, OutputTargetBase } from 'keyman/engine/js-processor';
44
import { KeyboardStub } from 'keyman/engine/keyboard-storage';
55
import { ContextManagerBase } from 'keyman/engine/main';
66
import { WebviewConfiguration } from './configuration.js';
@@ -31,7 +31,8 @@ export class ContextHost extends Mock {
3131
let transform: TextTransform = null;
3232

3333
if(transcription) {
34-
const preInput = transcription.preInput;
34+
//TODO-web-core: shouldn't need cast in the future?
35+
const preInput = transcription.preInput as Mock;
3536
// If our saved state matches the `preInput` from the incoming transcription, just reuse its transform.
3637
// Will generally not match during multitap operations, though.
3738
//

web/src/app/webview/src/keymanEngine.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { DeviceSpec, DefaultRules } from 'keyman/engine/keyboard';
2-
import { RuleBehavior } from 'keyman/engine/js-processor';
1+
import { DeviceSpec, DefaultRules, RuleBehavior } from 'keyman/engine/keyboard';
32
import { KeymanEngineBase, KeyboardInterfaceBase } from 'keyman/engine/main';
43
import { AnchoredOSKView, ViewConfiguration, StaticActivator } from 'keyman/engine/osk';
54
import { getAbsoluteX, getAbsoluteY } from 'keyman/engine/dom-utils';

web/src/engine/js-processor/src/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
export { BeepHandler, JSKeyboardProcessor, LogMessageHandler, ProcessorInitOptions } from "./jsKeyboardProcessor.js";
2-
export { RuleBehavior } from "./ruleBehavior.js";
32
export { JSKeyboardInterface, KeyInformation, StoreNonCharEntry } from "./jsKeyboardInterface.js";
43
export * from "./deadkeys.js";
5-
export * from "./stores.js";
4+
export { type ComplexKeyboardStore } from "./stores.js";
65
export { OutputTargetBase } from "./outputTargetBase.js";
76
export * from "./outputTargetBase.js";
87
export { Mock } from "./mock.js";

web/src/engine/js-processor/src/jsKeyboardInterface.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,27 @@
77

88
import { type DeviceSpec, KMWString } from "@keymanapp/web-utils";
99
import { ModifierKeyConstants } from '@keymanapp/common-types';
10-
import { Codes, type KeyEvent, KeyMapping, JSKeyboard, KeyboardHarness, KeyboardKeymanGlobal, type OutputTargetInterface, VariableStoreDictionary } from "keyman/engine/keyboard";
10+
import {
11+
Codes,
12+
JSKeyboard,
13+
KeyboardHarness,
14+
KeyboardKeymanGlobal,
15+
KeyMapping,
16+
MutableSystemStore,
17+
SystemStore,
18+
SystemStoreIDs,
19+
type KeyEvent,
20+
type OutputTargetInterface,
21+
RuleBehavior,
22+
VariableStore,
23+
VariableStoreDictionary,
24+
VariableStoreSerializer,
25+
} from "keyman/engine/keyboard";
1126
import { type OutputTargetBase } from './outputTargetBase.js';
1227
import { type Deadkey } from './deadkeys.js';
1328
import { Mock } from "./mock.js";
14-
import { RuleBehavior } from "./ruleBehavior.js";
15-
import { SystemStoreIDs, SystemStore, MutableSystemStore } from "keyman/engine/keyboard";
1629
import { PlatformSystemStore } from './platformSystemStore.js';
17-
import { ComplexKeyboardStore, type KeyboardStore, KeyboardStoreElement, VariableStore, VariableStoreSerializer } from "./stores.js";
30+
import { ComplexKeyboardStore, type KeyboardStore, KeyboardStoreElement } from "./stores.js";
1831

1932
//#endregion
2033

web/src/engine/js-processor/src/jsKeyboardProcessor.ts

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,19 @@ import { EventEmitter } from 'eventemitter3';
1010
import { ModifierKeyConstants } from '@keymanapp/common-types';
1111
import {
1212
Codes, type JSKeyboard, MinimalKeymanGlobal, KeyEvent, Layouts,
13-
DefaultRules, EmulationKeystrokes,
14-
type MutableSystemStore, SystemStoreIDs,
13+
DefaultRules, EmulationKeystrokes, type MutableSystemStore,
14+
OutputTargetInterface, RuleBehavior, SystemStoreIDs
1515
} from "keyman/engine/keyboard";
1616
import { Mock } from "./mock.js";
1717
import { type OutputTargetBase } from "./outputTargetBase.js";
18-
import { RuleBehavior } from "./ruleBehavior.js";
1918
import { JSKeyboardInterface } from './jsKeyboardInterface.js';
2019
import { DeviceSpec, globalObject, KMWString } from "@keymanapp/web-utils";
2120

2221
// #endregion
2322

2423
// Also relies on @keymanapp/web-utils, which is included via tsconfig.json.
2524

26-
export type BeepHandler = (outputTarget: OutputTargetBase) => void;
25+
export type BeepHandler = (outputTarget: OutputTargetInterface) => void;
2726
export type LogMessageHandler = (str: string) => void;
2827

2928
export interface ProcessorInitOptions {
@@ -250,7 +249,7 @@ export class JSKeyboardProcessor extends EventEmitter<EventMap> {
250249
if(!matchBehavior) {
251250
matchBehavior = defaultBehavior;
252251
} else {
253-
matchBehavior.mergeInDefaults(defaultBehavior);
252+
this.mergeInOtherProcessorAction(matchBehavior, defaultBehavior);
254253
}
255254
matchBehavior.triggerKeyDefault = false; // We've triggered it successfully.
256255
} // If null, we must rely on something else (like the browser, in DOM-aware code) to fulfill the default.
@@ -577,8 +576,8 @@ export class JSKeyboardProcessor extends EventEmitter<EventMap> {
577576
private performNewContextEvent(outputTarget: OutputTargetBase): RuleBehavior {
578577
const ruleBehavior = this.processNewContextEvent(this.contextDevice, outputTarget);
579578

580-
if(ruleBehavior) {
581-
ruleBehavior.finalize(this, outputTarget, true);
579+
if (ruleBehavior) {
580+
this.finalizeProcessorAction(ruleBehavior, outputTarget);
582581
}
583582
return ruleBehavior;
584583
}
@@ -610,4 +609,76 @@ export class JSKeyboardProcessor extends EventEmitter<EventMap> {
610609
}
611610
}
612611
};
612+
613+
public finalizeProcessorAction(data: RuleBehavior, outputTarget: OutputTargetInterface): void {
614+
if (!data.transcription) {
615+
throw "Cannot finalize a RuleBehavior with no transcription.";
616+
}
617+
618+
if (this.beepHandler && data.beep) {
619+
this.beepHandler(outputTarget);
620+
}
621+
622+
for (const storeID in data.setStore) {
623+
const sysStore = this.keyboardInterface.systemStores[storeID];
624+
if (sysStore) {
625+
try {
626+
sysStore.set(data.setStore[storeID]);
627+
} catch (error) {
628+
if (this.errorLogger) {
629+
this.errorLogger("Rule attempted to perform illegal operation - 'platform' may not be changed.");
630+
}
631+
}
632+
} else if (this.warningLogger) {
633+
this.warningLogger("Unknown store affected by keyboard rule: " + storeID);
634+
}
635+
}
636+
637+
this.keyboardInterface.applyVariableStores(data.variableStores);
638+
639+
if (this.keyboardInterface.variableStoreSerializer) {
640+
for (const storeID in data.saveStore) {
641+
this.keyboardInterface.variableStoreSerializer.saveStore(this.activeKeyboard.id, storeID, data.saveStore[storeID]);
642+
}
643+
}
644+
645+
if (data.triggersDefaultCommand) {
646+
const keyEvent = data.transcription.keystroke;
647+
this.defaultRules.applyCommand(keyEvent, outputTarget);
648+
}
649+
650+
if (this.warningLogger && data.warningLog) {
651+
this.warningLogger(data.warningLog);
652+
} else if (this.errorLogger && data.errorLog) {
653+
this.errorLogger(data.errorLog);
654+
}
655+
}
656+
657+
/**
658+
* Merges default-related behaviors from another RuleBehavior into this one. Assumes that the current instance
659+
* "came first" chronologically. Both RuleBehaviors must be sourced from the same keystroke.
660+
*
661+
* Intended use: merging rule-based behavior with default key behavior during scenarios like those described
662+
* at https://github.com/keymanapp/keyman/pull/4350#issuecomment-768753852.
663+
*
664+
* This function does not attempt a "complete" merge for two fully-constructed RuleBehaviors! Things
665+
* WILL break for unintended uses.
666+
* @param other
667+
*/
668+
private mergeInOtherProcessorAction(first: RuleBehavior, other: RuleBehavior): void {
669+
const keystroke = first.transcription.keystroke;
670+
const keyFromOther = other.transcription.keystroke;
671+
if (keystroke.Lcode != keyFromOther.Lcode || keystroke.Lmodifiers != keyFromOther.Lmodifiers) {
672+
throw "RuleBehavior default-merge not supported unless keystrokes are identical!";
673+
}
674+
675+
first.triggersDefaultCommand = first.triggersDefaultCommand || other.triggersDefaultCommand;
676+
677+
const mergingMock = Mock.from(first.transcription.preInput, false);
678+
mergingMock.apply(first.transcription.transform);
679+
mergingMock.apply(other.transcription.transform);
680+
681+
first.transcription = mergingMock.buildTranscriptionFrom(first.transcription.preInput, keystroke, false, first.transcription.alternates);
682+
}
683+
613684
}

web/src/engine/js-processor/src/mock.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { OutputTargetInterface } from 'keyman/engine/keyboard';
12
import { OutputTargetBase } from './outputTargetBase.js';
23
import { KMWString } from '@keymanapp/web-utils';
34

@@ -25,10 +26,18 @@ export class Mock extends OutputTargetBase {
2526
this.selForward = this.selEnd >= this.selStart;
2627
}
2728

29+
static assertIsOutputTargetBase(outputTarget: OutputTargetInterface): asserts outputTarget is OutputTargetBase {
30+
if (!(outputTarget instanceof OutputTargetBase)) {
31+
throw new TypeError("outputTarget is not a OutputTargetBase");
32+
}
33+
}
34+
2835
// Clones the state of an existing EditableElement, creating a Mock version of its state.
29-
static from(outputTarget: OutputTargetBase, readonly?: boolean) {
36+
static from(outputTarget: OutputTargetInterface, readonly?: boolean): Mock {
3037
let clone: Mock;
3138

39+
this.assertIsOutputTargetBase(outputTarget);
40+
3241
if (outputTarget instanceof Mock) {
3342
// Avoids the need to run expensive kmwstring.ts `length()`
3443
// calculations when deep-copying Mock instances.
@@ -56,7 +65,7 @@ export class Mock extends OutputTargetBase {
5665
}
5766

5867
// Also duplicate deadkey state! (Needed for fat-finger ops.)
59-
clone.setDeadkeys(outputTarget.deadkeys());
68+
clone.setDeadkeys((outputTarget as OutputTargetBase).deadkeys());
6069

6170
return clone;
6271
}

web/src/engine/js-processor/src/outputTargetBase.ts

Lines changed: 3 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { KMWString } from "@keymanapp/web-utils";
2+
import { Alternate, OutputTargetInterface, TextTransform, Transcription } from 'keyman/engine/keyboard';
23
import { findCommonSubstringEndIndex } from "./stringDivergence.js";
34
import { Mock } from "./mock.js";
4-
import { OutputTargetInterface } from 'keyman/engine/keyboard';
55

66
// Defines deadkey management in a manner attachable to each element interface.
77
import { type KeyEvent } from 'keyman/engine/keyboard';
@@ -17,53 +17,6 @@ export function isEmptyTransform(transform: LexicalModelTypes.Transform) {
1717
return transform.insert === '' && transform.deleteLeft === 0 && (transform.deleteRight ?? 0) === 0;
1818
}
1919

20-
export class TextTransform implements LexicalModelTypes.Transform {
21-
readonly insert: string;
22-
readonly deleteLeft: number;
23-
readonly deleteRight: number;
24-
readonly erasedSelection: boolean;
25-
id: number;
26-
27-
constructor(insert: string, deleteLeft: number, deleteRight: number, erasedSelection: boolean) {
28-
this.insert = insert;
29-
this.deleteLeft = deleteLeft;
30-
this.deleteRight = deleteRight;
31-
this.erasedSelection = erasedSelection;
32-
}
33-
34-
public static readonly nil = new TextTransform('', 0, 0, false);
35-
}
36-
37-
export class Transcription {
38-
readonly token: number;
39-
readonly keystroke: KeyEvent;
40-
readonly transform: TextTransform;
41-
alternates: Alternate[]; // constructed after the rest of the transcription.
42-
readonly preInput: Mock;
43-
44-
private static tokenSeed: number = 0;
45-
46-
constructor(keystroke: KeyEvent, transform: TextTransform, preInput: Mock, alternates?: Alternate[]/*, removedDks: Deadkey[], insertedDks: Deadkey[]*/) {
47-
const token = this.token = Transcription.tokenSeed++;
48-
49-
this.keystroke = keystroke;
50-
this.transform = transform;
51-
this.alternates = alternates;
52-
this.preInput = preInput;
53-
54-
this.transform.id = this.token;
55-
56-
// Assign the ID to each alternate, as well.
57-
if(alternates) {
58-
alternates.forEach(function(alt) {
59-
alt.sample.id = token;
60-
});
61-
}
62-
}
63-
}
64-
65-
export type Alternate = LexicalModelTypes.ProbabilityMass<LexicalModelTypes.Transform>;
66-
6720
export abstract class OutputTargetBase implements OutputTargetInterface {
6821
private _dks: DeadkeyTracker;
6922

@@ -122,7 +75,7 @@ export abstract class OutputTargetBase implements OutputTargetInterface {
12275
* As such, it assumes that the caret is immediately after any inserted text.
12376
* @param from An output target (preferably a Mock) representing the prior state of the input/output system.
12477
*/
125-
buildTransformFrom(original: OutputTargetBase): TextTransform {
78+
buildTransformFrom(original: OutputTargetInterface): TextTransform {
12679
const toLeft = this.getTextBeforeCaret();
12780
const fromLeft = original.getTextBeforeCaret();
12881

@@ -143,7 +96,7 @@ export abstract class OutputTargetBase implements OutputTargetInterface {
14396
return new TextTransform(insertedText, deletedLeft, deletedRight, original.getSelectedText() && !this.getSelectedText());
14497
}
14598

146-
buildTranscriptionFrom(original: OutputTargetBase, keyEvent: KeyEvent, readonly: boolean, alternates?: Alternate[]): Transcription {
99+
buildTranscriptionFrom(original: OutputTargetInterface, keyEvent: KeyEvent, readonly: boolean, alternates?: Alternate[]): Transcription {
147100
const transform = this.buildTransformFrom(original);
148101

149102
// If we ever decide to re-add deadkey tracking, this is the place for it.

0 commit comments

Comments
 (0)