diff --git a/developer/src/kmc-keyboard/src/compiler/key2.ts b/developer/src/kmc-keyboard/src/compiler/key2.ts index 824c2e848f..b48042d796 100644 --- a/developer/src/kmc-keyboard/src/compiler/key2.ts +++ b/developer/src/kmc-keyboard/src/compiler/key2.ts @@ -6,6 +6,7 @@ import GlobalSections = KMXPlus.GlobalSections; import Key2 = KMXPlus.Key2; import ListItem = KMXPlus.ListItem; import Key2Flicks = KMXPlus.Key2Flicks; +import { allUsedKeyIdsInLayers, calculateUniqueKeys } from '../util/util.js'; export class Key2Compiler extends SectionCompiler { @@ -20,28 +21,6 @@ export class Key2Compiler extends SectionCompiler { return valid; } - /** - * Return a set of all used keys - * public for test - */ - public allUsedKeyIds(): Set { - const s = new Set(); - if (this.keyboard?.layers) { - for (const layers of this.keyboard.layers || []) { - for (const layer of layers.layer || []) { - for (const row of layer.row || []) { - if (row.keys) { - for (const k of row.keys.split(" ")) { - s.add(k); - } - } - } - } - } - } - return s; - } - public compile(sections: GlobalSections): Key2 { if (!this.keyboard?.keys?.key && !this.keyboard?.keys?.flicks) { // short-circuit if no keys or flicks @@ -81,20 +60,8 @@ export class Key2Compiler extends SectionCompiler { } public loadKeys(sections: GlobalSections, sect: Key2) { - const usedKeys = this.allUsedKeyIds(); - - // TODO-LDML factor common code - // Need 'newer' (later) keys to override older ones. - const reverseKeys = [...this.keyboard.keys.key].reverse(); // newest to oldest - const alreadySeen = new Set(); - // filter out only the keys that haven't already been seen - const uniqueKeys = reverseKeys.filter(({id}) => { - if (!alreadySeen.has(id)) { - alreadySeen.add(id); - return true; - } - return false; - }); + const usedKeys = allUsedKeyIdsInLayers(this.keyboard?.layers); + const uniqueKeys = calculateUniqueKeys([...this.keyboard.keys?.key]); for (let key of uniqueKeys) { if (!usedKeys.has(key.id)) { diff --git a/developer/src/kmc-keyboard/src/compiler/keys.ts b/developer/src/kmc-keyboard/src/compiler/keys.ts index b50b42521e..f6856b9372 100644 --- a/developer/src/kmc-keyboard/src/compiler/keys.ts +++ b/developer/src/kmc-keyboard/src/compiler/keys.ts @@ -6,6 +6,7 @@ import { SectionCompiler } from "./section-compiler.js"; import GlobalSections = KMXPlus.GlobalSections; import Keys = KMXPlus.Keys; import USVirtualKeyMap = Constants.USVirtualKeyMap; +import { calculateUniqueKeys } from '../util/util.js'; export class KeysCompiler extends SectionCompiler { @@ -14,19 +15,7 @@ export class KeysCompiler extends SectionCompiler { } private validateHardwareLayer(layer: LDMLKeyboard.LKLayer) { - // TODO-LDML factor common code - // Need 'newer' (later) keys to override older ones. - const reverseKeys = [...this.keyboard.keys?.key].reverse(); // newest to oldest - const alreadySeen = new Set(); - // filter out only the keys that haven't already been seen - const uniqueKeys = reverseKeys.filter(({id}) => { - if (!alreadySeen.has(id)) { - alreadySeen.add(id); - return true; - } - return false; - }); - + const uniqueKeys = calculateUniqueKeys([...this.keyboard.keys?.key]); let valid = true; if(layer.row.length > USVirtualKeyMap.length) { this.callbacks.reportMessage(CompilerMessages.Error_HardwareLayerHasTooManyRows()); diff --git a/developer/src/kmc-keyboard/src/util/util.ts b/developer/src/kmc-keyboard/src/util/util.ts index d663fc490b..3ead0e7dfc 100644 --- a/developer/src/kmc-keyboard/src/util/util.ts +++ b/developer/src/kmc-keyboard/src/util/util.ts @@ -1,3 +1,4 @@ +import { LKKey, LKLayers } from "@keymanapp/common-types/src/ldml-keyboard/ldml-keyboard-xml"; /** * Verifies that value is an item in the enumeration. @@ -5,3 +6,51 @@ export function isValidEnumValue(enu: T, value: string) { return (Object.values(enu) as string[]).includes(value); } + +/** + * Returns unique LKKeys only, preserving later ones + * in case of conflict. (i.e. later overrides) + * @param keys list of keys to consider. (mutated) + * @returns Array of unique keys. Order is not specified. + */ +export function calculateUniqueKeys(keys?: LKKey[]): LKKey[] { + if (!keys) { + return []; + } + // Need 'newer' (later) keys to override older ones. + const reverseKeys = keys.reverse(); // newest to oldest + const alreadySeen = new Set(); + // filter out only the keys that haven't already been seen + const uniqueKeys = reverseKeys.filter(({ id }) => { + if (!alreadySeen.has(id)) { + alreadySeen.add(id); + return true; + } + return false; + }); + + return uniqueKeys; +} + +/** + * + * @param layersList list of layers elements, from `keyboard?.layers` + * @returns set of key IDs + */ +export function allUsedKeyIdsInLayers(layersList : LKLayers[] | null): Set { + const s = new Set(); + if (layersList) { + for (const layers of layersList || []) { + for (const layer of layers.layer || []) { + for (const row of layer.row || []) { + if (row.keys) { + for (const k of row.keys.split(" ")) { + s.add(k); + } + } + } + } + } + } + return s; +} diff --git a/developer/src/kmc-keyboard/test/test-base.ts b/developer/src/kmc-keyboard/test/test-base.ts deleted file mode 100644 index 7ff2b9cdb7..0000000000 --- a/developer/src/kmc-keyboard/test/test-base.ts +++ /dev/null @@ -1,9 +0,0 @@ -import 'mocha'; -import {assert} from 'chai'; - -describe('null-test', function () { - it('should-pass-null-test', function() { - assert.isTrue(true); - }); -}); - diff --git a/developer/src/kmc-keyboard/test/test-utils.ts b/developer/src/kmc-keyboard/test/test-utils.ts new file mode 100644 index 0000000000..8d04bb4dfa --- /dev/null +++ b/developer/src/kmc-keyboard/test/test-utils.ts @@ -0,0 +1,99 @@ +import 'mocha'; +import {assert} from 'chai'; +import { isValidEnumValue, calculateUniqueKeys, allUsedKeyIdsInLayers } from '../src/util/util.js'; + +describe('test of util/util.ts', () => { + describe('isValidEnumValue()', () => { + enum MyFruit { + apple, peach, pear, plum + }; + enum MyNumbers { + wieħed=1, tnejn=2, tlieta=3 + }; + enum MyLetters { + akka='h', magħtugha='ħ' + }; + + it('should handle a bare enum', ()=> { + assert.ok(isValidEnumValue(MyFruit, 'apple')); + assert.notOk(isValidEnumValue(MyFruit, 'banana')); + assert.notOk(isValidEnumValue(MyFruit, '')); + assert.notOk(isValidEnumValue(MyFruit, undefined)); + assert.notOk(isValidEnumValue(MyFruit, null)); + assert.notOk(isValidEnumValue(MyFruit, 'unknown')); + }); + + it('should handle a numerical enum', ()=> { + assert.ok(isValidEnumValue(MyNumbers, 'tlieta')); + assert.notOk(isValidEnumValue(MyNumbers, '3')); + assert.notOk(isValidEnumValue(MyNumbers, 'seventy-six')); + assert.notOk(isValidEnumValue(MyNumbers, '76')); + }); + + it('should handle a string enum', ()=> { + assert.ok(isValidEnumValue(MyLetters, 'h')); + assert.ok(isValidEnumValue(MyLetters, 'ħ')); + assert.notOk(isValidEnumValue(MyLetters, 'akka')); + assert.notOk(isValidEnumValue(MyLetters, 'z')); + assert.notOk(isValidEnumValue(MyLetters, 'zed')); + }); + }); + + describe('calculateUniqueKeys()', () => { + it('should handle some null cases', () => { + assert.sameDeepMembers(calculateUniqueKeys(null), + []); + assert.sameDeepMembers(calculateUniqueKeys([]), + []); + }); + it('should handle some real cases', () => { + assert.sameDeepMembers(calculateUniqueKeys([ + { id: 'a', to: 'a' }, + { id: 'a', to: 'a' }, // dup + { id: 'b', to: 'b' }, + { id: 'a', to: 'å' }, // override + ]),[ + { id: 'b', to: 'b' }, + { id: 'a', to: 'å' }, + ]); + }); + }); + + describe('allUsedKeyIdsInLayers()', () => { + it('should handle a null case', () => { + assert.sameDeepMembers(Array.from(allUsedKeyIdsInLayers(null).values()), + []); + }); + it('should handle a real case', () => { + assert.sameDeepMembers(Array.from(allUsedKeyIdsInLayers([ + { + layer: [ + { + row: [ + { keys: 'q w e r t y' }, + { keys: 'a s d f' }, + ] + }, + { + row: [ + { keys: 'Q W E R T Y' }, + { keys: 'A S D F' }, + ] + } + ] + }, + { + layer: [ + { + row: [ + { keys: 'q w e r t y' }, + { keys: '0 1 2 3' } + ] + }, + ] + } + ]).values()), + 'q w e r t y Q W E R T Y a s d f A S D F 0 1 2 3'.split(' ')); + }); + }); +});