From 533473c1997d5b8c9db93c3872a915cddb0c3399 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Tue, 4 Nov 2025 17:53:15 +0100 Subject: [PATCH 1/2] refactor(web): extract minimal TextStore interface that LanguageProcessor can use --- .../src/prediction/languageProcessor.interface.ts | 4 ++-- web/src/engine/keyboard/src/index.ts | 1 + web/src/engine/keyboard/src/syntheticTextStore.ts | 11 +++-------- web/src/engine/keyboard/src/textStore.ts | 14 ++++++++++++-- .../src/textStoreTranscriptionInterface.ts | 15 +++++++++++++++ .../engine/main/src/headless/languageProcessor.ts | 12 ++++++------ 6 files changed, 39 insertions(+), 18 deletions(-) create mode 100644 web/src/engine/keyboard/src/textStoreTranscriptionInterface.ts diff --git a/web/src/engine/interfaces/src/prediction/languageProcessor.interface.ts b/web/src/engine/interfaces/src/prediction/languageProcessor.interface.ts index d15898078d..eef934495a 100644 --- a/web/src/engine/interfaces/src/prediction/languageProcessor.interface.ts +++ b/web/src/engine/interfaces/src/prediction/languageProcessor.interface.ts @@ -1,6 +1,6 @@ import { LexicalModelTypes } from '@keymanapp/common-types'; import { EventEmitter } from "eventemitter3"; -import { TextStore } from "keyman/engine/keyboard"; +import { TextStore, TextStoreTranscriptionInterface } from "keyman/engine/keyboard"; export class ReadySuggestions { suggestions: LexicalModelTypes.Suggestion[]; @@ -48,7 +48,7 @@ export interface LanguageProcessorEventMap { * @param textStore The `TextStore` representation of the context the suggestion was applied to. * @returns */ - 'suggestionapplied': (textStore: TextStore) => boolean + 'suggestionapplied': (textStore: TextStoreTranscriptionInterface) => boolean } diff --git a/web/src/engine/keyboard/src/index.ts b/web/src/engine/keyboard/src/index.ts index fc8064cb37..15f97cbf8e 100644 --- a/web/src/engine/keyboard/src/index.ts +++ b/web/src/engine/keyboard/src/index.ts @@ -33,6 +33,7 @@ export { type VariableStore, VariableStoreSerializer, VariableStoreDictionary } export { SyntheticTextStore } from "./syntheticTextStore.js"; export { TextStore } from "./textStore.js"; +export { TextStoreTranscriptionInterface } from "./textStoreTranscriptionInterface.js"; export { findCommonSubstringEndIndex } from "./stringDivergence.js"; export { Deadkey } from "./deadkeys.js"; diff --git a/web/src/engine/keyboard/src/syntheticTextStore.ts b/web/src/engine/keyboard/src/syntheticTextStore.ts index d015b52fe6..bf4f0815c8 100644 --- a/web/src/engine/keyboard/src/syntheticTextStore.ts +++ b/web/src/engine/keyboard/src/syntheticTextStore.ts @@ -1,4 +1,5 @@ import { TextStore } from './textStore.js'; +import { TextStoreTranscriptionInterface } from './textStoreTranscriptionInterface.js'; import { KMWString } from '@keymanapp/web-utils'; export class SyntheticTextStore extends TextStore { @@ -25,17 +26,11 @@ export class SyntheticTextStore extends TextStore { this.selForward = this.selEnd >= this.selStart; } - static assertIsOutputTargetBase(textStore: TextStore): asserts textStore is TextStore { - if (!(textStore instanceof TextStore)) { - throw new TypeError("textStore is not a TextStore"); - } - } - // Clones the state of an existing EditableElement, creating a SyntheticTextStore version of its state. - static from(textStore: TextStore, readonly?: boolean): SyntheticTextStore { + static from(textStore: TextStoreTranscriptionInterface, readonly?: boolean): SyntheticTextStore { let clone: SyntheticTextStore; - this.assertIsOutputTargetBase(textStore); + this.assertIsTextStore(textStore); if (textStore instanceof SyntheticTextStore) { // Avoids the need to run expensive kmwstring.ts `length()` diff --git a/web/src/engine/keyboard/src/textStore.ts b/web/src/engine/keyboard/src/textStore.ts index 9b405deee3..55ec6bd774 100644 --- a/web/src/engine/keyboard/src/textStore.ts +++ b/web/src/engine/keyboard/src/textStore.ts @@ -5,7 +5,8 @@ import { findCommonSubstringEndIndex } from "./stringDivergence.js"; import { SyntheticTextStore } from "./syntheticTextStore.js"; // Defines deadkey management in a manner attachable to each element interface. -import { type KeyEvent } from 'keyman/engine/keyboard'; +import { type KeyEvent } from './keyEvent.js'; +import { TextStoreTranscriptionInterface } from './textStoreTranscriptionInterface.js'; import { Deadkey, DeadkeyTracker } from "./deadkeys.js"; import { LexicalModelTypes } from '@keymanapp/common-types'; @@ -59,6 +60,13 @@ export abstract class TextStore { this._dks = dks.clone(); } + static assertIsTextStore(textStore: TextStoreTranscriptionInterface): asserts textStore is TextStore { + if (!(textStore instanceof TextStore)) { + throw new TypeError("textStore is not a TextStore"); + } + } + + /** * Determines the basic operations needed to reconstruct the current TextStore's text from the prior state specified * by another TextStore based on their text and caret positions. @@ -67,7 +75,9 @@ export abstract class TextStore { * As such, it assumes that the caret is immediately after any inserted text. * @param from An output target (preferably a SyntheticTextStore) representing the prior state of the input/output system. */ - buildTransformFrom(original: TextStore): TextTransform { + buildTransformFrom(original: TextStoreTranscriptionInterface): TextTransform { + TextStore.assertIsTextStore(original); + const toLeft = this.getTextBeforeCaret(); const fromLeft = original.getTextBeforeCaret(); diff --git a/web/src/engine/keyboard/src/textStoreTranscriptionInterface.ts b/web/src/engine/keyboard/src/textStoreTranscriptionInterface.ts new file mode 100644 index 0000000000..259318adcf --- /dev/null +++ b/web/src/engine/keyboard/src/textStoreTranscriptionInterface.ts @@ -0,0 +1,15 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +import { LexicalModelTypes } from '@keymanapp/common-types'; +import { KeyEvent } from './keyEvent.js'; +import { Transcription } from './keyboards/transcription.js'; +import { Alternate } from './keyboards/textTransform.js'; + +// Interface with the methods LanguageProcessor needs from TextStore +// for transcription building and applying transforms. +export interface TextStoreTranscriptionInterface { + buildTranscriptionFrom(original: TextStoreTranscriptionInterface, keyEvent: KeyEvent, readonly: boolean, alternates?: Alternate[]): Transcription; + apply(transform: LexicalModelTypes.Transform): void; +} diff --git a/web/src/engine/main/src/headless/languageProcessor.ts b/web/src/engine/main/src/headless/languageProcessor.ts index b8e12ba985..7ded8c1cf7 100644 --- a/web/src/engine/main/src/headless/languageProcessor.ts +++ b/web/src/engine/main/src/headless/languageProcessor.ts @@ -1,6 +1,6 @@ import { EventEmitter } from "eventemitter3"; import { LMLayer, WorkerFactory } from "@keymanapp/lexical-model-layer/web"; -import { Transcription, TextStore, SyntheticTextStore } from 'keyman/engine/keyboard'; +import { Transcription, TextStoreTranscriptionInterface, SyntheticTextStore } from 'keyman/engine/keyboard'; import { LanguageProcessorEventMap, ModelSpec, StateChangeEnum, ReadySuggestions } from 'keyman/engine/interfaces'; import ContextWindow from "./contextWindow.js"; import { TranscriptionCache } from "./transcriptionCache.js"; @@ -125,7 +125,7 @@ export class LanguageProcessor extends EventEmitter { }); } - public invalidateContext(textStore: TextStore, layerId: string): Promise { + public invalidateContext(textStore: TextStoreTranscriptionInterface, layerId: string): Promise { // If there's no active model, there can be no predictions. // We'll also be missing important data needed to even properly REQUEST the predictions. if(!this.currentModel || !this.configuration) { @@ -153,7 +153,7 @@ export class LanguageProcessor extends EventEmitter { } } - public wordbreak(textStore: TextStore, layerId: string): Promise { + public wordbreak(textStore: TextStoreTranscriptionInterface, layerId: string): Promise { if(!this.isActive) { return null; } @@ -188,7 +188,7 @@ export class LanguageProcessor extends EventEmitter { * required because layerid can be changed by PostKeystroke * @returns */ - public applySuggestion(suggestion: Suggestion, textStore: TextStore, getLayerId: ()=>string): Promise { + public applySuggestion(suggestion: Suggestion, textStore: TextStoreTranscriptionInterface, getLayerId: ()=>string): Promise { if(!textStore) { throw "Accepting suggestions requires a destination TextStore instance." } @@ -263,7 +263,7 @@ export class LanguageProcessor extends EventEmitter { } } - public applyReversion(reversion: Reversion, textStore: TextStore) { + public applyReversion(reversion: Reversion, textStore: TextStoreTranscriptionInterface) { if(!textStore) { throw "Accepting suggestions requires a destination TextStore instance." } @@ -305,7 +305,7 @@ export class LanguageProcessor extends EventEmitter { return promise; } - public predictFromTarget(textStore: TextStore, layerId: string): Promise { + public predictFromTarget(textStore: TextStoreTranscriptionInterface, layerId: string): Promise { if(!this.isActive || !textStore) { return null; } From a9f0e2d2128eed4c1742c28d05b73030f4447784 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Wed, 5 Nov 2025 16:54:16 +0100 Subject: [PATCH 2/2] refactor(web): address code review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rename TextStoreTranscriptionInterface → TextStoreLanguageProcessorInterface - also fixup references to TextStore in languageProcessor.interface.ts --- .../src/prediction/languageProcessor.interface.ts | 10 +++++----- web/src/engine/keyboard/src/index.ts | 2 +- web/src/engine/keyboard/src/syntheticTextStore.ts | 4 ++-- web/src/engine/keyboard/src/textStore.ts | 6 +++--- ...ace.ts => textStoreLanguageProcessorInterface.ts} | 10 ++++++---- .../engine/main/src/headless/languageProcessor.ts | 12 ++++++------ 6 files changed, 23 insertions(+), 21 deletions(-) rename web/src/engine/keyboard/src/{textStoreTranscriptionInterface.ts => textStoreLanguageProcessorInterface.ts} (50%) diff --git a/web/src/engine/interfaces/src/prediction/languageProcessor.interface.ts b/web/src/engine/interfaces/src/prediction/languageProcessor.interface.ts index eef934495a..08ad259831 100644 --- a/web/src/engine/interfaces/src/prediction/languageProcessor.interface.ts +++ b/web/src/engine/interfaces/src/prediction/languageProcessor.interface.ts @@ -1,6 +1,6 @@ import { LexicalModelTypes } from '@keymanapp/common-types'; import { EventEmitter } from "eventemitter3"; -import { TextStore, TextStoreTranscriptionInterface } from "keyman/engine/keyboard"; +import { TextStoreLanguageProcessorInterface } from "keyman/engine/keyboard"; export class ReadySuggestions { suggestions: LexicalModelTypes.Suggestion[]; @@ -48,7 +48,7 @@ export interface LanguageProcessorEventMap { * @param textStore The `TextStore` representation of the context the suggestion was applied to. * @returns */ - 'suggestionapplied': (textStore: TextStoreTranscriptionInterface) => boolean + 'suggestionapplied': (textStore: TextStoreLanguageProcessorInterface) => boolean } @@ -56,7 +56,7 @@ export interface LanguageProcessorSpec extends EventEmitter; + invalidateContext(textStore: TextStoreLanguageProcessorInterface, layerId: string): Promise; /** * @@ -66,9 +66,9 @@ export interface LanguageProcessorSpec extends EventEmitter string): Promise; + applySuggestion(suggestion: LexicalModelTypes.Suggestion, textStore: TextStoreLanguageProcessorInterface, getLayerId: () => string): Promise; - applyReversion(reversion: LexicalModelTypes.Reversion, textStore: TextStore): Promise; + applyReversion(reversion: LexicalModelTypes.Reversion, textStore: TextStoreLanguageProcessorInterface): Promise; get wordbreaksAfterSuggestions(): boolean; diff --git a/web/src/engine/keyboard/src/index.ts b/web/src/engine/keyboard/src/index.ts index 15f97cbf8e..d4107ca384 100644 --- a/web/src/engine/keyboard/src/index.ts +++ b/web/src/engine/keyboard/src/index.ts @@ -33,7 +33,7 @@ export { type VariableStore, VariableStoreSerializer, VariableStoreDictionary } export { SyntheticTextStore } from "./syntheticTextStore.js"; export { TextStore } from "./textStore.js"; -export { TextStoreTranscriptionInterface } from "./textStoreTranscriptionInterface.js"; +export { TextStoreLanguageProcessorInterface } from "./textStoreLanguageProcessorInterface.js"; export { findCommonSubstringEndIndex } from "./stringDivergence.js"; export { Deadkey } from "./deadkeys.js"; diff --git a/web/src/engine/keyboard/src/syntheticTextStore.ts b/web/src/engine/keyboard/src/syntheticTextStore.ts index bf4f0815c8..2105f5ba7e 100644 --- a/web/src/engine/keyboard/src/syntheticTextStore.ts +++ b/web/src/engine/keyboard/src/syntheticTextStore.ts @@ -1,5 +1,5 @@ import { TextStore } from './textStore.js'; -import { TextStoreTranscriptionInterface } from './textStoreTranscriptionInterface.js'; +import { TextStoreLanguageProcessorInterface } from './textStoreLanguageProcessorInterface.js'; import { KMWString } from '@keymanapp/web-utils'; export class SyntheticTextStore extends TextStore { @@ -27,7 +27,7 @@ export class SyntheticTextStore extends TextStore { } // Clones the state of an existing EditableElement, creating a SyntheticTextStore version of its state. - static from(textStore: TextStoreTranscriptionInterface, readonly?: boolean): SyntheticTextStore { + static from(textStore: TextStoreLanguageProcessorInterface, readonly?: boolean): SyntheticTextStore { let clone: SyntheticTextStore; this.assertIsTextStore(textStore); diff --git a/web/src/engine/keyboard/src/textStore.ts b/web/src/engine/keyboard/src/textStore.ts index 55ec6bd774..95ed85cc27 100644 --- a/web/src/engine/keyboard/src/textStore.ts +++ b/web/src/engine/keyboard/src/textStore.ts @@ -6,7 +6,7 @@ import { SyntheticTextStore } from "./syntheticTextStore.js"; // Defines deadkey management in a manner attachable to each element interface. import { type KeyEvent } from './keyEvent.js'; -import { TextStoreTranscriptionInterface } from './textStoreTranscriptionInterface.js'; +import { TextStoreLanguageProcessorInterface } from './textStoreLanguageProcessorInterface.js'; import { Deadkey, DeadkeyTracker } from "./deadkeys.js"; import { LexicalModelTypes } from '@keymanapp/common-types'; @@ -60,7 +60,7 @@ export abstract class TextStore { this._dks = dks.clone(); } - static assertIsTextStore(textStore: TextStoreTranscriptionInterface): asserts textStore is TextStore { + static assertIsTextStore(textStore: TextStoreLanguageProcessorInterface): asserts textStore is TextStore { if (!(textStore instanceof TextStore)) { throw new TypeError("textStore is not a TextStore"); } @@ -75,7 +75,7 @@ export abstract class TextStore { * As such, it assumes that the caret is immediately after any inserted text. * @param from An output target (preferably a SyntheticTextStore) representing the prior state of the input/output system. */ - buildTransformFrom(original: TextStoreTranscriptionInterface): TextTransform { + buildTransformFrom(original: TextStoreLanguageProcessorInterface): TextTransform { TextStore.assertIsTextStore(original); const toLeft = this.getTextBeforeCaret(); diff --git a/web/src/engine/keyboard/src/textStoreTranscriptionInterface.ts b/web/src/engine/keyboard/src/textStoreLanguageProcessorInterface.ts similarity index 50% rename from web/src/engine/keyboard/src/textStoreTranscriptionInterface.ts rename to web/src/engine/keyboard/src/textStoreLanguageProcessorInterface.ts index 259318adcf..405c5496a9 100644 --- a/web/src/engine/keyboard/src/textStoreTranscriptionInterface.ts +++ b/web/src/engine/keyboard/src/textStoreLanguageProcessorInterface.ts @@ -7,9 +7,11 @@ import { KeyEvent } from './keyEvent.js'; import { Transcription } from './keyboards/transcription.js'; import { Alternate } from './keyboards/textTransform.js'; -// Interface with the methods LanguageProcessor needs from TextStore -// for transcription building and applying transforms. -export interface TextStoreTranscriptionInterface { - buildTranscriptionFrom(original: TextStoreTranscriptionInterface, keyEvent: KeyEvent, readonly: boolean, alternates?: Alternate[]): Transcription; +/** + * Interface with the methods LanguageProcessor needs from TextStore + * for transcription building and applying transforms. + */ +export interface TextStoreLanguageProcessorInterface { + buildTranscriptionFrom(original: TextStoreLanguageProcessorInterface, keyEvent: KeyEvent, readonly: boolean, alternates?: Alternate[]): Transcription; apply(transform: LexicalModelTypes.Transform): void; } diff --git a/web/src/engine/main/src/headless/languageProcessor.ts b/web/src/engine/main/src/headless/languageProcessor.ts index 7ded8c1cf7..7ac7f1c886 100644 --- a/web/src/engine/main/src/headless/languageProcessor.ts +++ b/web/src/engine/main/src/headless/languageProcessor.ts @@ -1,6 +1,6 @@ import { EventEmitter } from "eventemitter3"; import { LMLayer, WorkerFactory } from "@keymanapp/lexical-model-layer/web"; -import { Transcription, TextStoreTranscriptionInterface, SyntheticTextStore } from 'keyman/engine/keyboard'; +import { Transcription, TextStoreLanguageProcessorInterface, SyntheticTextStore } from 'keyman/engine/keyboard'; import { LanguageProcessorEventMap, ModelSpec, StateChangeEnum, ReadySuggestions } from 'keyman/engine/interfaces'; import ContextWindow from "./contextWindow.js"; import { TranscriptionCache } from "./transcriptionCache.js"; @@ -125,7 +125,7 @@ export class LanguageProcessor extends EventEmitter { }); } - public invalidateContext(textStore: TextStoreTranscriptionInterface, layerId: string): Promise { + public invalidateContext(textStore: TextStoreLanguageProcessorInterface, layerId: string): Promise { // If there's no active model, there can be no predictions. // We'll also be missing important data needed to even properly REQUEST the predictions. if(!this.currentModel || !this.configuration) { @@ -153,7 +153,7 @@ export class LanguageProcessor extends EventEmitter { } } - public wordbreak(textStore: TextStoreTranscriptionInterface, layerId: string): Promise { + public wordbreak(textStore: TextStoreLanguageProcessorInterface, layerId: string): Promise { if(!this.isActive) { return null; } @@ -188,7 +188,7 @@ export class LanguageProcessor extends EventEmitter { * required because layerid can be changed by PostKeystroke * @returns */ - public applySuggestion(suggestion: Suggestion, textStore: TextStoreTranscriptionInterface, getLayerId: ()=>string): Promise { + public applySuggestion(suggestion: Suggestion, textStore: TextStoreLanguageProcessorInterface, getLayerId: ()=>string): Promise { if(!textStore) { throw "Accepting suggestions requires a destination TextStore instance." } @@ -263,7 +263,7 @@ export class LanguageProcessor extends EventEmitter { } } - public applyReversion(reversion: Reversion, textStore: TextStoreTranscriptionInterface) { + public applyReversion(reversion: Reversion, textStore: TextStoreLanguageProcessorInterface) { if(!textStore) { throw "Accepting suggestions requires a destination TextStore instance." } @@ -305,7 +305,7 @@ export class LanguageProcessor extends EventEmitter { return promise; } - public predictFromTarget(textStore: TextStoreTranscriptionInterface, layerId: string): Promise { + public predictFromTarget(textStore: TextStoreLanguageProcessorInterface, layerId: string): Promise { if(!this.isActive || !textStore) { return null; }