refactor(developer): move KVK embed into embed-osk-kvk.ts

This commit is contained in:
Marc Durdin 2025-11-27 11:42:08 +01:00
parent f50934bf69
commit ce5a5bb2f1
4 changed files with 513 additions and 459 deletions

View file

@ -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<VirtualKey, KMXPlus.KeysKeys>;
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<number, LayerBag>, 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<ModifierKeyConstant, LayerBag>();
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<number, KMXPlus.KeysKeys>();
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),
};
}

View file

@ -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<VirtualKey, KMXPlus.KeysKeys>;
// 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<number, LayerBag>, 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<ModifierKeyConstant, LayerBag>();
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<number, KMXPlus.KeysKeys>();
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),
};
};

View file

@ -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 <key>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);
});
});
});
});

View file

@ -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 <key>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, {});