Merge branch 'epic/web-core' into refactor/web/defaultRules

This commit is contained in:
Eberhard Beilharz 2026-01-19 16:21:22 +01:00 • committed by GitHub
commit aeb8cbe39a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 194 additions and 15 deletions

View file

@ -290,7 +290,8 @@ export class CoreKeyboardProcessor extends EventEmitter<EventMap> implements Key
/**
* Processes post-keystroke actions for the given device and text store.
* Handles any actions that should occur after a keystroke is processed.
* Handles any actions that should occur after a keystroke is processed
* or after applying suggestions.
*
* @param {DeviceSpec} device The device specification.
* @param {TextStore} textStore The current text store context.
@ -298,7 +299,10 @@ export class CoreKeyboardProcessor extends EventEmitter<EventMap> implements Key
* @returns {ProcessorAction} The resulting processor action, or null if not applicable.
*/
public processPostKeystroke(device: DeviceSpec, textStore: TextStore): ProcessorAction {
// TODO-web-core: Implement this method (#15286)
// TODO-embed-osk-in-kmx: Implement this method (#15286)
// This gets called after processing a keystroke to process the PostKeystroke group
// (irrelevant for web-core since that is handled in Core), but also after
// applying a suggestion, which we do need to handle.
return null;
}
@ -343,7 +347,7 @@ export class CoreKeyboardProcessor extends EventEmitter<EventMap> implements Key
* @returns {boolean} True if the keyboard layer changed, false otherwise.
*/
public selectLayer(keyEvent: KeyEvent): boolean {
// TODO-web-core: Implement this method (#15284)
// TODO-embed-osk-in-kmx: Implement this method (#15284)
return false;
}

View file

@ -34,7 +34,7 @@ export class KMXKeyboard {
},{
scope: KM_CORE_OPTION_SCOPE.OPT_ENVIRONMENT,
key: KM_CORE_KMX_ENV.SIMULATEALTGR,
value: "0" // TODO: We won't support simulating AltGr option in v19
value: "0" // TODO: We won't support simulating AltGr option in v19 - see also emulatesAltGr
}
]
const result = KM_Core.instance.state_create(_keyboard, environment_opts);
@ -55,7 +55,7 @@ export class KMXKeyboard {
}
public constructKeyEvent(key: ActiveKey | ActiveSubKey, device: DeviceSpec, stateKeys: StateKeyMap): KeyEvent {
// TODO-web-core: Implement this method (#15290)
// TODO-embed-osk-in-kmx: Implement this method (#15290)
return null;
}
@ -86,7 +86,8 @@ export class KMXKeyboard {
}
public get isChiral(): boolean {
// TODO-web-core: Implement this method (#15290)
// TODO-embed-osk-in-kmx: Implement this method (#15290)
// Only relevant for OSK
return false;
}
@ -95,7 +96,7 @@ export class KMXKeyboard {
* @return {boolean}
*/
public get emulatesAltGr(): boolean {
// TODO-web-core: Implement this method (#15290)
// TODO: We won't support simulating AltGr option in v19 - see also c'tor
return false;
}
@ -108,8 +109,8 @@ export class KMXKeyboard {
* @param {number} data 1 for KeyDown or FocusReceived,
* 0 for KeyUp or FocusLost
*/
public notify(eventCode: NotifyEventCode, textStore: TextStore, data: number): void { // I2187
// TODO-web-core: do we need to support this? (#15290)
public notify(eventCode: NotifyEventCode, textStore: TextStore, data: number): void {
// TODO: implement for IMX (cf #2239 and #7928)
}
}

View file

@ -13,4 +13,5 @@ export type VariableStore = { [name: string]: string; };
export interface VariableStoreSerializer {
loadStore(keyboardID: string, storeName: string): VariableStore;
saveStore(keyboardID: string, storeName: string, storeMap: VariableStore): void;
findStores(keyboardID: string): VariableStore[];
}

View file

@ -9,27 +9,51 @@ import { CookieSerializer } from "keyman/engine/dom-utils";
// dynamic property names; they'd have to be known at compile time to facilitate
// strict type checking.
class VarStoreSerializer extends CookieSerializer<VariableStore> {
constructor(keyboardID: string, storeName: string) {
public constructor(keyboardID: string, storeName: string) {
super(`KeymanWeb_${keyboardID}_Option_${storeName}`);
}
load() {
public load(): VariableStore {
return super.load(decodeURIComponent);
}
save(storeMap: VariableStore) {
public save(storeMap: VariableStore) {
super.save(storeMap, encodeURIComponent);
}
/**
* Find all variable stores associated with a given keyboard.
*
* @param {string} keyboardID The keyboard ID whose variable stores are to be found.
*
* @returns An array of VariableStore objects found for the keyboard.
*/
public static findStores(keyboardID: string): VariableStore[] {
const pattern = new RegExp(`^KeymanWeb_${keyboardID}_Option_`);
const matching = CookieSerializer.loadAllMatching<VariableStore>(pattern, decodeURIComponent);
return matching.map(m => m.value);
}
}
export class VariableStoreCookieSerializer implements VariableStoreSerializer {
loadStore(keyboardID: string, storeName: string): VariableStore {
public loadStore(keyboardID: string, storeName: string): VariableStore {
const storeCookieSerializer = new VarStoreSerializer(keyboardID, storeName);
return storeCookieSerializer.load();
}
saveStore(keyboardID: string, storeName: string, storeMap: VariableStore) {
public saveStore(keyboardID: string, storeName: string, storeMap: VariableStore) {
const storeCookieSerializer = new VarStoreSerializer(keyboardID, storeName);
storeCookieSerializer.save(storeMap);
}
/**
* Find all variable stores associated with a given keyboard.
*
* @param {string} keyboardID The keyboard ID whose variable stores are to be found.
*
* @returns An array of VariableStore objects found for the keyboard.
*/
public findStores(keyboardID: string): VariableStore[] {
return VarStoreSerializer.findStores(keyboardID);
}
}

View file

@ -0,0 +1,144 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
import { assert } from 'chai';
import { VariableStore } from 'keyman/engine/keyboard';
import { VariableStoreCookieSerializer } from 'keyman/engine/main';
describe('VariableStoreCookieSerializer', () => {
describe('saveStore and loadStore', () => {
const keyboardID = 'test-keyboard';
const storeName = 'testStore';
const cookieName = `KeymanWeb_${keyboardID}_Option_${storeName}`;
afterEach(async () => {
document.cookie = `${cookieName}=; max-age=0`;
});
it('should save and load a simple store with string values', () => {
// Arrange
const serializer = new VariableStoreCookieSerializer();
const storeData: VariableStore = {
option1: 'value1',
option2: 'value2',
option3: 'value3'
};
// Act
serializer.saveStore(keyboardID, storeName, storeData);
const loaded = serializer.loadStore(keyboardID, storeName);
// Assert
assert.deepEqual(loaded, storeData, 'loaded store should match saved store');
});
it('should save and load a store with empty values', () => {
// Arrange
const serializer = new VariableStoreCookieSerializer();
const storeData: VariableStore = {
option1: '',
option2: 'value',
option3: ''
};
// Act
serializer.saveStore(keyboardID, storeName, storeData);
const loaded = serializer.loadStore(keyboardID, storeName);
// Assert
assert.deepEqual(loaded, storeData, 'empty string values should be preserved');
});
it('should save and load a store with special characters', () => {
// Arrange
const serializer = new VariableStoreCookieSerializer();
const storeData: VariableStore = {
encoded: 'value:with;special&chars=',
emoji: '⭐'
};
// Act
serializer.saveStore(keyboardID, storeName, storeData);
const loaded = serializer.loadStore(keyboardID, storeName);
// Assert
assert.deepEqual(loaded, storeData, 'special characters should be properly encoded/decoded');
});
it('should handle multiple stores for the same keyboard independently', () => {
// Arrange
const serializer = new VariableStoreCookieSerializer();
const store1Data: VariableStore = { option: 'store1value' };
const store2Data: VariableStore = { option: 'store2value' };
// Act
serializer.saveStore(keyboardID, 'store1', store1Data);
serializer.saveStore(keyboardID, 'store2', store2Data);
const loaded1 = serializer.loadStore(keyboardID, 'store1');
const loaded2 = serializer.loadStore(keyboardID, 'store2');
// Assert
assert.deepEqual(loaded1, store1Data, 'first store should be independent');
assert.deepEqual(loaded2, store2Data, 'second store should be independent');
// Cleanup
document.cookie = `KeymanWeb_${keyboardID}_Option_store1=; max-age=0`;
document.cookie = `KeymanWeb_${keyboardID}_Option_store2=; max-age=0`;
});
it('should return empty object for non-existent store', () => {
// Arrange
const serializer = new VariableStoreCookieSerializer();
// Act
const loaded = serializer.loadStore(keyboardID, 'nonexistent');
// Assert
assert.deepEqual(loaded, {}, 'non-existent store should return empty object');
});
});
describe('findStores', () => {
it('should return an empty array when no stores exist for a keyboardID', () => {
// Arrange
const serializer = new VariableStoreCookieSerializer();
const keyboardID = 'test-keyboard';
const expected: VariableStore[] = [];
// Act
const result = serializer.findStores(keyboardID);
// Assert
assert.deepEqual(result, expected, 'result should be an empty array');
assert.isTrue(Array.isArray(result), 'result should be an array');
assert.strictEqual(result.length, 0, 'result array length should be 0');
});
it('should return store for keyboard', () => {
// Arrange
const serializer = new VariableStoreCookieSerializer();
const storeData1: VariableStore = {
option1: 'value1',
option2: 'value2',
option3: 'value3'
};
serializer.saveStore('test-keyboard', 'storeName', storeData1);
const storeData2: VariableStore = {
settingA: 'A',
settingB: 'B'
};
serializer.saveStore('another-keyboard', 'anotherStore', storeData2);
// Act
const stores = serializer.findStores('test-keyboard');
// Assert
assert.deepEqual(stores, [storeData1], 'stores should match saved store');
// Cleanup
document.cookie = `KeymanWeb_test-keyboard_Option_storeName=; max-age=0`;
});
});
});

View file

@ -84,11 +84,16 @@ export default {
// Relative, from the containing package.json
files: ['web/build/test/dom/cases/keyboard-storage/**/*.tests.mjs']
},
{
name: 'engine/main',
// Relative, from the containing package.json
files: ['web/build/test/dom/cases/main/**/*.tests.mjs']
},
{
name: 'engine/osk',
// Relative, from the containing package.json
files: ['web/build/test/dom/cases/osk/**/*.tests.mjs']
}
},
],
middleware: [
// Rewrites short-hand paths for test resources, making them fully relative to the repo root.