From ce5a5bb2f1abb4e40f9ef142ce3a9683c131a676 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 27 Nov 2025 11:42:08 +0100 Subject: [PATCH] refactor(developer): move KVK embed into embed-osk-kvk.ts --- .../src/compiler/embed-osk/embed-osk-kvk.ts | 158 +++++++++ .../src/compiler/embed-osk/embed-osk.ts | 204 +++--------- .../src/kmc-kmn/test/embed-osk-kvk.tests.ts | 315 ++++++++++++++++++ developer/src/kmc-kmn/test/embed-osk.tests.ts | 295 +--------------- 4 files changed, 513 insertions(+), 459 deletions(-) create mode 100644 developer/src/kmc-kmn/src/compiler/embed-osk/embed-osk-kvk.ts create mode 100644 developer/src/kmc-kmn/test/embed-osk-kvk.tests.ts diff --git a/developer/src/kmc-kmn/src/compiler/embed-osk/embed-osk-kvk.ts b/developer/src/kmc-kmn/src/compiler/embed-osk/embed-osk-kvk.ts new file mode 100644 index 0000000000..6b1d51d3bd --- /dev/null +++ b/developer/src/kmc-kmn/src/compiler/embed-osk/embed-osk-kvk.ts @@ -0,0 +1,158 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by mcdurdin on 2025-11-27 + * + * Convert Keyman .kvks files to KMX+ format. + */ +import { KMXPlus, VisualKeyboard, translateLdmlModifiersToVisualKeyboardShift, visualKeyboardShiftToLayerName, ModifierKeyConstant, usVirtualKeyName, translateVisualKeyboardShiftToLdmlModifiers } from "@keymanapp/common-types"; +import { CompilerCallbacks, oskFontMagicToken } from "@keymanapp/developer-utils"; +import { KmnCompilerMessages } from "../kmn-compiler-messages.js"; +import { oskLayouts } from "./osk-layout.js"; + +type VirtualKey = number; +type LayerBag = Map; + +export class EmbedOskKvkInKmx { + + constructor(private callbacks: CompilerCallbacks) { + } + + /** + * Transform a .kvk file to the KMX+ format + */ + public transformVisualKeyboardToKmxPlus(kmx: KMXPlus.KMXPlusFile, vk: VisualKeyboard.VisualKeyboard): void { + + // TODO-EMBED-OSK-IN-KMX: if(displayMap) { + // // Remap using the osk-char-use-rewriter + // Osk.remapVisualKeyboard(vk, displayMap); + // } + + const layerBags = this.buildLayerBags(vk, kmx.kmxplus.strs, kmx.kmxplus.keys); + const form = this.buildForm(vk, layerBags, kmx.kmxplus.strs); + kmx.kmxplus.layr.forms.push(form); + + // For now, we only support dotted circle (U+25CC) as our base character + kmx.kmxplus.disp.baseCharacter = kmx.kmxplus.strs.allocString('\u25cc'); + } + + /** + * Build the layout of keys, with gaps for missing keys, for the given form -- + * either ANSI (US) or ISO (EU), which are the only two supported layouts in + * .kvk + */ + private buildForm(vk: VisualKeyboard.VisualKeyboard, layerBags: Map, strs: KMXPlus.Strs) { + const baseLayoutName = 'en-us'; // This is the only value we support for 19.0 + const formName: KMXPlus.LayrFormHardware = + vk.header.flags & VisualKeyboard.VisualKeyboardHeaderFlags.kvkh102 + ? KMXPlus.LayrFormHardware.iso + : KMXPlus.LayrFormHardware.us; + + const form = new KMXPlus.LayrForm(); + + form.baseLayout = strs.allocString(baseLayoutName); + form.flags = 0; + + if(vk.header.flags & VisualKeyboard.VisualKeyboardHeaderFlags.kvkhDisplayUnderlying) { + form.flags |= KMXPlus.LayrFormFlags.showBaseLayout; + } + + if(vk.header.flags & VisualKeyboard.VisualKeyboardHeaderFlags.kvkhAltGr) { + form.flags |= KMXPlus.LayrFormFlags.chiralSeparate; + } + + // We will reserve space for the font facename to be rewritten, with a magic + // token that the package compiler will search for; see kmp-compiler.ts. + form.fontFaceName = strs.allocString(oskFontMagicToken); + + // We only currently support 100% font size + form.fontSizePct = 100; + form.hardware = strs.allocString(formName); + + // For hardware-style keyboards, device width is not relevant + form.minDeviceWidth = 0; + + layerBags.forEach((keys, modifier) => { + const layr = new KMXPlus.LayrEntry(); + + // layr.id is not relevant for hardware keyboards, but we include it to + // make it easier to debug. We can use the existing KVK shift string + // generation, because we can only have KVK modifiers here, even though + // the LDML modifiers spec supports other modifiers + const vkShift = translateLdmlModifiersToVisualKeyboardShift(modifier); + layr.id = strs.allocString(visualKeyboardShiftToLayerName(vkShift)); + layr.mod = modifier; + + // fill the rows + + for(const row of oskLayouts[formName]) { + const layrRow = new KMXPlus.LayrRow(); + layr.rows.push(layrRow); + for(const vk of row) { + const key = keys.get(vk); + layrRow.keys.push(strs.allocString(key?.id?.value ?? 'gap')); + } + } + + form.layers.push(layr); + }); + + return form; + } + + /** + * Collect all the relevant keys from the visual keyboard, add them to the key + * bag, and build a set of key bags, one for each layer in the visual + * keyboard. + */ + private buildLayerBags(vk: VisualKeyboard.VisualKeyboard, strs: KMXPlus.Strs, keys: KMXPlus.Keys) { + const layerBags = new Map(); + + let hasHintedAboutNonUnicode = false; + + for (const key of vk.keys) { + const keyId = visualKeyboardShiftToLayerName(key.shift) + '-' + (usVirtualKeyName(key.vkey) ?? ('Unknown_'+key.vkey.toString())); + + if(!(key.flags & VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode)) { + if(!hasHintedAboutNonUnicode) { + this.callbacks.reportMessage(KmnCompilerMessages.Hint_EmbeddedOskDoesNotSupportNonUnicode({keyId})); + hasHintedAboutNonUnicode = true; + } + continue; + } + + if(key.flags & VisualKeyboard.VisualKeyboardKeyFlags.kvkkBitmap) { + this.callbacks.reportMessage(KmnCompilerMessages.Warn_EmbeddedOskDoesNotSupportBitmaps({keyId})); + continue; + } + + const keykey: KMXPlus.KeysKeys = { + id: strs.allocString(keyId), + to: strs.allocString(key.text), + flags: 0, // available flags are: gap, extend; neither needed + flicks: "", + longPress: null, + longPressDefault: strs.allocString(), + multiTap: null, + switch: strs.allocString(), + width: 100, + }; + + const mod = translateVisualKeyboardShiftToLdmlModifiers(key.shift); + if (!layerBags.has(mod)) { + const bag = new Map(); + layerBags.set(mod, bag); + } + + layerBags.get(mod).set(key.vkey, keykey); + keys.keys.push(keykey); + } + return layerBags; + } + + public readonly unitTestEndpoints = { + transformVisualKeyboardToKmxPlus: this.transformVisualKeyboardToKmxPlus.bind(this), + buildForm: this.buildForm.bind(this), + buildLayerBags: this.buildLayerBags.bind(this), + }; +} \ No newline at end of file diff --git a/developer/src/kmc-kmn/src/compiler/embed-osk/embed-osk.ts b/developer/src/kmc-kmn/src/compiler/embed-osk/embed-osk.ts index 279ab1ea10..03933547c4 100644 --- a/developer/src/kmc-kmn/src/compiler/embed-osk/embed-osk.ts +++ b/developer/src/kmc-kmn/src/compiler/embed-osk/embed-osk.ts @@ -6,16 +6,14 @@ * Convert Keyman .kvks and .keyman-touch-layout files to KMX+ format and embed * in .kmx. */ -import { KMX, KMXPlus, ModifierKeyConstant, translateLdmlModifiersToVisualKeyboardShift, translateVisualKeyboardShiftToLdmlModifiers, usVirtualKeyName, VisualKeyboard, visualKeyboardShiftToLayerName } from "@keymanapp/common-types"; -import { CompilerCallbacks, KMXPlusBuilder, oskFontMagicToken } from "@keymanapp/developer-utils"; +import { KMX, KMXPlus } from "@keymanapp/common-types"; +import { CompilerCallbacks, KMXPlusBuilder } from "@keymanapp/developer-utils"; import { KMXPlusVersion } from "@keymanapp/ldml-keyboard-constants"; import { KmnCompilerOptions } from "../compiler.js"; import { PuaMap, loadKvkFile } from "../osk.js"; -import { oskLayouts } from "./osk-layout.js"; -import { KmnCompilerMessages } from "../kmn-compiler-messages.js"; +import { EmbedOskKvkInKmx } from "./embed-osk-kvk.js"; -type VirtualKey = number; -type LayerBag = Map; +// import { EmbedOskTouchLayoutInKmx } from "./embed-osk-touch-layout.js"; export class EmbedOskInKmx { constructor( @@ -24,6 +22,29 @@ export class EmbedOskInKmx { ) { } + private createEmptyKmxPlusFile() { + // TODO-EMBED-OSK-IN-KMX: merge this default construction with LDML compiler + // start to write the ldml format + const kmx = new KMXPlus.KMXPlusFile(KMXPlusVersion.Version19); + const strs = kmx.kmxplus.strs = new KMXPlus.Strs(); + kmx.kmxplus.layr = new KMXPlus.Layr(); + kmx.kmxplus.elem = new KMXPlus.Elem(kmx.kmxplus); + kmx.kmxplus.disp = new KMXPlus.Disp(); + kmx.kmxplus.keys = new KMXPlus.Keys(strs); + // list? + kmx.kmxplus.loca = new KMXPlus.Loca(); + kmx.kmxplus.meta = new KMXPlus.Meta(); + kmx.kmxplus.meta.author = strs.allocString(); + kmx.kmxplus.meta.conform = strs.allocString(); + kmx.kmxplus.meta.indicator = strs.allocString(); + kmx.kmxplus.meta.layout = strs.allocString(); + kmx.kmxplus.meta.name = strs.allocString(); + kmx.kmxplus.meta.settings = 0; + kmx.kmxplus.meta.version = strs.allocString(); + + return kmx; + } + /** * Take .kvks and .keyman-touch-layout files, merge them and build them as * KMX+ data, and embed into the provided KMX @@ -35,13 +56,19 @@ export class EmbedOskInKmx { * @returns */ public embed(kmx: Uint8Array, kvksFilename: string, touchLayoutFilename: string, displayMap: PuaMap) { - const vk = loadKvkFile(kvksFilename, this.callbacks); + const kmxPlus = this.createEmptyKmxPlusFile(); - const kmxPlus = this.transformVisualKeyboardToKmxPlus(vk); - if(!kmxPlus) { - return null; + if(kvksFilename) { + const embedKvk = new EmbedOskKvkInKmx(this.callbacks); + const vk = loadKvkFile(kvksFilename, this.callbacks); + if(!vk) { + // error will have been reported by loadKvkFile + return null; + } + embedKvk.transformVisualKeyboardToKmxPlus(kmxPlus, vk); } + // TODO-EMBED-OSK-IN-KMX: touch layout to ldml // TODO-EMBED-OSK-IN-KMX: display map remapping @@ -51,159 +78,6 @@ export class EmbedOskInKmx { return this.injectKmxPlusIntoKmxFile(kmx, data); } - /** - * Transform a .kvk file to the KMX+ format - */ - private transformVisualKeyboardToKmxPlus(vk: VisualKeyboard.VisualKeyboard): KMXPlus.KMXPlusFile { - - // TODO-EMBED-OSK-IN-KMX: if(displayMap) { - // // Remap using the osk-char-use-rewriter - // Osk.remapVisualKeyboard(vk, displayMap); - // } - - // TODO-EMBED-OSK-IN-KMX: merge this default construction with LDML compiler - // start to write the ldml format - const kmx = new KMXPlus.KMXPlusFile(KMXPlusVersion.Version19); - const strs = kmx.kmxplus.strs = new KMXPlus.Strs(); - const layr = kmx.kmxplus.layr = new KMXPlus.Layr(); - kmx.kmxplus.elem = new KMXPlus.Elem(kmx.kmxplus); - const disp = kmx.kmxplus.disp = new KMXPlus.Disp(); - const keys = kmx.kmxplus.keys = new KMXPlus.Keys(kmx.kmxplus.strs); - // list? - kmx.kmxplus.loca = new KMXPlus.Loca(); - kmx.kmxplus.meta = new KMXPlus.Meta(); - kmx.kmxplus.meta.author = strs.allocString(); - kmx.kmxplus.meta.conform = strs.allocString(); - kmx.kmxplus.meta.indicator = strs.allocString(); - kmx.kmxplus.meta.layout = strs.allocString(); - kmx.kmxplus.meta.name = strs.allocString(); - kmx.kmxplus.meta.settings = 0; - kmx.kmxplus.meta.version = strs.allocString(); - - const layerBags = this.buildLayerBags(vk, strs, keys); - const form = this.buildForm(vk, layerBags, strs); - layr.forms.push(form); - - // For now, we only support dotted circle (U+25CC) as our base character - disp.baseCharacter = strs.allocString('\u25cc'); - - return kmx; - } - - /** - * Build the layout of keys, with gaps for missing keys, for the given form -- - * either ANSI (US) or ISO (EU), which are the only two supported layouts in - * .kvk - */ - private buildForm(vk: VisualKeyboard.VisualKeyboard, layerBags: Map, strs: KMXPlus.Strs) { - const baseLayoutName = 'en-us'; // This is the only value we support for 19.0 - const formName: KMXPlus.LayrFormHardware = - vk.header.flags & VisualKeyboard.VisualKeyboardHeaderFlags.kvkh102 - ? KMXPlus.LayrFormHardware.iso - : KMXPlus.LayrFormHardware.us; - - const form = new KMXPlus.LayrForm(); - - form.baseLayout = strs.allocString(baseLayoutName); - form.flags = 0; - - if(vk.header.flags & VisualKeyboard.VisualKeyboardHeaderFlags.kvkhDisplayUnderlying) { - form.flags |= KMXPlus.LayrFormFlags.showBaseLayout; - } - - if(vk.header.flags & VisualKeyboard.VisualKeyboardHeaderFlags.kvkhAltGr) { - form.flags |= KMXPlus.LayrFormFlags.chiralSeparate; - } - - // We will reserve space for the font facename to be rewritten, with a magic - // token that the package compiler will search for; see kmp-compiler.ts. - form.fontFaceName = strs.allocString(oskFontMagicToken); - - // We only currently support 100% font size - form.fontSizePct = 100; - form.hardware = strs.allocString(formName); - - // For hardware-style keyboards, device width is not relevant - form.minDeviceWidth = 0; - - layerBags.forEach((keys, modifier) => { - const layr = new KMXPlus.LayrEntry(); - - // layr.id is not relevant for hardware keyboards, but we include it to - // make it easier to debug. We can use the existing KVK shift string - // generation, because we can only have KVK modifiers here, even though - // the LDML modifiers spec supports other modifiers - const vkShift = translateLdmlModifiersToVisualKeyboardShift(modifier); - layr.id = strs.allocString(visualKeyboardShiftToLayerName(vkShift)); - layr.mod = modifier; - - // fill the rows - - for(const row of oskLayouts[formName]) { - const layrRow = new KMXPlus.LayrRow(); - layr.rows.push(layrRow); - for(const vk of row) { - const key = keys.get(vk); - layrRow.keys.push(strs.allocString(key?.id?.value ?? 'gap')); - } - } - - form.layers.push(layr); - }); - - return form; - } - - /** - * Collect all the relevant keys from the visual keyboard, add them to the key - * bag, and build a set of key bags, one for each layer in the visual - * keyboard. - */ - private buildLayerBags(vk: VisualKeyboard.VisualKeyboard, strs: KMXPlus.Strs, keys: KMXPlus.Keys) { - const layerBags = new Map(); - - let hasHintedAboutNonUnicode = false; - - for (const key of vk.keys) { - const keyId = visualKeyboardShiftToLayerName(key.shift) + '-' + (usVirtualKeyName(key.vkey) ?? ('Unknown_'+key.vkey.toString())); - - if(!(key.flags & VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode)) { - if(!hasHintedAboutNonUnicode) { - this.callbacks.reportMessage(KmnCompilerMessages.Hint_EmbeddedOskDoesNotSupportNonUnicode({keyId})); - hasHintedAboutNonUnicode = true; - } - continue; - } - - if(key.flags & VisualKeyboard.VisualKeyboardKeyFlags.kvkkBitmap) { - this.callbacks.reportMessage(KmnCompilerMessages.Warn_EmbeddedOskDoesNotSupportBitmaps({keyId})); - continue; - } - - const keykey: KMXPlus.KeysKeys = { - id: strs.allocString(keyId), - to: strs.allocString(key.text), - flags: 0, // available flags are: gap, extend; neither needed - flicks: "", - longPress: null, - longPressDefault: strs.allocString(), - multiTap: null, - switch: strs.allocString(), - width: 100, - }; - - const mod = translateVisualKeyboardShiftToLdmlModifiers(key.shift); - if (!layerBags.has(mod)) { - const bag = new Map(); - layerBags.set(mod, bag); - } - - layerBags.get(mod).set(key.vkey, keykey); - keys.keys.push(keykey); - } - return layerBags; - } - /** * Takes an existing KMX file, which must have a version >= 19.0, and must * have space pre-allocated for the KMX+ header, and injects the prebuilt KMX+ @@ -263,10 +137,8 @@ export class EmbedOskInKmx { } public readonly unitTestEndpoints = { - transformVisualKeyboardToKmxPlus: this.transformVisualKeyboardToKmxPlus.bind(this), - buildForm: this.buildForm.bind(this), - buildLayerBags: this.buildLayerBags.bind(this), injectKmxPlusIntoKmxFile: this.injectKmxPlusIntoKmxFile.bind(this), + createEmptyKmxPlusFile: this.createEmptyKmxPlusFile.bind(this), }; }; diff --git a/developer/src/kmc-kmn/test/embed-osk-kvk.tests.ts b/developer/src/kmc-kmn/test/embed-osk-kvk.tests.ts new file mode 100644 index 0000000000..b748c9f4dd --- /dev/null +++ b/developer/src/kmc-kmn/test/embed-osk-kvk.tests.ts @@ -0,0 +1,315 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ +import 'mocha'; +import { assert } from 'chai'; +import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; +import { KMX, KMXPlus, ModifierKeyConstant, USVirtualKeyCodes, VisualKeyboard } from '@keymanapp/common-types'; +import { KMXPlusBuilder, oskFontMagicToken } from '@keymanapp/developer-utils'; +import { makePathToFixture } from './helpers/index.js'; +import { KmnCompilerMessages } from '../src/main.js'; +import { EmbedOskInKmx } from '../src/compiler/embed-osk/embed-osk.js'; +import { loadKvkFile } from '../src/compiler/osk.js'; +import { EmbedOskKvkInKmx } from '../src/compiler/embed-osk/embed-osk-kvk.js'; + +// VK header is not used in all functions, e.g. buildLayerBags, so this is a +// default header for those tests +const NullVisualKeyboardHeader: VisualKeyboard.VisualKeyboardHeader = { + flags: VisualKeyboard.VisualKeyboardHeaderFlags.kvkhNone, + ansiFont: null, + unicodeFont: null, +}; + +describe('Compiler OSK Embedding', function() { + + const callbacks = new TestCompilerCallbacks(); + + this.beforeEach(function() { + callbacks.clear(); + }); + + this.afterEach(function() { + if(this.currentTest.isFailed()) { + callbacks.printMessages(); + } + }); + + describe('EmbedOskKvkInKmx', function() { + const embedder = new EmbedOskKvkInKmx(callbacks); + + describe('EmbedOskKvkInKmx.buildLayerBags', function() { + it('should build a bag of layers from an in-memory .kvks structure', async function() { + const vk: VisualKeyboard.VisualKeyboard = { + header: NullVisualKeyboardHeader, + keys: [ + { + vkey: USVirtualKeyCodes.K_A, + text: 'a', + shift: 0, + flags: VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode, + }, + { + vkey: USVirtualKeyCodes.K_B, + text: 'B', + shift: VisualKeyboard.VisualKeyboardShiftState.KVKS_SHIFT, + flags: VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode, + }, + { + vkey: USVirtualKeyCodes.K_C, + text: 'Ctrl+Shift+C', + shift: VisualKeyboard.VisualKeyboardShiftState.KVKS_CTRL | VisualKeyboard.VisualKeyboardShiftState.KVKS_SHIFT, + flags: VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode, + }, + ] + }; + + const strs = new KMXPlus.Strs(); + const keys = new KMXPlus.Keys(strs); + const bag = embedder.unitTestEndpoints.buildLayerBags(vk, strs, keys); + + assert.lengthOf(strs.strings, 7); + assert.equal(strs.strings[0].value, ''); + assert.equal(strs.strings[1].value, 'default-K_A'); + assert.equal(strs.strings[2].value, 'a'); + assert.equal(strs.strings[3].value, 'shift-K_B'); + assert.equal(strs.strings[4].value, 'B'); + assert.equal(strs.strings[5].value, 'shift-ctrl-K_C'); + assert.equal(strs.strings[6].value, 'Ctrl+Shift+C'); + + assert.lengthOf(keys.flicks, 1); + assert.lengthOf(keys.flicks[0].flicks, 0); + assert.equal(keys.flicks[0].id, strs.strings[0]); + + assert.deepEqual(keys.keys, [ + { + flags: 0, + flicks: "", + id: strs.strings[1], + longPress: null, + longPressDefault: strs.strings[0], + multiTap: null, + switch: strs.strings[0], + to: strs.strings[2], + width: 100 + }, + { + flags: 0, + flicks: "", + id: strs.strings[3], + longPress: null, + longPressDefault: strs.strings[0], + multiTap: null, + switch: strs.strings[0], + to: strs.strings[4], + width: 100 + }, + { + flags: 0, + flicks: "", + id: strs.strings[5], + longPress: null, + longPressDefault: strs.strings[0], + multiTap: null, + switch: strs.strings[0], + to: strs.strings[6], + width: 100 + }, + ]); + + assert.isArray(keys.kmap); + assert.isEmpty(keys.kmap); + + // bag will be a map of maps; this test has three layers with an unmodified base layer K_A, a shift+K_B, and Ctrl+Shift+C + assert.isNotNull(bag); + assert.equal(bag.size, 3); + assert.isTrue(bag.has(0)); + assert.isTrue(bag.has(ModifierKeyConstant.K_SHIFTFLAG)); + assert.isTrue(bag.has(ModifierKeyConstant.K_SHIFTFLAG | ModifierKeyConstant.K_CTRLFLAG)); + + const defaultLayer = bag.get(0); + assert.equal(defaultLayer.size, 1); + + assert.isTrue(defaultLayer.has(USVirtualKeyCodes.K_A)); + const k_a = defaultLayer.get(USVirtualKeyCodes.K_A); + assert.equal(k_a.flags, 0); + assert.equal(k_a.flicks, ""); + assert.equal(k_a.id, strs.strings[1]); + assert.equal(k_a.longPress, null); + assert.equal(k_a.longPressDefault, strs.strings[0]); + assert.equal(k_a.multiTap, null); + assert.equal(k_a.switch, strs.strings[0]); + assert.equal(k_a.to, strs.strings[2]); + assert.equal(k_a.width, 100); + + const shiftLayer = bag.get(ModifierKeyConstant.K_SHIFTFLAG); + assert.equal(shiftLayer.size, 1); + + assert.isTrue(shiftLayer.has(USVirtualKeyCodes.K_B)); + const k_b = shiftLayer.get(USVirtualKeyCodes.K_B); + assert.equal(k_b.flags, 0); + assert.equal(k_b.flicks, ""); + assert.equal(k_b.id, strs.strings[3]); + assert.equal(k_b.longPress, null); + assert.equal(k_b.longPressDefault, strs.strings[0]); + assert.equal(k_b.multiTap, null); + assert.equal(k_b.switch, strs.strings[0]); + assert.equal(k_b.to, strs.strings[4]); + assert.equal(k_b.width, 100); + + const shiftCtrlLayer = bag.get(ModifierKeyConstant.K_SHIFTFLAG | ModifierKeyConstant.K_CTRLFLAG); + assert.equal(shiftCtrlLayer.size, 1); + + assert.isTrue(shiftCtrlLayer.has(USVirtualKeyCodes.K_C)); + const k_c = shiftCtrlLayer.get(USVirtualKeyCodes.K_C); + assert.equal(k_c.flags, 0); + assert.equal(k_c.flicks, ""); + assert.equal(k_c.id, strs.strings[5]); + assert.equal(k_c.longPress, null); + assert.equal(k_c.longPressDefault, strs.strings[0]); + assert.equal(k_c.multiTap, null); + assert.equal(k_c.switch, strs.strings[0]); + assert.equal(k_c.to, strs.strings[6]); + assert.equal(k_c.width, 100); + + }); + + it('should emit WARN_EmbeddedOskDoesNotSupportBitmaps if a key with kvkkBitmap flag is found', async function() { + const vk: VisualKeyboard.VisualKeyboard = { + header: NullVisualKeyboardHeader, + keys: [ + { + vkey: USVirtualKeyCodes.K_A, + shift: 0, + // kvkkUnicode required because otherwise the key is ignored as 'ansi' + flags: VisualKeyboard.VisualKeyboardKeyFlags.kvkkBitmap | VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode, + bitmap: new Uint8Array() + }, + ] + }; + + const strs = new KMXPlus.Strs(); + const keys = new KMXPlus.Keys(strs); + const bag = embedder.unitTestEndpoints.buildLayerBags(vk, strs, keys); + assert.isNotNull(bag); + assert.equal(bag.size, 0); + assert.isTrue(callbacks.hasMessage(KmnCompilerMessages.WARN_EmbeddedOskDoesNotSupportBitmaps)); + }); + + it('should emit HINT_EmbeddedOskDoesNotSupportNonUnicode if a key without kvkkUnicode flag is found', async function() { + const vk: VisualKeyboard.VisualKeyboard = { + header: NullVisualKeyboardHeader, + keys: [ + { + vkey: USVirtualKeyCodes.K_A, + shift: 0, + // !kvkkUnicode + flags: 0 as VisualKeyboard.VisualKeyboardKeyFlags, + }, + ] + }; + + const strs = new KMXPlus.Strs(); + const keys = new KMXPlus.Keys(strs); + const bag = embedder.unitTestEndpoints.buildLayerBags(vk, strs, keys); + assert.isNotNull(bag); + assert.equal(bag.size, 0); + assert.isTrue(callbacks.hasMessage(KmnCompilerMessages.HINT_EmbeddedOskDoesNotSupportNonUnicode)); + }); + }); + + describe('EmbedOskKvkInKmx.buildForm', function() { + it('should a layout of keys from a layer bag, from an in-memory .kvks structure', async function() { + const vk: VisualKeyboard.VisualKeyboard = { + header: NullVisualKeyboardHeader, + keys: [ + { vkey: USVirtualKeyCodes.K_A, text: 'a', shift: 0, flags: VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode, }, + { vkey: USVirtualKeyCodes.K_B, text: 'b', shift: 0, flags: VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode, }, + { vkey: USVirtualKeyCodes.K_C, text: 'c', shift: 0, flags: VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode, }, + ] + }; + + const strs = new KMXPlus.Strs(); + const keys = new KMXPlus.Keys(strs); + const layerBags = embedder.unitTestEndpoints.buildLayerBags(vk, strs, keys); + assert.isNotNull(layerBags); + const form = embedder.unitTestEndpoints.buildForm(vk, layerBags, strs); + assert.isNotNull(form); + assert.equal(form.baseLayout.value, 'en-us'); // For v19 + assert.equal(form.flags, 0); + assert.equal(form.fontFaceName.value, oskFontMagicToken); + assert.equal(form.fontSizePct, 100); + assert.equal(form.hardware.value, 'us'); + assert.equal(form.minDeviceWidth, 0); + + assert.lengthOf(form.layers, 1); + assert.equal(form.layers[0].id.value, 'default'); + assert.equal(form.layers[0].mod, 0); // no modifiers + assert.equal(form.layers[0].rows.length, 5); + + assert.equal(form.layers[0].rows[0].keys.length, 13); + assert.equal(form.layers[0].rows[1].keys.length, 13); + assert.equal(form.layers[0].rows[2].keys.length, 11); + assert.equal(form.layers[0].rows[3].keys.length, 10); + assert.equal(form.layers[0].rows[4].keys.length, 1); + + assert.deepEqual(form.layers[0].rows[0].keys.map(key => key.value), ['gap','gap','gap','gap','gap','gap','gap','gap','gap','gap','gap','gap','gap']); + assert.deepEqual(form.layers[0].rows[1].keys.map(key => key.value), ['gap','gap','gap','gap','gap','gap','gap','gap','gap','gap','gap','gap','gap']); + assert.deepEqual(form.layers[0].rows[2].keys.map(key => key.value), ['default-K_A','gap','gap','gap','gap','gap','gap','gap','gap','gap','gap']); + assert.deepEqual(form.layers[0].rows[3].keys.map(key => key.value), ['gap','gap','default-K_C','gap','default-K_B','gap','gap','gap','gap','gap']); + assert.deepEqual(form.layers[0].rows[4].keys.map(key => key.value), ['gap']); + }); + }); + + describe('EmbedOskKvkInKmx.transformVisualKeyboardToKmxPlus', function() { + it('should transform a .kvks file into an KMX+ structure', async function() { + const vk = loadKvkFile(makePathToFixture('embed-osk', 'khmer_angkor.kvks'), callbacks); + assert.isNotNull(vk); + + const kmxPlus = new EmbedOskInKmx(callbacks,{}).unitTestEndpoints.createEmptyKmxPlusFile(); + assert.isNotNull(kmxPlus); + + embedder.unitTestEndpoints.transformVisualKeyboardToKmxPlus(kmxPlus, vk); + + // Verify various aspects of the kmxPlus based on the source .kvks + assert.equal(kmxPlus.kmxplus.keys.flicks.length, 1); + + // number of s in the .kvks = 186, vscode search + assert.equal(kmxPlus.kmxplus.keys.keys.length, 186); + + // first key in the file is RA K_B ឞ + assert.equal(kmxPlus.kmxplus.keys.keys[0].id.value, 'rightalt-K_B'); + assert.equal(kmxPlus.kmxplus.keys.keys[0].to.value, 'ឞ'); + + // last key in the file is Shift K_BKQUOTE » + assert.equal(kmxPlus.kmxplus.keys.keys[kmxPlus.kmxplus.keys.keys.length-1].id.value, 'shift-K_BKQUOTE'); + assert.equal(kmxPlus.kmxplus.keys.keys[kmxPlus.kmxplus.keys.keys.length-1].to.value, '»'); + + // first layer is ralt + // first key on the first row of the RALT layer should be RALT+BKQUOTE + assert.equal(kmxPlus.kmxplus.layr.forms.length, 1); + assert.equal(kmxPlus.kmxplus.layr.forms[0].baseLayout.value, 'en-us'); + assert.equal(kmxPlus.kmxplus.layr.forms[0].flags, KMXPlus.LayrFormFlags.chiralSeparate); + // TODO-EMBED-OSK-IN-KMX: need to test showBaseLayout at some point + assert.equal(kmxPlus.kmxplus.layr.forms[0].fontFaceName.value, oskFontMagicToken); + assert.equal(kmxPlus.kmxplus.layr.forms[0].fontSizePct, 100); + assert.equal(kmxPlus.kmxplus.layr.forms[0].hardware.value, 'us'); + assert.equal(kmxPlus.kmxplus.layr.forms[0].minDeviceWidth, 0); + assert.equal(kmxPlus.kmxplus.layr.forms[0].layers.length, 4); + assert.equal(kmxPlus.kmxplus.layr.forms[0].layers[0].id.value, 'rightalt'); + assert.equal(kmxPlus.kmxplus.layr.forms[0].layers[0].mod, KMX.KMXFile.RALTFLAG); + assert.equal(kmxPlus.kmxplus.layr.forms[0].layers[0].rows.length, 5); + assert.equal(kmxPlus.kmxplus.layr.forms[0].layers[0].rows[0].keys.length, 13); + assert.equal(kmxPlus.kmxplus.layr.forms[0].layers[0].rows[0].keys[0].value, 'rightalt-K_BKQUOTE'); + + // Finally, pass the through KMXPlusBuilder, there should be no errors, + // hints, or warnings for this file + + const builder = new KMXPlusBuilder(kmxPlus); + const data = builder.compile(); + + assert.isNotNull(data); + assert.lengthOf(callbacks.messages, 0); + }); + }); + }); +}); diff --git a/developer/src/kmc-kmn/test/embed-osk.tests.ts b/developer/src/kmc-kmn/test/embed-osk.tests.ts index fdf234cecf..c9383f5d4d 100644 --- a/developer/src/kmc-kmn/test/embed-osk.tests.ts +++ b/developer/src/kmc-kmn/test/embed-osk.tests.ts @@ -6,25 +6,13 @@ import { fileURLToPath } from 'node:url'; import 'mocha'; import { assert } from 'chai'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; -import { KMX, KMXPlus, ModifierKeyConstant, USVirtualKeyCodes, VisualKeyboard } from '@keymanapp/common-types'; -import { KMXPlusBuilder, oskFontMagicToken } from '@keymanapp/developer-utils'; -import { makePathToFixture } from './helpers/index.js'; -import { KmnCompiler, KmnCompilerMessages } from '../src/main.js'; +import { KMX } from '@keymanapp/common-types'; +import { KmnCompiler } from '../src/main.js'; import { EmbedOskInKmx } from '../src/compiler/embed-osk/embed-osk.js'; -import { loadKvkFile } from '../src/compiler/osk.js'; const __dirname = dirname(fileURLToPath(import.meta.url)).replace(/\\/g, '/'); const keyboardsDir = __dirname + '/../../../../../common/test/keyboards/'; - -// VK header is not used in all functions, e.g. buildLayerBags, so this is a -// default header for those tests -const NullVisualKeyboardHeader: VisualKeyboard.VisualKeyboardHeader = { - flags: VisualKeyboard.VisualKeyboardHeaderFlags.kvkhNone, - ansiFont: null, - unicodeFont: null, -}; - describe('Compiler OSK Embedding', function() { const callbacks = new TestCompilerCallbacks(); @@ -261,285 +249,6 @@ describe('Compiler OSK Embedding', function() { }); }); - describe('EmbedOskInKmx.buildLayerBags', function() { - const embedder = new EmbedOskInKmx(callbacks, {}); - - it('should build a bag of layers from an in-memory .kvks structure', async function() { - const vk: VisualKeyboard.VisualKeyboard = { - header: NullVisualKeyboardHeader, - keys: [ - { - vkey: USVirtualKeyCodes.K_A, - text: 'a', - shift: 0, - flags: VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode, - }, - { - vkey: USVirtualKeyCodes.K_B, - text: 'B', - shift: VisualKeyboard.VisualKeyboardShiftState.KVKS_SHIFT, - flags: VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode, - }, - { - vkey: USVirtualKeyCodes.K_C, - text: 'Ctrl+Shift+C', - shift: VisualKeyboard.VisualKeyboardShiftState.KVKS_CTRL | VisualKeyboard.VisualKeyboardShiftState.KVKS_SHIFT, - flags: VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode, - }, - ] - }; - - const strs = new KMXPlus.Strs(); - const keys = new KMXPlus.Keys(strs); - const bag = embedder.unitTestEndpoints.buildLayerBags(vk, strs, keys); - - assert.lengthOf(strs.strings, 7); - assert.equal(strs.strings[0].value, ''); - assert.equal(strs.strings[1].value, 'default-K_A'); - assert.equal(strs.strings[2].value, 'a'); - assert.equal(strs.strings[3].value, 'shift-K_B'); - assert.equal(strs.strings[4].value, 'B'); - assert.equal(strs.strings[5].value, 'shift-ctrl-K_C'); - assert.equal(strs.strings[6].value, 'Ctrl+Shift+C'); - - assert.lengthOf(keys.flicks, 1); - assert.lengthOf(keys.flicks[0].flicks, 0); - assert.equal(keys.flicks[0].id, strs.strings[0]); - - assert.deepEqual(keys.keys, [ - { - flags: 0, - flicks: "", - id: strs.strings[1], - longPress: null, - longPressDefault: strs.strings[0], - multiTap: null, - switch: strs.strings[0], - to: strs.strings[2], - width: 100 - }, - { - flags: 0, - flicks: "", - id: strs.strings[3], - longPress: null, - longPressDefault: strs.strings[0], - multiTap: null, - switch: strs.strings[0], - to: strs.strings[4], - width: 100 - }, - { - flags: 0, - flicks: "", - id: strs.strings[5], - longPress: null, - longPressDefault: strs.strings[0], - multiTap: null, - switch: strs.strings[0], - to: strs.strings[6], - width: 100 - }, - ]); - - assert.isArray(keys.kmap); - assert.isEmpty(keys.kmap); - - // bag will be a map of maps; this test has three layers with an unmodified base layer K_A, a shift+K_B, and Ctrl+Shift+C - assert.isNotNull(bag); - assert.equal(bag.size, 3); - assert.isTrue(bag.has(0)); - assert.isTrue(bag.has(ModifierKeyConstant.K_SHIFTFLAG)); - assert.isTrue(bag.has(ModifierKeyConstant.K_SHIFTFLAG | ModifierKeyConstant.K_CTRLFLAG)); - - const defaultLayer = bag.get(0); - assert.equal(defaultLayer.size, 1); - - assert.isTrue(defaultLayer.has(USVirtualKeyCodes.K_A)); - const k_a = defaultLayer.get(USVirtualKeyCodes.K_A); - assert.equal(k_a.flags, 0); - assert.equal(k_a.flicks, ""); - assert.equal(k_a.id, strs.strings[1]); - assert.equal(k_a.longPress, null); - assert.equal(k_a.longPressDefault, strs.strings[0]); - assert.equal(k_a.multiTap, null); - assert.equal(k_a.switch, strs.strings[0]); - assert.equal(k_a.to, strs.strings[2]); - assert.equal(k_a.width, 100); - - const shiftLayer = bag.get(ModifierKeyConstant.K_SHIFTFLAG); - assert.equal(shiftLayer.size, 1); - - assert.isTrue(shiftLayer.has(USVirtualKeyCodes.K_B)); - const k_b = shiftLayer.get(USVirtualKeyCodes.K_B); - assert.equal(k_b.flags, 0); - assert.equal(k_b.flicks, ""); - assert.equal(k_b.id, strs.strings[3]); - assert.equal(k_b.longPress, null); - assert.equal(k_b.longPressDefault, strs.strings[0]); - assert.equal(k_b.multiTap, null); - assert.equal(k_b.switch, strs.strings[0]); - assert.equal(k_b.to, strs.strings[4]); - assert.equal(k_b.width, 100); - - const shiftCtrlLayer = bag.get(ModifierKeyConstant.K_SHIFTFLAG | ModifierKeyConstant.K_CTRLFLAG); - assert.equal(shiftCtrlLayer.size, 1); - - assert.isTrue(shiftCtrlLayer.has(USVirtualKeyCodes.K_C)); - const k_c = shiftCtrlLayer.get(USVirtualKeyCodes.K_C); - assert.equal(k_c.flags, 0); - assert.equal(k_c.flicks, ""); - assert.equal(k_c.id, strs.strings[5]); - assert.equal(k_c.longPress, null); - assert.equal(k_c.longPressDefault, strs.strings[0]); - assert.equal(k_c.multiTap, null); - assert.equal(k_c.switch, strs.strings[0]); - assert.equal(k_c.to, strs.strings[6]); - assert.equal(k_c.width, 100); - - }); - - it('should emit WARN_EmbeddedOskDoesNotSupportBitmaps if a key with kvkkBitmap flag is found', async function() { - const vk: VisualKeyboard.VisualKeyboard = { - header: NullVisualKeyboardHeader, - keys: [ - { - vkey: USVirtualKeyCodes.K_A, - shift: 0, - // kvkkUnicode required because otherwise the key is ignored as 'ansi' - flags: VisualKeyboard.VisualKeyboardKeyFlags.kvkkBitmap | VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode, - bitmap: new Uint8Array() - }, - ] - }; - - const strs = new KMXPlus.Strs(); - const keys = new KMXPlus.Keys(strs); - const bag = embedder.unitTestEndpoints.buildLayerBags(vk, strs, keys); - assert.isNotNull(bag); - assert.equal(bag.size, 0); - assert.isTrue(callbacks.hasMessage(KmnCompilerMessages.WARN_EmbeddedOskDoesNotSupportBitmaps)); - }); - - it('should emit HINT_EmbeddedOskDoesNotSupportNonUnicode if a key without kvkkUnicode flag is found', async function() { - const vk: VisualKeyboard.VisualKeyboard = { - header: NullVisualKeyboardHeader, - keys: [ - { - vkey: USVirtualKeyCodes.K_A, - shift: 0, - // !kvkkUnicode - flags: 0 as VisualKeyboard.VisualKeyboardKeyFlags, - }, - ] - }; - - const strs = new KMXPlus.Strs(); - const keys = new KMXPlus.Keys(strs); - const bag = embedder.unitTestEndpoints.buildLayerBags(vk, strs, keys); - assert.isNotNull(bag); - assert.equal(bag.size, 0); - assert.isTrue(callbacks.hasMessage(KmnCompilerMessages.HINT_EmbeddedOskDoesNotSupportNonUnicode)); - }); - }); - - describe('EmbedOskInKmx.buildForm', function() { - const embedder = new EmbedOskInKmx(callbacks, {}); - - it('should a layout of keys from a layer bag, from an in-memory .kvks structure', async function() { - const vk: VisualKeyboard.VisualKeyboard = { - header: NullVisualKeyboardHeader, - keys: [ - { vkey: USVirtualKeyCodes.K_A, text: 'a', shift: 0, flags: VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode, }, - { vkey: USVirtualKeyCodes.K_B, text: 'b', shift: 0, flags: VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode, }, - { vkey: USVirtualKeyCodes.K_C, text: 'c', shift: 0, flags: VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode, }, - ] - }; - - const strs = new KMXPlus.Strs(); - const keys = new KMXPlus.Keys(strs); - const layerBags = embedder.unitTestEndpoints.buildLayerBags(vk, strs, keys); - assert.isNotNull(layerBags); - const form = embedder.unitTestEndpoints.buildForm(vk, layerBags, strs); - assert.isNotNull(form); - assert.equal(form.baseLayout.value, 'en-us'); // For v19 - assert.equal(form.flags, 0); - assert.equal(form.fontFaceName.value, oskFontMagicToken); - assert.equal(form.fontSizePct, 100); - assert.equal(form.hardware.value, 'us'); - assert.equal(form.minDeviceWidth, 0); - - assert.lengthOf(form.layers, 1); - assert.equal(form.layers[0].id.value, 'default'); - assert.equal(form.layers[0].mod, 0); // no modifiers - assert.equal(form.layers[0].rows.length, 5); - - assert.equal(form.layers[0].rows[0].keys.length, 13); - assert.equal(form.layers[0].rows[1].keys.length, 13); - assert.equal(form.layers[0].rows[2].keys.length, 11); - assert.equal(form.layers[0].rows[3].keys.length, 10); - assert.equal(form.layers[0].rows[4].keys.length, 1); - - assert.deepEqual(form.layers[0].rows[0].keys.map(key => key.value), ['gap','gap','gap','gap','gap','gap','gap','gap','gap','gap','gap','gap','gap']); - assert.deepEqual(form.layers[0].rows[1].keys.map(key => key.value), ['gap','gap','gap','gap','gap','gap','gap','gap','gap','gap','gap','gap','gap']); - assert.deepEqual(form.layers[0].rows[2].keys.map(key => key.value), ['default-K_A','gap','gap','gap','gap','gap','gap','gap','gap','gap','gap']); - assert.deepEqual(form.layers[0].rows[3].keys.map(key => key.value), ['gap','gap','default-K_C','gap','default-K_B','gap','gap','gap','gap','gap']); - assert.deepEqual(form.layers[0].rows[4].keys.map(key => key.value), ['gap']); - }); - }); - - describe('EmbedOskInKmx.transformVisualKeyboardToKmxPlus', function() { - const embedder = new EmbedOskInKmx(callbacks, {}); - - it('should transform a .kvks file into an KMX+ structure', async function() { - const vk = loadKvkFile(makePathToFixture('embed-osk', 'khmer_angkor.kvks'), callbacks); - assert.isNotNull(vk); - - const kmxPlus = embedder.unitTestEndpoints.transformVisualKeyboardToKmxPlus(vk); - assert.isNotNull(kmxPlus); - - // Verify various aspects of the kmxPlus based on the source .kvks - assert.equal(kmxPlus.kmxplus.keys.flicks.length, 1); - - // number of s in the .kvks = 186, vscode search - assert.equal(kmxPlus.kmxplus.keys.keys.length, 186); - - // first key in the file is RA K_B ឞ - assert.equal(kmxPlus.kmxplus.keys.keys[0].id.value, 'rightalt-K_B'); - assert.equal(kmxPlus.kmxplus.keys.keys[0].to.value, 'ឞ'); - - // last key in the file is Shift K_BKQUOTE » - assert.equal(kmxPlus.kmxplus.keys.keys[kmxPlus.kmxplus.keys.keys.length-1].id.value, 'shift-K_BKQUOTE'); - assert.equal(kmxPlus.kmxplus.keys.keys[kmxPlus.kmxplus.keys.keys.length-1].to.value, '»'); - - // first layer is ralt - // first key on the first row of the RALT layer should be RALT+BKQUOTE - assert.equal(kmxPlus.kmxplus.layr.forms.length, 1); - assert.equal(kmxPlus.kmxplus.layr.forms[0].baseLayout.value, 'en-us'); - assert.equal(kmxPlus.kmxplus.layr.forms[0].flags, KMXPlus.LayrFormFlags.chiralSeparate); - // TODO-EMBED-OSK-IN-KMX: need to test showBaseLayout at some point - assert.equal(kmxPlus.kmxplus.layr.forms[0].fontFaceName.value, oskFontMagicToken); - assert.equal(kmxPlus.kmxplus.layr.forms[0].fontSizePct, 100); - assert.equal(kmxPlus.kmxplus.layr.forms[0].hardware.value, 'us'); - assert.equal(kmxPlus.kmxplus.layr.forms[0].minDeviceWidth, 0); - assert.equal(kmxPlus.kmxplus.layr.forms[0].layers.length, 4); - assert.equal(kmxPlus.kmxplus.layr.forms[0].layers[0].id.value, 'rightalt'); - assert.equal(kmxPlus.kmxplus.layr.forms[0].layers[0].mod, KMX.KMXFile.RALTFLAG); - assert.equal(kmxPlus.kmxplus.layr.forms[0].layers[0].rows.length, 5); - assert.equal(kmxPlus.kmxplus.layr.forms[0].layers[0].rows[0].keys.length, 13); - assert.equal(kmxPlus.kmxplus.layr.forms[0].layers[0].rows[0].keys[0].value, 'rightalt-K_BKQUOTE'); - - // Finally, pass the through KMXPlusBuilder, there should be no errors, - // hints, or warnings for this file - - const builder = new KMXPlusBuilder(kmxPlus); - const data = builder.compile(); - - assert.isNotNull(data); - assert.lengthOf(callbacks.messages, 0); - }); - }); - describe('EmbedOskInKmx.embed', function() { // const embedder = new EmbedOskInKmx(callbacks, {});