feat(developer): add vkey support

vkeyMaps table compiler, unit test, and fixes to json schema. Adds a
vkeyMaps entry to the basic e2e test.

us-virtual-keys.ts has a new mapping for CLDR VKey Enums, but I will
tweak this in an upcoming commit to use constants instead of hard-coded
values.
This commit is contained in:
Marc Durdin 2022-09-05 10:14:10 +10:00
parent c08dbfbf9f
commit cc0ddffd61
17 changed files with 349 additions and 13 deletions

View file

@ -1,5 +1,4 @@
import { constants } from '@keymanapp/ldml-keyboard-constants';
import KMXPlusFile, { Vkey } from '../kmx/kmx-plus';
import KMXPlusFile from '../kmx/kmx-plus';
import LDMLKeyboardXMLSourceFile from '../ldml-keyboard/ldml-keyboard-xml';
import LDMLKeyboardXMLSourceFileReader from '../ldml-keyboard/ldml-keyboard-xml-reader';
import CompilerCallbacks from './callbacks';
@ -7,12 +6,14 @@ import { KeysCompiler } from './keys';
import { LocaCompiler } from './loca';
import { MetaCompiler } from './meta';
import { NameCompiler } from './name';
import { VkeyCompiler } from './vkey';
const SECTION_COMPILERS = [
KeysCompiler,
LocaCompiler,
MetaCompiler,
NameCompiler
NameCompiler,
VkeyCompiler
];
export default class Compiler {
@ -67,15 +68,14 @@ export default class Compiler {
}
const sect = section.compile();
if(!sect) {
// This should not really happen -- validate() should be telling us
// if something is going to fail to compiler
passed = false;
continue;
}
kmx.kmxplus[section.id] = sect as any;
}
// TEMP until we have a Vkey compiler
kmx.kmxplus[constants.section.vkey] = new Vkey() as any;
return passed ? kmx : null;
}
}

View file

@ -10,7 +10,7 @@ export enum CompilerErrorSeverity {
};
const m = (code: number, message: string) => { return { code, message } };
// const SevInfo = CompilerErrorSeverity.Info;
const SevInfo = CompilerErrorSeverity.Info;
const SevHint = CompilerErrorSeverity.Hint;
// const SevWarn = CompilerErrorSeverity.Warn;
const SevError = CompilerErrorSeverity.Error;
@ -45,6 +45,22 @@ export class CompilerMessages {
m(this.HINT_LocaleIsNotMinimalAndClean, `Locale '${sourceLocale}' is not minimal or correctly formatted and should be '${locale}'`);
static HINT_LocaleIsNotMinimalAndClean = SevHint | 0x0008;
static Error_VkeyIsNotValid = (vkey: string) =>
m(this.ERROR_VkeyIsNotValid, `Virtual key '${vkey}' is not found in the CLDR VKey Enum table.`);
static ERROR_VkeyIsNotValid = SevError | 0x0009;
static Hint_VkeyMapIsRedundant = (vkey: string) =>
m(this.HINT_VkeyMapIsRedundant, `Virtual key '${vkey}' is mapped to itself, which is redundant.`);
static HINT_VkeyMapIsRedundant = SevHint | 0x000A;
static Error_VkeyMapIsRepeated = (vkey: string) =>
m(this.ERROR_VkeyMapIsRepeated, `Virtual key '${vkey}' has more than one VkeyMap entry.`);
static ERROR_VkeyMapIsRepeated = SevError | 0x000B;
static Info_MultipleVkeyMapsHaveSameTarget = (vkey: string) =>
m(this.INFO_MultipleVkeyMapsHaveSameTarget, `Target virtual key '${vkey}' has multiple source mappings, which may be an error.`);
static INFO_MultipleVkeyMapsHaveSameTarget = SevInfo | 0x000C;
static severityName(code: number): string {
let severity = code & CompilerErrorSeverity.Severity_Mask;
switch(severity) {

View file

@ -16,6 +16,10 @@ export class SectionCompiler {
return null;
}
public get required(): boolean {
return true;
}
public compile(): Section {
return null;
}

View file

@ -0,0 +1,68 @@
import { constants } from "@keymanapp/ldml-keyboard-constants";
import { Vkey } from "../kmx/kmx-plus";
import { LdmlVkeyNames } from "../ldml-keyboard/us-virtual-keys";
import { CompilerMessages } from "./messages";
import { SectionCompiler } from "./section-compiler";
export class VkeyCompiler extends SectionCompiler {
public get id() {
return constants.section.vkey;
}
public get required(): boolean {
return !!this.keyboard.vkeyMaps;
}
public validate(): boolean {
let valid = true;
if(this.keyboard.vkeyMaps) {
let from: string[] = [], to: string[] = [];
this.keyboard.vkeyMaps.vkeyMap.forEach(vk => {
if(LdmlVkeyNames[vk.from] === undefined) {
this.callbacks.reportMessage(CompilerMessages.Error_VkeyIsNotValid(vk.from));
valid = false;
}
if(LdmlVkeyNames[vk.to] === undefined) {
this.callbacks.reportMessage(CompilerMessages.Error_VkeyIsNotValid(vk.to));
valid = false;
}
if(vk.from == vk.to) {
this.callbacks.reportMessage(CompilerMessages.Hint_VkeyMapIsRedundant(vk.from));
}
if(from.find(svk => svk == vk.from)) {
this.callbacks.reportMessage(CompilerMessages.Error_VkeyMapIsRepeated(vk.from));
valid = false;
}
from.push(vk.from);
if(to.find(svk => svk == vk.to)) {
this.callbacks.reportMessage(CompilerMessages.Info_MultipleVkeyMapsHaveSameTarget(vk.to));
}
to.push(vk.to);
});
}
return valid;
}
public compile(): Vkey {
let result = new Vkey();
if(!this.keyboard.vkeyMaps) {
return result;
}
result.vkeys = this.keyboard.vkeyMaps?.vkeyMap.map(vk => {
return {
vkey: LdmlVkeyNames[vk.from],
target: LdmlVkeyNames[vk.to]
};
});
// Sort according to vkey binary order, per C7043
result.vkeys.sort((a,b) => a.vkey - b.vkey);
return result;
}
}

View file

@ -30,6 +30,7 @@ export default class LDMLKeyboardXMLSourceFileReader {
box(source?.keyboard, 'layerMaps');
box(source?.keyboard?.names, 'name');
box(source?.keyboard?.vkeyMaps, 'vkeyMap');
box(source?.keyboard?.keys, 'key');
box(source?.keyboard?.locales, 'locale');
if(source?.keyboard?.layerMaps) {

View file

@ -25,6 +25,7 @@ export interface LKKeyboard {
settings?: LKSettings;
keys?: LKKeys;
layerMaps?: LKLayerMaps[];
vkeyMaps?: LKVkeyMaps;
};
export interface LKLocales {
@ -82,3 +83,12 @@ export interface LKLayerMap {
export interface LKRow {
keys?: string;
};
export interface LKVkeyMaps {
vkeyMap?: LKVkeyMap[];
};
export interface LKVkeyMap {
from?: string;
to?: string;
};

View file

@ -137,3 +137,59 @@ export const USVirtualKeyMap: number[][] = [
// [shift] * Z X C V B N M , . / [shift] *=oE2
[ k.K_oE2, k.K_Z, k.K_X, k.K_C, k.K_V, k.K_B, k.K_N, k.K_M, k.K_COMMA, k.K_PERIOD, k.K_SLASH ]
];
/**
* Maps LDML VKey Names from CLDR VKey Enum in TR35 to Keyman virtual key codes
*/
export const LdmlVkeyNames: Record<string, number> = {
'SPACE': 0x20, // A03
'0': 0x30, // E10
'1': 0x31, // E01
'2': 0x32, // E02
'3': 0x33, // E03
'4': 0x34, // E04
'5': 0x35, // E05
'6': 0x36, // E06
'7': 0x37, // E07
'8': 0x38, // E08
'9': 0x39, // E09
'A': 0x41, // C01
'B': 0x42, // B05
'C': 0x43, // B03
'D': 0x44, // C03
'E': 0x45, // D03
'F': 0x46, // C04
'G': 0x47, // C05
'H': 0x48, // C06
'I': 0x49, // D08
'J': 0x4A, // C07
'K': 0x4B, // C08
'L': 0x4C, // C09
'M': 0x4D, // B07
'N': 0x4E, // B06
'O': 0x4F, // D09
'P': 0x50, // D10
'Q': 0x51, // D01
'R': 0x52, // D04
'S': 0x53, // C02
'T': 0x54, // D05
'U': 0x55, // D07
'V': 0x56, // B05
'W': 0x57, // D02
'X': 0x58, // B02
'Y': 0x59, // D06
'Z': 0x5A, // B01
'SEMICOLON': 0xBA, // C10
'EQUAL': 0xBB, // E12
'COMMA': 0xBC, // B08
'HYPHEN': 0xBD, // E11
'PERIOD': 0xBE, // B09
'SLASH': 0xBF, // B10
'GRAVE': 0xC0, // E00
'LBRACKET': 0xDB, // D11
'BACKSLASH': 0xDC, // D13
'RBRACKET': 0xDD, // D12
'QUOTE': 0xDE, // C11
'LESS-THAN': 0xE2, // B00 102nd key on European layouts, right of left shift.
'ABNT2': 0xC1, // B11 Extra key, left of right-shift
};

View file

@ -14,13 +14,18 @@
# xxd -g 1 -l 12 /tmp/basic.kmx | cut -d' ' -f 10-13
# # rm /tmp/basic.kmx # or you may wish to examine it in more detail
#
# To generate a binary file for comparison purposes:
#
# node ../../../common/tools/hextobin/build/hextobin.js test/fixtures/basic.txt test/fixtures/basic-h.kmx
#
#
block(kmxheader) # struct COMP_KEYBOARD {
4b 58 54 53 # KMX_DWORD dwIdentifier; // 0000 Keyman compiled keyboard id
00 10 00 00 # KMX_DWORD dwFileVersion; // 0004 Version of the file - Keyman 4.0 is 0x0400
62 78 ee 07 # KMX_DWORD dwCheckSum; // 0008 As stored in keyboard
4c f9 ee bd # KMX_DWORD dwCheckSum; // 0008 As stored in keyboard
00 00 00 00 # KMX_DWORD KeyboardID; // 000C as stored in HKEY_LOCAL_MACHINE//system//currentcontrolset//control//keyboard layouts
01 00 00 00 # KMX_DWORD IsRegistered; // 0010
00 00 00 00 # KMX_DWORD version; // 0014 keyboard version
@ -52,7 +57,7 @@ block(sect) # struct COMP_KMXPLUS_SECT {
73 65 63 74 # KMX_DWORD header.ident; // 0000 Section name
sizeof(sect) # KMX_DWORD header.size; // 0004 Section length
diff(sect,eof) # KMX_DWORD total; // 0008 KMXPlus entire length
05 00 00 00 # KMX_DWORD count; // 000C number of section headers
06 00 00 00 # KMX_DWORD count; // 000C number of section headers
# };
# Next sections are sect entries
# KMX_DWORD sect; // 0010+ Section identity
@ -73,6 +78,9 @@ block(sect) # struct COMP_KMXPLUS_SECT {
73 74 72 73
diff(sect,strs)
76 6b 65 79
diff(sect,vkey)
block(keys) # struct COMP_KMXPLUS_KEYS {
6b 65 79 73 # KMX_DWORD header.ident; // 0000 Section name - keys
sizeof(keys) # KMX_DWORD header.size; // 0004 Section length
@ -155,4 +163,13 @@ block(strs) # struct COMP_KMXPLUS_STRS {
block(endstrs) # end of strs block
block(vkey) # struct COMP_KMXPLUS_VKEY {
76 6b 65 79 # KMX_DWORD header.ident; // 0000 Section name - vkey
sizeof(vkey) # KMX_DWORD header.size; // 0004 Section length
01 00 00 00 # KMX_DWORD count; // 0008 Number of vkey maps
00 00 00 00 # KMX_DWORD reserved; // 000C padding
51 00 00 00 41 00 00 00 # KMX_DWORD vkey; KMX_DWORD target; // K_Q => K_A
# };
block(eof) # end of file

View file

@ -12,6 +12,10 @@
<name value="TestKbd" />
</names>
<vkeyMaps>
<vkeyMap from="Q" to="A" />
</vkeyMaps>
<keys>
<key id="hmaqtua" to="ħ" />
<key id="that" to="ថា" />

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE keyboard SYSTEM "../../../../../../../resources/standards-data/ldml-keyboards/techpreview/ldmlKeyboard.dtd">
<keyboard locale="mt" conformsTo="techpreview">
<names>
<name value="Invalid" />
</names>
<vkeyMaps>
<!-- key names are upper case, so this from attribute is invalid -->
<vkeyMap from="q" to="A" />
<!-- Sorry, hyphen isn't spelled like this even if it should be -->
<vkeyMap from="HYFEN" to="Z" />
</vkeyMaps>
<keys />
</keyboard>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE keyboard SYSTEM "../../../../../../../resources/standards-data/ldml-keyboards/techpreview/ldmlKeyboard.dtd">
<keyboard locale="mt" conformsTo="techpreview">
<names>
<name value="Invalid" />
</names>
<vkeyMaps>
<!-- repeated definitions for same vkey are invalid -->
<vkeyMap from="A" to="Q" />
<vkeyMap from="A" to="W" />
</vkeyMaps>
<keys />
</keyboard>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE keyboard SYSTEM "../../../../../../../resources/standards-data/ldml-keyboards/techpreview/ldmlKeyboard.dtd">
<keyboard locale="mt" conformsTo="techpreview">
<names>
<name value="Invalid" />
</names>
<vkeyMaps>
<!-- not a real 'to' key -->
<vkeyMap from="A" to="A-ACUTE" />
</vkeyMaps>
<keys />
</keyboard>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE keyboard SYSTEM "../../../../../../../resources/standards-data/ldml-keyboards/techpreview/ldmlKeyboard.dtd">
<keyboard locale="mt" conformsTo="techpreview">
<names>
<name value="My First Keyboard" />
</names>
<vkeyMaps>
<vkeyMap from="Q" to="A" />
<vkeyMap from="W" to="Z" />
<vkeyMap from="A" to="Q" />
<vkeyMap from="Z" to="W" />
</vkeyMaps>
<keys />
</keyboard>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE keyboard SYSTEM "../../../../../../../resources/standards-data/ldml-keyboards/techpreview/ldmlKeyboard.dtd">
<keyboard locale="mt" conformsTo="techpreview">
<names>
<name value="Invalid" />
</names>
<vkeyMaps>
<!-- this is redundant so it should be emitted -->
<vkeyMap from="A" to="A" />
</vkeyMaps>
<keys />
</keyboard>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE keyboard SYSTEM "../../../../../../../resources/standards-data/ldml-keyboards/techpreview/ldmlKeyboard.dtd">
<keyboard locale="mt" conformsTo="techpreview">
<names>
<name value="Invalid" />
</names>
<vkeyMaps>
<!-- it's likely this is incorrect, so it should raise a linter message -->
<vkeyMap from="A" to="Q" />
<vkeyMap from="Z" to="Q" />
</vkeyMaps>
<keys />
</keyboard>

View file

@ -0,0 +1,66 @@
import 'mocha';
import { assert } from 'chai';
import { VkeyCompiler } from '../src/keyman/compiler/vkey';
import { CompilerCallbacks, loadSectionFixture } from './helpers';
import { Vkey } from '../src/keyman/kmx/kmx-plus';
import { CompilerMessages } from '../src/keyman/compiler/messages';
import { USVirtualKeyCodes } from '../src/keyman/ldml-keyboard/us-virtual-keys';
describe('name', function () {
this.slow(500); // 0.5 sec -- json schema validation takes a while
it('should compile minimal vkey data', function() {
const callbacks = new CompilerCallbacks();
let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/minimal.xml', callbacks) as Vkey;
assert.equal(callbacks.messages.length, 0);
assert.equal(vkey.vkeys.length, 4);
// Note, final order is sorted by `vkey` member
assert.deepEqual(vkey.vkeys[0], {vkey: USVirtualKeyCodes.K_A, target: USVirtualKeyCodes.K_Q});
assert.deepEqual(vkey.vkeys[1], {vkey: USVirtualKeyCodes.K_Q, target: USVirtualKeyCodes.K_A});
assert.deepEqual(vkey.vkeys[2], {vkey: USVirtualKeyCodes.K_W, target: USVirtualKeyCodes.K_Z});
assert.deepEqual(vkey.vkeys[3], {vkey: USVirtualKeyCodes.K_Z, target: USVirtualKeyCodes.K_W});
});
it('should hint on redundant data', function() {
const callbacks = new CompilerCallbacks();
let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/redundant.xml', callbacks) as Vkey;
assert.isNotNull(vkey);
assert.equal(callbacks.messages.length, 1);
assert.deepEqual(callbacks.messages[0], CompilerMessages.Hint_VkeyMapIsRedundant('A'));
});
it('should report an info message if same target found', function() {
const callbacks = new CompilerCallbacks();
let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/same-target.xml', callbacks) as Vkey;
assert.isNotNull(vkey);
assert.equal(callbacks.messages.length, 1);
assert.deepEqual(callbacks.messages[0], CompilerMessages.Info_MultipleVkeyMapsHaveSameTarget('Q'));
});
it('should error on invalid "from" vkey', function() {
const callbacks = new CompilerCallbacks();
let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/invalid-from-vkey.xml', callbacks) as Vkey;
assert.isNull(vkey);
assert.equal(callbacks.messages.length, 2);
assert.deepEqual(callbacks.messages[0], CompilerMessages.Error_VkeyIsNotValid('q'));
assert.deepEqual(callbacks.messages[1], CompilerMessages.Error_VkeyIsNotValid('HYFEN'));
});
it('should error on invalid "to" vkey', function() {
const callbacks = new CompilerCallbacks();
let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/invalid-to-vkey.xml', callbacks) as Vkey;
assert.isNull(vkey);
assert.equal(callbacks.messages.length, 1);
assert.deepEqual(callbacks.messages[0], CompilerMessages.Error_VkeyIsNotValid('A-ACUTE'));
});
it('should error on repeated vkeys', function() {
const callbacks = new CompilerCallbacks();
let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/invalid-repeated-vkey.xml', callbacks) as Vkey;
assert.isNull(vkey);
assert.equal(callbacks.messages.length, 1);
assert.deepEqual(callbacks.messages[0], CompilerMessages.Error_VkeyMapIsRepeated('A'));
});
});

View file

@ -29,10 +29,7 @@
"$ref": "#/definitions/settings"
},
"vkeyMaps": {
"type": "array",
"items": {
"$ref": "#/definitions/vkeyMaps"
}
"$ref": "#/definitions/vkeyMaps"
},
"displayMap": {
"type": "array",