diff --git a/android/Tests/KeyboardHarness/app/src/main/assets/keyboardharness.kpj b/android/Tests/KeyboardHarness/app/src/main/assets/keyboardharness.kpj index 80a3e1568a2..5df60525f84 100644 --- a/android/Tests/KeyboardHarness/app/src/main/assets/keyboardharness.kpj +++ b/android/Tests/KeyboardHarness/app/src/main/assets/keyboardharness.kpj @@ -7,6 +7,6 @@ True True True - lexicalmodel + keyboard diff --git a/common/include/kmx_file.h b/common/include/kmx_file.h index 748ba32d920..d0be382048d 100644 --- a/common/include/kmx_file.h +++ b/common/include/kmx_file.h @@ -5,7 +5,7 @@ #pragma once -#include +#include "km_types.h" #ifdef KM_CORE_LIBRARY // TODO: move this to a common namespace keyman::common::kmx_file or similar in the future @@ -52,8 +52,10 @@ namespace kmx { #define VERSION_160 0x00001000 #define VERSION_170 0x00001100 +#define VERSION_190 0x00001300 + #define VERSION_MIN VERSION_50 -#define VERSION_MAX VERSION_170 +#define VERSION_MAX VERSION_190 // // Backspace types @@ -62,6 +64,9 @@ namespace kmx { #define BK_DEFAULT 0 #define BK_DEADKEY 1 +// Next character to delete is a Unicode surrogate pair +#define BK_SURROGATE 4 + // Different begin types #define BEGIN_ANSI 0 #define BEGIN_UNICODE 1 @@ -267,14 +272,24 @@ namespace kmx { #define C_CODE_IFSYSTEMSTORE(store, val1, val2) U_UC_SENTINEL U_CODE_IFSYSTEMSTORE store val1 val2 #define C_CODE_SETSYSTEMSTORE(store, val) U_UC_SENTINEL U_CODE_SETSYSTEMSTORE store val +// +// COMP_KEYBOARD.dwFlags bitfield +// + #define KF_SHIFTFREESCAPS 0x0001 #define KF_CAPSONONLY 0x0002 #define KF_CAPSALWAYSOFF 0x0004 #define KF_LOGICALLAYOUT 0x0008 #define KF_AUTOMATICVERSION 0x0010 -// 16.0: Support for LDML Keyboards in KMXPlus file format -#define KF_KMXPLUS 0x0020 +/** 16.0+: A `COMP_KEYBOARD_KMXPLUSINFO` structure is present immediately after `COMP_KEYBOARD` */ +#define KF_KMXPLUS 0x0020 + +/** + * 19.0+: The `COMP_KEYBOARD_KMXPLUSINFO` structure contains a v19 embedded OSK; + * may be used with or without KF_KMXPLUS. + */ +#define KF_KMXPLUSOSK 0x0040 #define HK_ALT 0x00010000 #define HK_CTRL 0x00020000 @@ -362,17 +377,17 @@ struct COMP_KEYBOARD_KMXPLUSINFO { }; /** - * Only valid if comp_keyboard.dwFlags&KF_KMXPLUS + * Only valid if comp_keyboard.dwFlags&(KF_KMXPLUS|KF_KMXPLUSOSK) */ struct COMP_KEYBOARD_EX { - COMP_KEYBOARD header; // 0000 see COMP_KEYBOARD - COMP_KEYBOARD_KMXPLUSINFO kmxplus; // 0040 see COMP_KEYBOARD_EXTRA + struct COMP_KEYBOARD header; // 0000 see COMP_KEYBOARD + struct COMP_KEYBOARD_KMXPLUSINFO kmxplus; // 0040 see COMP_KEYBOARD_EXTRA }; -typedef COMP_KEYBOARD *PCOMP_KEYBOARD; -typedef COMP_STORE *PCOMP_STORE; -typedef COMP_KEY *PCOMP_KEY; -typedef COMP_GROUP *PCOMP_GROUP; +typedef struct COMP_KEYBOARD *PCOMP_KEYBOARD; +typedef struct COMP_STORE *PCOMP_STORE; +typedef struct COMP_KEY *PCOMP_KEY; +typedef struct COMP_GROUP *PCOMP_GROUP; extern const int CODE__SIZE[]; #define CODE__SIZE_MAX 5 @@ -382,10 +397,10 @@ extern const int CODE__SIZE[]; #define KEYBOARDFILEGROUP_SIZE 24 #define KEYBOARDFILEKEY_SIZE 20 -static_assert(sizeof(COMP_STORE) == KEYBOARDFILESTORE_SIZE, "COMP_STORE must be KEYBOARDFILESTORE_SIZE bytes"); -static_assert(sizeof(COMP_KEY) == KEYBOARDFILEKEY_SIZE, "COMP_KEY must be KEYBOARDFILEKEY_SIZE bytes"); -static_assert(sizeof(COMP_GROUP) == KEYBOARDFILEGROUP_SIZE, "COMP_GROUP must be KEYBOARDFILEGROUP_SIZE bytes"); -static_assert(sizeof(COMP_KEYBOARD) == KEYBOARDFILEHEADER_SIZE, "COMP_KEYBOARD must be KEYBOARDFILEHEADER_SIZE bytes"); +static_assert(sizeof(struct COMP_STORE) == KEYBOARDFILESTORE_SIZE, "COMP_STORE must be KEYBOARDFILESTORE_SIZE bytes"); +static_assert(sizeof(struct COMP_KEY) == KEYBOARDFILEKEY_SIZE, "COMP_KEY must be KEYBOARDFILEKEY_SIZE bytes"); +static_assert(sizeof(struct COMP_GROUP) == KEYBOARDFILEGROUP_SIZE, "COMP_GROUP must be KEYBOARDFILEGROUP_SIZE bytes"); +static_assert(sizeof(struct COMP_KEYBOARD) == KEYBOARDFILEHEADER_SIZE, "COMP_KEYBOARD must be KEYBOARDFILEHEADER_SIZE bytes"); #ifdef KM_CORE_LIBRARY } // namespace kmx diff --git a/common/test/keyboards/embed_osk/.gitignore b/common/test/keyboards/embed_osk/.gitignore new file mode 100644 index 00000000000..d16386367f7 --- /dev/null +++ b/common/test/keyboards/embed_osk/.gitignore @@ -0,0 +1 @@ +build/ \ No newline at end of file diff --git a/common/test/keyboards/embed_osk/embed_osk.kpj b/common/test/keyboards/embed_osk/embed_osk.kpj new file mode 100644 index 00000000000..2b5513fc525 --- /dev/null +++ b/common/test/keyboards/embed_osk/embed_osk.kpj @@ -0,0 +1,12 @@ + + + + $PROJECTPATH\build + $PROJECTPATH\source + True + True + False + keyboard + 2.0 + + diff --git a/common/test/keyboards/embed_osk/source/embed_osk.kps b/common/test/keyboards/embed_osk/source/embed_osk.kps new file mode 100644 index 00000000000..5ae723b1327 --- /dev/null +++ b/common/test/keyboards/embed_osk/source/embed_osk.kps @@ -0,0 +1,33 @@ + + + + 19.0.0.0 + 7.0 + + + + + + + test_v19_kmxplus + test_v19_kmxplus + + + + ..\build\test_v19_kmxplus.kmx + 0 + .kmx + + + + + test_v19_kmxplus + test_v19_kmxplus + 1.0 + + English + + + + + diff --git a/common/test/keyboards/embed_osk/source/test_v19_kmxplus.kmn b/common/test/keyboards/embed_osk/source/test_v19_kmxplus.kmn new file mode 100644 index 00000000000..7a4f6cad131 --- /dev/null +++ b/common/test/keyboards/embed_osk/source/test_v19_kmxplus.kmn @@ -0,0 +1,7 @@ +store(&VERSION) '19.0' +store(&NAME) 'Test KMX+ space reserved in header' +store(&TARGETS) 'desktop' + +begin Unicode > use(main) + +group(main) using keys diff --git a/common/web/types/src/consts/modifier-key-constants.ts b/common/web/types/src/consts/modifier-key-constants.ts index 245cbf70f37..1f09c183f14 100644 --- a/common/web/types/src/consts/modifier-key-constants.ts +++ b/common/web/types/src/consts/modifier-key-constants.ts @@ -4,9 +4,16 @@ * Modifier key bit-flags */ +import { VisualKeyboardShiftState } from "../kvk/visual-keyboard.js"; + +/** + * This type is declared as a `const` rather than as an `enum` for + * historical reasons. Use `ModifierKeyConstant` instead where possible. + */ export const ModifierKeyConstants = { - // Define Keyman Developer modifier bit-flags (exposed for use by other modules) - // Compare against /common/include/kmx_file.h. CTRL+F "#define LCTRLFLAG" to find the secton. + // Define Keyman Developer modifier bit-flags (exposed for use by other + // modules) Compare against /common/include/kmx_file.h. CTRL+F "#define + // LCTRLFLAG" to find the section. LCTRLFLAG: 0x0001, // Left Control flag RCTRLFLAG: 0x0002, // Right Control flag LALTFLAG: 0x0004, // Left Alt flag @@ -30,4 +37,155 @@ export const ModifierKeyConstants = { // Note: OTHER_MODIFIER = 0x10000, used by KMX+ for the // other modifier flag in layers, > 16 bit so not available here. // See keys_mod_other in keyman_core_ldml.ts +}; + +/** + * Defines the standard modifier key flags used by Keyman. Note that some keys + * are chiral, and toggle key state is ignored if neither the __FLAG nor the + * corresponding NOT__FLAG are set. + */ +export enum ModifierKeyConstant { + NO_MODIFIER = 0, + LCTRLFLAG = 0x0001, // Left Control flag + RCTRLFLAG = 0x0002, // Right Control flag + LALTFLAG = 0x0004, // Left Alt flag + RALTFLAG = 0x0008, // Right Alt flag + K_SHIFTFLAG = 0x0010, // Either shift flag + K_CTRLFLAG = 0x0020, // Either ctrl flag + K_ALTFLAG = 0x0040, // Either alt flag + K_METAFLAG = 0x0080, // Either Meta-key flag (tentative). Not usable in keyboard rules; + // Used internally (currently, only by KMW) to ensure Meta-key + // shortcuts safely bypass rules + // Meta key = Command key on macOS, Windows key on Windows/Linux + CAPITALFLAG = 0x0100, // Caps lock on + NOTCAPITALFLAG = 0x0200, // Caps lock NOT on + NUMLOCKFLAG = 0x0400, // Num lock on + NOTNUMLOCKFLAG = 0x0800, // Num lock NOT on + SCROLLFLAG = 0x1000, // Scroll lock on + NOTSCROLLFLAG = 0x2000, // Scroll lock NOT on + ISVIRTUALKEY = 0x4000, // It is a Virtual Key Sequence + VIRTUALCHARKEY = 0x8000, // Keyman 6.0: Virtual Key Cap Sequence NOT YET +}; + +export const LDML_MODIFIER_TO_KVK_MODIFIER = /* @__PURE__ */ (() => { + const m = new Map(); + m.set(ModifierKeyConstants.LCTRLFLAG, VisualKeyboardShiftState.KVKS_LCTRL); + m.set(ModifierKeyConstants.RCTRLFLAG, VisualKeyboardShiftState.KVKS_RCTRL); + m.set(ModifierKeyConstants.LALTFLAG, VisualKeyboardShiftState.KVKS_LALT); + m.set(ModifierKeyConstants.RALTFLAG, VisualKeyboardShiftState.KVKS_RALT); + m.set(ModifierKeyConstants.K_SHIFTFLAG, VisualKeyboardShiftState.KVKS_SHIFT); + m.set(ModifierKeyConstants.K_CTRLFLAG, VisualKeyboardShiftState.KVKS_CTRL); + m.set(ModifierKeyConstants.K_ALTFLAG, VisualKeyboardShiftState.KVKS_ALT); + return m; +})(); + +export const KVK_MODIFIER_TO_LDML_MODIFIER = /* @__PURE__ */ (() => { + const m = new Map(); + m.set(VisualKeyboardShiftState.KVKS_LCTRL, ModifierKeyConstants.LCTRLFLAG); + m.set(VisualKeyboardShiftState.KVKS_RCTRL, ModifierKeyConstants.RCTRLFLAG); + m.set(VisualKeyboardShiftState.KVKS_LALT, ModifierKeyConstants.LALTFLAG); + m.set(VisualKeyboardShiftState.KVKS_RALT, ModifierKeyConstants.RALTFLAG); + m.set(VisualKeyboardShiftState.KVKS_SHIFT, ModifierKeyConstants.K_SHIFTFLAG); + m.set(VisualKeyboardShiftState.KVKS_CTRL, ModifierKeyConstants.K_CTRLFLAG); + m.set(VisualKeyboardShiftState.KVKS_ALT, ModifierKeyConstants.K_ALTFLAG); + return m; +})(); + +export function translateLdmlModifiersToVisualKeyboardShift(modifiers: ModifierKeyConstant): VisualKeyboardShiftState { + if(modifiers == ModifierKeyConstant.NO_MODIFIER) { + return VisualKeyboardShiftState.KVKS_NORMAL; + } + + if(modifiers & + (ModifierKeyConstant.CAPITALFLAG | ModifierKeyConstant.NUMLOCKFLAG | ModifierKeyConstant.SCROLLFLAG) + ) { + // Caps/Num/Scroll are not supported in .kvk, in combination or alone + return null; + } + + let shift: VisualKeyboardShiftState = 0; + + for(const mod of LDML_MODIFIER_TO_KVK_MODIFIER.keys()) { + if(modifiers & mod) { + shift |= LDML_MODIFIER_TO_KVK_MODIFIER.get(mod); + } + } + + return shift; +} + +export function translateVisualKeyboardShiftToLdmlModifiers(shift: VisualKeyboardShiftState): ModifierKeyConstant { + if(shift == VisualKeyboardShiftState.KVKS_NORMAL) { + return 0; + } + + let mod = 0; + for(const state of KVK_MODIFIER_TO_LDML_MODIFIER.keys()) { + if(shift & state) { + mod |= KVK_MODIFIER_TO_LDML_MODIFIER.get(state); + } + } + + return mod; +} + + + +function VkShiftStateToKmxShiftState(ShiftState: number): number { + + interface TVKToKMX { + VK: VisualKeyboardShiftState; KMX: ModifierKeyConstant; + } + + const Map: TVKToKMX[] = [ + {VK: VisualKeyboardShiftState.KVKS_SHIFT, KMX: ModifierKeyConstant.K_SHIFTFLAG}, + {VK: VisualKeyboardShiftState.KVKS_CTRL, KMX: ModifierKeyConstant.K_CTRLFLAG}, + {VK: VisualKeyboardShiftState.KVKS_ALT, KMX: ModifierKeyConstant.K_ALTFLAG}, + {VK: VisualKeyboardShiftState.KVKS_LCTRL, KMX: ModifierKeyConstant.LCTRLFLAG}, + {VK: VisualKeyboardShiftState.KVKS_RCTRL, KMX: ModifierKeyConstant.RCTRLFLAG}, + {VK: VisualKeyboardShiftState.KVKS_LALT, KMX: ModifierKeyConstant.LALTFLAG}, + {VK: VisualKeyboardShiftState.KVKS_RALT, KMX: ModifierKeyConstant.RALTFLAG} + ]; + + let result = 0; + for(let i = 0; i < Map.length; i++) { + if (ShiftState & Map[i].VK) { + result |= Map[i].KMX; + } + } + + return result; +} + +/** + * Convert a VK modifier combination bitmask to a hyphen-separated string of + * modifier names, e.g. for use in layer names and key identifiers. The name + * order matches the bit flag order from ModifierKeyConstant, not the bit flag + * order from VisualKeyboardShiftState, for historical reasons. + */ +export function visualKeyboardShiftToLayerName(shift: VisualKeyboardShiftState): string { + + // index is ModifierKeyConstant, not VisualKeyboardShiftState + const modifierNames: string[] = [ + 'leftctrl', + 'rightctrl', + 'leftalt', + 'rightalt', + 'shift', + 'ctrl', + 'alt' + ]; + + const mod = VkShiftStateToKmxShiftState(shift); + if(mod == 0) { + return 'default'; + } + + let result = ''; + for(let i = 0; i < modifierNames.length; i++) { + if(mod & (1 << i)) { + result += modifierNames[i] + '-'; + } + } + return result.substring(0, result.length - 1); } diff --git a/common/web/types/src/consts/virtual-key-constants.ts b/common/web/types/src/consts/virtual-key-constants.ts index 7b083241bdc..3e3fca2426b 100644 --- a/common/web/types/src/consts/virtual-key-constants.ts +++ b/common/web/types/src/consts/virtual-key-constants.ts @@ -141,10 +141,8 @@ export const USVirtualKeyCodes = { K_TABFWD:50012 }; -const k = USVirtualKeyCodes; - /** Map a CLDR scancode to a US VKey ala USVirtualKeyCodes */ -export const CLDRScanToUSVirtualKeyCodes = { +export const CLDRScanToUSVirtualKeyCodes = /* @__PURE__ */ ((k) => ({ 0x02: k.K_1, 0x03: k.K_2, 0x04: k.K_3, @@ -202,7 +200,7 @@ export const CLDRScanToUSVirtualKeyCodes = { 0x73: k.K_oC1, 0x7D: k.K_oE2, // << Same as 0x56; found on jis -}; +}))(USVirtualKeyCodes); export type KeyMap = number[][]; @@ -230,3 +228,27 @@ export function CLDRScanToVkey(scan: number, badScans?: Set): number { } } +const USVirtualKeyCodeNames = new Map(); + +function fillVirtualKeyNames() { + Object.keys(USVirtualKeyCodes).forEach(name => { + USVirtualKeyCodeNames.set((USVirtualKeyCodes)[name], name); + }); + + // These three keys have multiple definitions + USVirtualKeyCodeNames.set(USVirtualKeyCodes.K_oE2, 'K_oE2'); + USVirtualKeyCodeNames.set(USVirtualKeyCodes.K_oC1, 'K_oC1'); + USVirtualKeyCodeNames.set(USVirtualKeyCodes.K_oDF, 'K_oDF'); +} + +/** + * Get the defined name of a virtual key + * @param vk A defined Keyman virtual key + * @returns the name of the virtual key, or undefined if not found + */ +export function usVirtualKeyName(vk: number): string { + if(USVirtualKeyCodeNames.size == 0) { + fillVirtualKeyNames(); + } + return USVirtualKeyCodeNames.get(vk); +} \ No newline at end of file diff --git a/common/web/types/src/kmx/kmx-plus/kmx-plus-file.ts b/common/web/types/src/kmx/kmx-plus/kmx-plus-file.ts new file mode 100644 index 00000000000..924f9b9f9dd --- /dev/null +++ b/common/web/types/src/kmx/kmx-plus/kmx-plus-file.ts @@ -0,0 +1,419 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by mcdurdin on 2025-10-08 + * + * Binary file format Restructure structs for KMX+ + */ +import * as KMX from '../kmx.js'; +import * as r from 'restructure'; +import KMXFile = KMX.KMXFile; +import { KMXPlusVersion } from '@keymanapp/ldml-keyboard-constants'; + +/** + * Binary representation of KMX+ data, using Restructure. These structures + * should be directly used only by KMX+ file readers and writers; in general, + * most things should use the in-memory `KMXPlusData` structures in kmx-plus.ts. + */ +export class KMXPlusFileFormat extends KMXFile { + + /* KMXPlus file structures */ + + public readonly COMP_PLUS_SECT_ITEM: any; + public readonly COMP_PLUS_SECT: any; + + // COMP_PLUS_BKSP == COMP_PLUS_TRAN + public readonly COMP_PLUS_BKSP_ITEM: any; + public readonly COMP_PLUS_BKSP: any; + + public readonly COMP_PLUS_DISP_ITEM_v17: any; + public readonly COMP_PLUS_DISP_v17: any; + public readonly COMP_PLUS_DISP_ITEM_v19: any; + public readonly COMP_PLUS_DISP_v19: any; + + public readonly COMP_PLUS_ELEM_ELEMENT: any; + public readonly COMP_PLUS_ELEM_STRING: any; + public readonly COMP_PLUS_ELEM: any; + + // COMP_PLUS_KEYS is now COMP_PLUS_KEYS_KMAP + + public readonly COMP_PLUS_LAYR_ENTRY: any; + public readonly COMP_PLUS_LAYR_KEY: any; + public readonly COMP_PLUS_LAYR_FORM_v17: any; + public readonly COMP_PLUS_LAYR_FORM_v19: any; + public readonly COMP_PLUS_LAYR_ROW: any; + public readonly COMP_PLUS_LAYR_v17: any; + public readonly COMP_PLUS_LAYR_v19: any; + + public readonly COMP_PLUS_KEYS_FLICK: any; + public readonly COMP_PLUS_KEYS_FLICKS: any; + public readonly COMP_PLUS_KEYS_KEY: any; + public readonly COMP_PLUS_KEYS_KMAP: any; + public readonly COMP_PLUS_KEYS: any; + + public readonly COMP_PLUS_LIST_LIST: any; + public readonly COMP_PLUS_LIST_INDEX: any; + public readonly COMP_PLUS_LIST: any; + + public readonly COMP_PLUS_LOCA_ITEM: any; + public readonly COMP_PLUS_LOCA: any; + + public readonly COMP_PLUS_META: any; + + public readonly COMP_PLUS_STRS_ITEM: any; + public readonly COMP_PLUS_STRS: any; + + public readonly COMP_PLUS_TRAN_GROUP: any; + public readonly COMP_PLUS_TRAN_TRANSFORM: any; + public readonly COMP_PLUS_TRAN_REORDER: any; + public readonly COMP_PLUS_TRAN: any; + + public readonly COMP_PLUS_USET_USET: any; + public readonly COMP_PLUS_USET_RANGE: any; + public readonly COMP_PLUS_USET: any; + + public readonly COMP_PLUS_VKEY_ITEM: any; + public readonly COMP_PLUS_VKEY: any; + + public readonly COMP_PLUS_VARS: any; + public readonly COMP_PLUS_VARS_ITEM: any; + + private readonly COMP_PLUS_SectionHeader: any; + + constructor(public readonly version: KMXPlusVersion) { + super(); + // Binary-correct structures matching kmx_plus.h + + if(![KMXPlusVersion.Version17, KMXPlusVersion.Version19].includes(version)) { + throw new Error(`Support for version ${version} not implemented`); + } + + // helpers + const STR_REF = r.uint32le; + const ELEM_REF = r.uint32le; + const LIST_REF = r.uint32le; + const STR_OR_CHAR32 = r.uint32le; + const CHAR32 = r.uint32le; + const STR_OR_CHAR32_OR_USET = r.uint32le; + const IDENT = r.uint32le; + + // Section header - version dependent + + if(version == KMXPlusVersion.Version17) { + this.COMP_PLUS_SectionHeader = new r.Struct({ + ident: IDENT, + size: r.uint32le, + }); + } else { + this.COMP_PLUS_SectionHeader = new r.Struct({ + ident: IDENT, + size: r.uint32le, + version: r.uint32le, + }); + } + + // 'sect' + + this.COMP_PLUS_SECT_ITEM = new r.Struct({ + sect: r.uint32le, + offset: r.uint32le //? new r.VoidPointer(r.uint32le, {type: 'global'}) + }); + + this.COMP_PLUS_SECT = new r.Struct({ + header: this.COMP_PLUS_SectionHeader, + total: r.uint32le, + count: r.uint32le, + items: new r.Array(this.COMP_PLUS_SECT_ITEM, 'count') + }); + + // 'bksp' - see 'tran' + + // 'disp' + this.COMP_PLUS_DISP_ITEM_v17 = new r.Struct({ + to: STR_REF, + id: STR_REF, + display: STR_REF, + }); + + this.COMP_PLUS_DISP_v17 = new r.Struct({ + header: this.COMP_PLUS_SectionHeader, + count: r.uint32le, + baseCharacter: CHAR32, + items: new r.Array(this.COMP_PLUS_DISP_ITEM_v17, 'count'), + }); + + this.COMP_PLUS_DISP_ITEM_v19 = new r.Struct({ + toId: STR_REF, + display: STR_REF, + flags: r.uint32le, + }); + + this.COMP_PLUS_DISP_v19 = new r.Struct({ + header: this.COMP_PLUS_SectionHeader, + count: r.uint32le, + baseCharacter: CHAR32, + items: new r.Array(this.COMP_PLUS_DISP_ITEM_v19, 'count'), + }); + + // 'elem' + + this.COMP_PLUS_ELEM_ELEMENT = new r.Struct({ + element: STR_OR_CHAR32_OR_USET, + flags: r.uint32le + }); + + this.COMP_PLUS_ELEM_STRING = new r.Struct({ + offset: r.uint32le, + length: r.uint32le + }); + + this.COMP_PLUS_ELEM = new r.Struct({ + header: this.COMP_PLUS_SectionHeader, + count: r.uint32le, + strings: new r.Array(this.COMP_PLUS_ELEM_STRING, 'count') + // + variable subtable: Element data (see KMXPlusBuilder.emitElements()) + }); + + // 'finl' - see 'tran' + + // 'keys' - see 'keys.kmap' + + // 'layr' + + this.COMP_PLUS_LAYR_ENTRY = new r.Struct({ + id: r.uint32le, // str + mod: r.uint32le, // bitfield + row: r.uint32le, // index into rows + count: r.uint32le, + }); + + this.COMP_PLUS_LAYR_KEY = new r.Struct({ + key: r.uint32le, // str: key id + }); + + this.COMP_PLUS_LAYR_FORM_v17 = new r.Struct({ + hardware: STR_REF, // str: hardware name + layer: r.uint32le, // index into layers + count: r.uint32le, + minDeviceWidth: r.uint32le, // integer: millimeters + }); + + this.COMP_PLUS_LAYR_FORM_v19 = new r.Struct({ + hardware: STR_REF, // str: hardware name + layer: r.uint32le, // index into layers + count: r.uint32le, + minDeviceWidth: r.uint32le, // integer: millimeters + baseLayout: STR_REF, // v19: str: identifier for base layout (reserved) + fontFaceName: STR_REF, // v19: str: font face name + fontSizePct: r.uint32le, // v19: font size in % of default size + flags: r.uint32le, // v19: flags + }); + + this.COMP_PLUS_LAYR_ROW = new r.Struct({ + key: r.uint32le, + count: r.uint32le, + }); + + this.COMP_PLUS_LAYR_v17 = new r.Struct({ + header: this.COMP_PLUS_SectionHeader, + formCount: r.uint32le, + layerCount: r.uint32le, + rowCount: r.uint32le, + keyCount: r.uint32le, + forms: new r.Array(this.COMP_PLUS_LAYR_FORM_v17, 'formCount'), + layers: new r.Array(this.COMP_PLUS_LAYR_ENTRY, 'layerCount'), + rows: new r.Array(this.COMP_PLUS_LAYR_ROW, 'rowCount'), + keys: new r.Array(this.COMP_PLUS_LAYR_KEY, 'keyCount'), + }); + + this.COMP_PLUS_LAYR_v19 = new r.Struct({ + header: this.COMP_PLUS_SectionHeader, + formCount: r.uint32le, + layerCount: r.uint32le, + rowCount: r.uint32le, + keyCount: r.uint32le, + forms: new r.Array(this.COMP_PLUS_LAYR_FORM_v19, 'formCount'), + layers: new r.Array(this.COMP_PLUS_LAYR_ENTRY, 'layerCount'), + rows: new r.Array(this.COMP_PLUS_LAYR_ROW, 'rowCount'), + keys: new r.Array(this.COMP_PLUS_LAYR_KEY, 'keyCount'), + }); + + // 'keys' + + this.COMP_PLUS_KEYS_FLICK = new r.Struct({ + directions: LIST_REF, // list + to: STR_OR_CHAR32, // str | codepoint + }); + + this.COMP_PLUS_KEYS_FLICKS = new r.Struct({ + count: r.uint32le, + flick: r.uint32le, + id: STR_REF, // str + }); + + this.COMP_PLUS_KEYS_KEY = new r.Struct({ + to: STR_OR_CHAR32, // str | codepoint + flags: r.uint32le, + id: STR_REF, // str + switch: STR_REF, // str + width: r.uint32le, // width*10 ( 1 = 0.1 keys) + longPress: LIST_REF, // list index + longPressDefault: STR_REF, // str + multiTap: LIST_REF, // list index + flicks: r.uint32le, // index into flicks table + }); + + this.COMP_PLUS_KEYS_KMAP = new r.Struct({ + vkey: r.uint32le, + mod: r.uint32le, + key: r.uint32le, // index into 'keys' subtable + }); + + this.COMP_PLUS_KEYS = new r.Struct({ + header: this.COMP_PLUS_SectionHeader, + keyCount: r.uint32le, + flicksCount: r.uint32le, + flickCount: r.uint32le, + kmapCount: r.uint32le, + keys: new r.Array(this.COMP_PLUS_KEYS_KEY, 'keyCount'), + flicks: new r.Array(this.COMP_PLUS_KEYS_FLICKS, 'flicksCount'), + flick: new r.Array(this.COMP_PLUS_KEYS_FLICK, 'flickCount'), + kmap: new r.Array(this.COMP_PLUS_KEYS_KMAP, 'kmapCount'), + }); + + // 'list' + + this.COMP_PLUS_LIST_LIST = new r.Struct({ + index: r.uint32le, + count: r.uint32le, + }); + + this.COMP_PLUS_LIST_INDEX = new r.Struct({ + str: STR_REF, // str + }); + + this.COMP_PLUS_LIST = new r.Struct({ + header: this.COMP_PLUS_SectionHeader, + listCount: r.uint32le, + indexCount: r.uint32le, + lists: new r.Array(this.COMP_PLUS_LIST_LIST, 'listCount'), + indices: new r.Array(this.COMP_PLUS_LIST_INDEX, 'indexCount'), + }); + + // 'loca' + + this.COMP_PLUS_LOCA_ITEM = r.uint32le; //str + + this.COMP_PLUS_LOCA = new r.Struct({ + header: this.COMP_PLUS_SectionHeader, + count: r.uint32le, + items: new r.Array(this.COMP_PLUS_LOCA_ITEM, 'count') + }); + + // 'meta' + + this.COMP_PLUS_META = new r.Struct({ + header: this.COMP_PLUS_SectionHeader, + author: STR_REF, //str + conform: STR_REF, //str + layout: STR_REF, //str + name: STR_REF, //str + indicator: STR_REF, //str + version: STR_REF, //str + settings: r.uint32le, //new r.Bitfield(r.uint32le, ['normalizationDisabled']) + }); + + // 'name' is gone + + // 'ordr' now part of 'tran' + + // 'strs' + + this.COMP_PLUS_STRS_ITEM = new r.Struct({ + // While we use length which is number of utf-16 code units excluding null terminator, + // we always write a null terminator, so we can get restructure to do that for us here + offset: r.uint32le, //? new r.Pointer(r.uint32le, new r.String(null, 'utf16le')), + length: r.uint32le + }); + + this.COMP_PLUS_STRS = new r.Struct({ + header: this.COMP_PLUS_SectionHeader, + count: r.uint32le, + items: new r.Array(this.COMP_PLUS_STRS_ITEM, 'count') + // + variable subtable: String data (see KMXPlusBuilder.emitStrings()) + }); + + // 'tran' + + this.COMP_PLUS_TRAN_GROUP = new r.Struct({ + type: r.uint32le, //type of group + count: r.uint32le, //number of items + index: r.uint32le, //index into subtable + }); + + this.COMP_PLUS_TRAN_TRANSFORM = new r.Struct({ + from: STR_REF, //str + to: STR_REF, //str + mapFrom: ELEM_REF, //elem + mapTo: ELEM_REF //elem + }); + + this.COMP_PLUS_TRAN_REORDER = new r.Struct({ + elements: ELEM_REF, //elem + before: ELEM_REF, //elem + }); + + this.COMP_PLUS_TRAN = new r.Struct({ + header: this.COMP_PLUS_SectionHeader, + groupCount: r.uint32le, + transformCount: r.uint32le, + reorderCount: r.uint32le, + groups: new r.Array(this.COMP_PLUS_TRAN_GROUP, 'groupCount'), + transforms: new r.Array(this.COMP_PLUS_TRAN_TRANSFORM, 'transformCount'), + reorders: new r.Array(this.COMP_PLUS_TRAN_REORDER, 'reorderCount'), + }); + + // 'uset' + + this.COMP_PLUS_USET_USET = new r.Struct({ + range: r.uint32le, + count: r.uint32le, + pattern: STR_REF, // str + }); + + this.COMP_PLUS_USET_RANGE = new r.Struct({ + start: CHAR32, + end: CHAR32, + }); + + this.COMP_PLUS_USET = new r.Struct({ + header: this.COMP_PLUS_SectionHeader, + usetCount: r.uint32le, + rangeCount: r.uint32le, + usets: new r.Array(this.COMP_PLUS_USET_USET, 'usetCount'), + ranges: new r.Array(this.COMP_PLUS_USET_RANGE, 'rangeCount'), + }); + + // 'vars' + + this.COMP_PLUS_VARS_ITEM = new r.Struct({ + type: r.uint32le, + id: STR_REF, // str + value: STR_REF, // str + elem: ELEM_REF, + }); + + this.COMP_PLUS_VARS = new r.Struct({ + header: this.COMP_PLUS_SectionHeader, + markers: LIST_REF, + varCount: r.uint32le, + varEntries: new r.Array(this.COMP_PLUS_VARS_ITEM, 'varCount'), + }); + + // 'vkey' is removed + + // Aliases + + this.COMP_PLUS_BKSP = this.COMP_PLUS_TRAN; + } +} diff --git a/common/web/types/src/kmx/kmx-plus/kmx-plus.ts b/common/web/types/src/kmx/kmx-plus/kmx-plus.ts index 4fd5dcd6320..2aa6a06a840 100644 --- a/common/web/types/src/kmx/kmx-plus/kmx-plus.ts +++ b/common/web/types/src/kmx/kmx-plus/kmx-plus.ts @@ -1,9 +1,12 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * KMX+ file format structures and helper functions + */ import { constants } from '@keymanapp/ldml-keyboard-constants'; -import * as r from 'restructure'; import { ElementString } from './element-string.js'; import { ListItem } from '../../ldml-keyboard/string-list.js'; import * as util from '../../util/util.js'; -import * as KMX from '../kmx.js'; import { UnicodeSetParser, UnicodeSet } from '../../ldml-keyboard/unicodeset-parser-api.js'; import { VariableParser } from '../../ldml-keyboard/pattern-parser.js'; import { MarkerParser } from '../../ldml-keyboard/pattern-parser.js'; @@ -12,7 +15,7 @@ import isOneChar = util.isOneChar; import toOneChar = util.toOneChar; import unescapeString = util.unescapeString; import escapeStringForRegex = util.escapeStringForRegex; -import KMXFile = KMX.KMXFile; +import { KMXPlusFileFormat } from './kmx-plus-file.js'; // Implementation of file structures from /core/src/ldml/C7043_ldml.md // Writer in kmx-builder.ts @@ -480,11 +483,93 @@ export class Bksp extends Tran { } }; +export enum DispItemFlags { + isId = constants.disp_item_flags_is_id, + isSvg = constants.disp_item_flags_is_svg, + + maskHint = constants.disp_item_flags_mask_hint, + + hintPrimary = constants.disp_item_hint_primary << constants.disp_item_flags_shift_hint, + hintNW = constants.disp_item_hint_nw << constants.disp_item_flags_shift_hint, + hintN = constants.disp_item_hint_n << constants.disp_item_flags_shift_hint, + hintNE = constants.disp_item_hint_ne << constants.disp_item_flags_shift_hint, + hintW = constants.disp_item_hint_w << constants.disp_item_flags_shift_hint, + hintE = constants.disp_item_hint_e << constants.disp_item_flags_shift_hint, + hintSW = constants.disp_item_hint_sw << constants.disp_item_flags_shift_hint, + hintS = constants.disp_item_hint_s << constants.disp_item_flags_shift_hint, + hintSE = constants.disp_item_hint_se << constants.disp_item_flags_shift_hint, + + maskKeyCapType = constants.disp_item_flags_mask_key_cap_type, + + keyCapShift = constants.disp_key_cap_shift << constants.disp_item_flags_shift_key_cap_type, + keyCapEnter = constants.disp_key_cap_enter << constants.disp_item_flags_shift_key_cap_type, + keyCapTab = constants.disp_key_cap_tab << constants.disp_item_flags_shift_key_cap_type, + keyCapBksp = constants.disp_key_cap_bksp << constants.disp_item_flags_shift_key_cap_type, + keyCapMenu = constants.disp_key_cap_menu << constants.disp_item_flags_shift_key_cap_type, + keyCapHide = constants.disp_key_cap_hide << constants.disp_item_flags_shift_key_cap_type, + keyCapAlt = constants.disp_key_cap_alt << constants.disp_item_flags_shift_key_cap_type, + keyCapCtrl = constants.disp_key_cap_ctrl << constants.disp_item_flags_shift_key_cap_type, + keyCapCaps = constants.disp_key_cap_caps << constants.disp_item_flags_shift_key_cap_type, + keyCapAbc_Upper = constants.disp_key_cap_abc_upper << constants.disp_item_flags_shift_key_cap_type, + keyCapAbc_Lower = constants.disp_key_cap_abc_lower << constants.disp_item_flags_shift_key_cap_type, + keyCap123 = constants.disp_key_cap_123 << constants.disp_item_flags_shift_key_cap_type, + keyCapSymbol = constants.disp_key_cap_symbol << constants.disp_item_flags_shift_key_cap_type, + keyCapCurrency = constants.disp_key_cap_currency << constants.disp_item_flags_shift_key_cap_type, + keyCapShifted = constants.disp_key_cap_shifted << constants.disp_item_flags_shift_key_cap_type, + keyCapAltgr = constants.disp_key_cap_altgr << constants.disp_item_flags_shift_key_cap_type, + keyCapTableft = constants.disp_key_cap_tableft << constants.disp_item_flags_shift_key_cap_type, + keyCapLalt = constants.disp_key_cap_lalt << constants.disp_item_flags_shift_key_cap_type, + keyCapRalt = constants.disp_key_cap_ralt << constants.disp_item_flags_shift_key_cap_type, + keyCapLctrl = constants.disp_key_cap_lctrl << constants.disp_item_flags_shift_key_cap_type, + keyCapRctrl = constants.disp_key_cap_rctrl << constants.disp_item_flags_shift_key_cap_type, + keyCapLaltctrl = constants.disp_key_cap_laltctrl << constants.disp_item_flags_shift_key_cap_type, + keyCapRaltctrl = constants.disp_key_cap_raltctrl << constants.disp_item_flags_shift_key_cap_type, + keyCapLaltctrlshift = constants.disp_key_cap_laltctrlshift << constants.disp_item_flags_shift_key_cap_type, + keyCapRaltctrlshift = constants.disp_key_cap_raltctrlshift << constants.disp_item_flags_shift_key_cap_type, + keyCapAltshift = constants.disp_key_cap_altshift << constants.disp_item_flags_shift_key_cap_type, + keyCapCtrlshift = constants.disp_key_cap_ctrlshift << constants.disp_item_flags_shift_key_cap_type, + keyCapAltctrlshift = constants.disp_key_cap_altctrlshift << constants.disp_item_flags_shift_key_cap_type, + keyCapLaltshift = constants.disp_key_cap_laltshift << constants.disp_item_flags_shift_key_cap_type, + keyCapRaltshift = constants.disp_key_cap_raltshift << constants.disp_item_flags_shift_key_cap_type, + keyCapLctrlshift = constants.disp_key_cap_lctrlshift << constants.disp_item_flags_shift_key_cap_type, + keyCapRctrlshift = constants.disp_key_cap_rctrlshift << constants.disp_item_flags_shift_key_cap_type, + keyCapLtrenter = constants.disp_key_cap_ltrenter << constants.disp_item_flags_shift_key_cap_type, + keyCapLtrbksp = constants.disp_key_cap_ltrbksp << constants.disp_item_flags_shift_key_cap_type, + keyCapRtlenter = constants.disp_key_cap_rtlenter << constants.disp_item_flags_shift_key_cap_type, + keyCapRtlbksp = constants.disp_key_cap_rtlbksp << constants.disp_item_flags_shift_key_cap_type, + keyCapShiftlock = constants.disp_key_cap_shiftlock << constants.disp_item_flags_shift_key_cap_type, + keyCapShiftedlock = constants.disp_key_cap_shiftedlock << constants.disp_item_flags_shift_key_cap_type, + keyCapZwnj = constants.disp_key_cap_zwnj << constants.disp_item_flags_shift_key_cap_type, + keyCapZwnjios = constants.disp_key_cap_zwnjios << constants.disp_item_flags_shift_key_cap_type, + keyCapZwnjandroid = constants.disp_key_cap_zwnjandroid << constants.disp_item_flags_shift_key_cap_type, + keyCapZwnjgeneric = constants.disp_key_cap_zwnjgeneric << constants.disp_item_flags_shift_key_cap_type, + keyCapSp = constants.disp_key_cap_sp << constants.disp_item_flags_shift_key_cap_type, + keyCapNbsp = constants.disp_key_cap_nbsp << constants.disp_item_flags_shift_key_cap_type, + keyCapNarnbsp = constants.disp_key_cap_narnbsp << constants.disp_item_flags_shift_key_cap_type, + keyCapEnq = constants.disp_key_cap_enq << constants.disp_item_flags_shift_key_cap_type, + keyCapEmq = constants.disp_key_cap_emq << constants.disp_item_flags_shift_key_cap_type, + keyCapEnsp = constants.disp_key_cap_ensp << constants.disp_item_flags_shift_key_cap_type, + keyCapEmsp = constants.disp_key_cap_emsp << constants.disp_item_flags_shift_key_cap_type, + keyCapPunctsp = constants.disp_key_cap_punctsp << constants.disp_item_flags_shift_key_cap_type, + keyCapThsp = constants.disp_key_cap_thsp << constants.disp_item_flags_shift_key_cap_type, + keyCapHsp = constants.disp_key_cap_hsp << constants.disp_item_flags_shift_key_cap_type, + keyCapZwsp = constants.disp_key_cap_zwsp << constants.disp_item_flags_shift_key_cap_type, + keyCapZwj = constants.disp_key_cap_zwj << constants.disp_item_flags_shift_key_cap_type, + keyCapWj = constants.disp_key_cap_wj << constants.disp_item_flags_shift_key_cap_type, + keyCapCgj = constants.disp_key_cap_cgj << constants.disp_item_flags_shift_key_cap_type, + keyCapLtrm = constants.disp_key_cap_ltrm << constants.disp_item_flags_shift_key_cap_type, + keyCapRtlm = constants.disp_key_cap_rtlm << constants.disp_item_flags_shift_key_cap_type, + keyCapSh = constants.disp_key_cap_sh << constants.disp_item_flags_shift_key_cap_type, + keyCapHtab = constants.disp_key_cap_htab << constants.disp_item_flags_shift_key_cap_type, +} + // 'disp' -export class DispItem { - to: StrsItem; - id: StrsItem; +export interface DispItem { + to: StrsItem; // not used in v19 + id: StrsItem; // not used in v19 display: StrsItem; + toId: StrsItem; // v19 + flags: DispItemFlags; // v19 }; export class Disp extends Section { @@ -494,13 +579,31 @@ export class Disp extends Section { // 'layr' +export enum LayrFormFlags { + showBaseLayout = constants.layr_form_flags_show_base_layout, + chiralSeparate = constants.layr_form_flags_chiral_separate, +}; + +export enum LayrFormHardware { + touch = 'touch', // layr_form_hardware_touch + abnt2 = 'abnt2', // layr_form_hardware_abnt2 + iso = 'iso', // layr_form_hardware_iso + jis = 'jis', // layr_form_hardware_jis + ks = 'ks', // layr_form_hardware_ks + us = 'us', // layr_form_hardware_us +}; + /** * In-memory `` */ -export class LayrList { +export class LayrForm { hardware: StrsItem; layers: LayrEntry[] = []; minDeviceWidth: number; // millimeters + baseLayout: StrsItem; // v19 + fontFaceName: StrsItem; // v19 + fontSizePct: number; // v19 (integer percentage) + flags: LayrFormFlags; // v19 }; /** @@ -520,11 +623,23 @@ export class LayrList { }; export class Layr extends Section { - lists: LayrList[] = []; + forms: LayrForm[] = []; +}; + +export enum KeysKeysFlags { + /** + * 0 if to is a char, 1 if it is a string + */ + extend = constants.keys_key_flags_extend, + + /** + * 1 if the key is a gap + */ + gap = constants.keys_key_flags_gap, }; export class KeysKeys { - flags: number; + flags: KeysKeysFlags; flicks: string; // for in-memory only id: StrsItem; longPress: ListItem; @@ -613,8 +728,12 @@ export class List extends Section { export { ListItem as ListItem }; +/** + * In-memory representation of KMX+ data. See also `KMXPlusFileFormat` and + * `KMXPlusFile`. + */ export interface KMXPlusData { - sect?: Strs; // sect is ignored in-memory + sect?: Sect; // sect is ignored in-memory bksp?: Bksp; disp?: Disp; elem?: Elem; // elem is ignored in-memory @@ -629,356 +748,7 @@ export interface KMXPlusData { vars?: Vars; }; -export class KMXPlusFile extends KMXFile { - - /* KMXPlus file structures */ - - public readonly COMP_PLUS_SECT_ITEM: any; - public readonly COMP_PLUS_SECT: any; - - // COMP_PLUS_BKSP == COMP_PLUS_TRAN - public readonly COMP_PLUS_BKSP_ITEM: any; - public readonly COMP_PLUS_BKSP: any; - - public readonly COMP_PLUS_DISP_ITEM: any; - public readonly COMP_PLUS_DISP: any; - - public readonly COMP_PLUS_ELEM_ELEMENT: any; - public readonly COMP_PLUS_ELEM_STRING: any; - public readonly COMP_PLUS_ELEM: any; - - // COMP_PLUS_KEYS is now COMP_PLUS_KEYS_KMAP - - public readonly COMP_PLUS_LAYR_ENTRY: any; - public readonly COMP_PLUS_LAYR_KEY: any; - public readonly COMP_PLUS_LAYR_LIST: any; - public readonly COMP_PLUS_LAYR_ROW: any; - public readonly COMP_PLUS_LAYR: any; - - public readonly COMP_PLUS_KEYS_FLICK: any; - public readonly COMP_PLUS_KEYS_FLICKS: any; - public readonly COMP_PLUS_KEYS_KEY: any; - public readonly COMP_PLUS_KEYS_KMAP: any; - public readonly COMP_PLUS_KEYS: any; - - public readonly COMP_PLUS_LIST_LIST: any; - public readonly COMP_PLUS_LIST_INDEX: any; - public readonly COMP_PLUS_LIST: any; - - public readonly COMP_PLUS_LOCA_ITEM: any; - public readonly COMP_PLUS_LOCA: any; - - public readonly COMP_PLUS_META: any; - - public readonly COMP_PLUS_STRS_ITEM: any; - public readonly COMP_PLUS_STRS: any; - - public readonly COMP_PLUS_TRAN_GROUP: any; - public readonly COMP_PLUS_TRAN_TRANSFORM: any; - public readonly COMP_PLUS_TRAN_REORDER: any; - public readonly COMP_PLUS_TRAN: any; - - public readonly COMP_PLUS_USET_USET: any; - public readonly COMP_PLUS_USET_RANGE: any; - public readonly COMP_PLUS_USET: any; - - public readonly COMP_PLUS_VKEY_ITEM: any; - public readonly COMP_PLUS_VKEY: any; - - public readonly COMP_PLUS_VARS: any; - public readonly COMP_PLUS_VARS_ITEM: any; - +export class KMXPlusFile extends KMXPlusFileFormat { /* File in-memory data */ - public kmxplus: KMXPlusData = { }; - - constructor() { - super(); - // Binary-correct structures matching kmx_plus.h - - // helpers - const STR_REF = r.uint32le; - const ELEM_REF = r.uint32le; - const LIST_REF = r.uint32le; - const STR_OR_CHAR32 = r.uint32le; - const CHAR32 = r.uint32le; - const STR_OR_CHAR32_OR_USET = r.uint32le; - const IDENT = r.uint32le; - // 'sect' - - this.COMP_PLUS_SECT_ITEM = new r.Struct({ - sect: r.uint32le, - offset: r.uint32le //? new r.VoidPointer(r.uint32le, {type: 'global'}) - }); - - this.COMP_PLUS_SECT = new r.Struct({ - ident: IDENT, - size: r.uint32le, - total: r.uint32le, - count: r.uint32le, - items: new r.Array(this.COMP_PLUS_SECT_ITEM, 'count') - }); - - // 'bksp' - see 'tran' - - // 'disp' - this.COMP_PLUS_DISP_ITEM = new r.Struct({ - to: STR_REF, - id: STR_REF, - display: STR_REF, - }); - - this.COMP_PLUS_DISP = new r.Struct({ - ident: IDENT, - size: r.uint32le, - count: r.uint32le, - baseCharacter: CHAR32, - items: new r.Array(this.COMP_PLUS_DISP_ITEM, 'count'), - }); - - // 'elem' - - this.COMP_PLUS_ELEM_ELEMENT = new r.Struct({ - element: STR_OR_CHAR32_OR_USET, - flags: r.uint32le - }); - - this.COMP_PLUS_ELEM_STRING = new r.Struct({ - offset: r.uint32le, - length: r.uint32le - }); - - this.COMP_PLUS_ELEM = new r.Struct({ - ident: IDENT, - size: r.uint32le, - count: r.uint32le, - strings: new r.Array(this.COMP_PLUS_ELEM_STRING, 'count') - // + variable subtable: Element data (see KMXPlusBuilder.emitElements()) - }); - - // 'finl' - see 'tran' - - // 'keys' - see 'keys.kmap' - - // 'layr' - - this.COMP_PLUS_LAYR_ENTRY = new r.Struct({ - id: r.uint32le, // str - mod: r.uint32le, // bitfield - row: r.uint32le, // index into rows - count: r.uint32le, - }); - - this.COMP_PLUS_LAYR_KEY = new r.Struct({ - key: r.uint32le, // str: key id - }); - - this.COMP_PLUS_LAYR_LIST = new r.Struct({ - hardware: STR_REF, // str: hardware name - layer: r.uint32le, // index into layers - count: r.uint32le, - minDeviceWidth: r.uint32le, // integer: millimeters - }); - - this.COMP_PLUS_LAYR_ROW = new r.Struct({ - key: r.uint32le, - count: r.uint32le, - }); - - this.COMP_PLUS_LAYR = new r.Struct({ - ident: IDENT, - size: r.uint32le, - listCount: r.uint32le, - layerCount: r.uint32le, - rowCount: r.uint32le, - keyCount: r.uint32le, - lists: new r.Array(this.COMP_PLUS_LAYR_LIST, 'listCount'), - layers: new r.Array(this.COMP_PLUS_LAYR_ENTRY, 'layerCount'), - rows: new r.Array(this.COMP_PLUS_LAYR_ROW, 'rowCount'), - keys: new r.Array(this.COMP_PLUS_LAYR_KEY, 'keyCount'), - }); - - this.COMP_PLUS_KEYS_FLICK = new r.Struct({ - directions: LIST_REF, // list - to: STR_OR_CHAR32, // str | codepoint - }); - - this.COMP_PLUS_KEYS_FLICKS = new r.Struct({ - count: r.uint32le, - flick: r.uint32le, - id: STR_REF, // str - }); - - this.COMP_PLUS_KEYS_KEY = new r.Struct({ - to: STR_OR_CHAR32, // str | codepoint - flags: r.uint32le, - id: STR_REF, // str - switch: STR_REF, // str - width: r.uint32le, // width*10 ( 1 = 0.1 keys) - longPress: LIST_REF, // list index - longPressDefault: STR_REF, // str - multiTap: LIST_REF, // list index - flicks: r.uint32le, // index into flicks table - }); - - this.COMP_PLUS_KEYS_KMAP = new r.Struct({ - vkey: r.uint32le, - mod: r.uint32le, - key: r.uint32le, // index into 'keys' subtable - }); - - this.COMP_PLUS_KEYS = new r.Struct({ - ident: IDENT, - size: r.uint32le, - keyCount: r.uint32le, - flicksCount: r.uint32le, - flickCount: r.uint32le, - kmapCount: r.uint32le, - keys: new r.Array(this.COMP_PLUS_KEYS_KEY, 'keyCount'), - flicks: new r.Array(this.COMP_PLUS_KEYS_FLICKS, 'flicksCount'), - flick: new r.Array(this.COMP_PLUS_KEYS_FLICK, 'flickCount'), - kmap: new r.Array(this.COMP_PLUS_KEYS_KMAP, 'kmapCount'), - }); - - // 'list' - - this.COMP_PLUS_LIST_LIST = new r.Struct({ - index: r.uint32le, - count: r.uint32le, - }); - - this.COMP_PLUS_LIST_INDEX = new r.Struct({ - str: STR_REF, // str - }); - - this.COMP_PLUS_LIST = new r.Struct({ - ident: IDENT, - size: r.uint32le, - listCount: r.uint32le, - indexCount: r.uint32le, - lists: new r.Array(this.COMP_PLUS_LIST_LIST, 'listCount'), - indices: new r.Array(this.COMP_PLUS_LIST_INDEX, 'indexCount'), - }); - - // 'loca' - - this.COMP_PLUS_LOCA_ITEM = r.uint32le; //str - - this.COMP_PLUS_LOCA = new r.Struct({ - ident: IDENT, - size: r.uint32le, - count: r.uint32le, - items: new r.Array(this.COMP_PLUS_LOCA_ITEM, 'count') - }); - - // 'meta' - - this.COMP_PLUS_META = new r.Struct({ - ident: IDENT, - size: r.uint32le, - author: STR_REF, //str - conform: STR_REF, //str - layout: STR_REF, //str - name: STR_REF, //str - indicator: STR_REF, //str - version: STR_REF, //str - settings: r.uint32le, //new r.Bitfield(r.uint32le, ['normalizationDisabled']) - }); - - // 'name' is gone - - // 'ordr' now part of 'tran' - - // 'strs' - - this.COMP_PLUS_STRS_ITEM = new r.Struct({ - // While we use length which is number of utf-16 code units excluding null terminator, - // we always write a null terminator, so we can get restructure to do that for us here - offset: r.uint32le, //? new r.Pointer(r.uint32le, new r.String(null, 'utf16le')), - length: r.uint32le - }); - - this.COMP_PLUS_STRS = new r.Struct({ - ident: IDENT, - size: r.uint32le, - count: r.uint32le, - items: new r.Array(this.COMP_PLUS_STRS_ITEM, 'count') - // + variable subtable: String data (see KMXPlusBuilder.emitStrings()) - }); - - // 'tran' - - this.COMP_PLUS_TRAN_GROUP = new r.Struct({ - type: r.uint32le, //type of group - count: r.uint32le, //number of items - index: r.uint32le, //index into subtable - }); - - this.COMP_PLUS_TRAN_TRANSFORM = new r.Struct({ - from: STR_REF, //str - to: STR_REF, //str - mapFrom: ELEM_REF, //elem - mapTo: ELEM_REF //elem - }); - - this.COMP_PLUS_TRAN_REORDER = new r.Struct({ - elements: ELEM_REF, //elem - before: ELEM_REF, //elem - }); - - this.COMP_PLUS_TRAN = new r.Struct({ - ident: IDENT, - size: r.uint32le, - groupCount: r.uint32le, - transformCount: r.uint32le, - reorderCount: r.uint32le, - groups: new r.Array(this.COMP_PLUS_TRAN_GROUP, 'groupCount'), - transforms: new r.Array(this.COMP_PLUS_TRAN_TRANSFORM, 'transformCount'), - reorders: new r.Array(this.COMP_PLUS_TRAN_REORDER, 'reorderCount'), - }); - - // 'uset' - this.COMP_PLUS_USET_USET = new r.Struct({ - range: r.uint32le, - count: r.uint32le, - pattern: STR_REF, // str - }); - - this.COMP_PLUS_USET_RANGE = new r.Struct({ - start: CHAR32, - end: CHAR32, - }); - - this.COMP_PLUS_USET = new r.Struct({ - ident: IDENT, - size: r.uint32le, - usetCount: r.uint32le, - rangeCount: r.uint32le, - usets: new r.Array(this.COMP_PLUS_USET_USET, 'usetCount'), - ranges: new r.Array(this.COMP_PLUS_USET_RANGE, 'rangeCount'), - }); - - // 'vars' - - this.COMP_PLUS_VARS_ITEM = new r.Struct({ - type: r.uint32le, - id: STR_REF, // str - value: STR_REF, // str - elem: ELEM_REF, - }); - - this.COMP_PLUS_VARS = new r.Struct({ - ident: IDENT, - size: r.uint32le, - markers: LIST_REF, - varCount: r.uint32le, - varEntries: new r.Array(this.COMP_PLUS_VARS_ITEM, 'varCount'), - }); - - // 'vkey' is removed - - // Aliases - - this.COMP_PLUS_BKSP = this.COMP_PLUS_TRAN; - } -} +}; diff --git a/common/web/types/src/kmx/kmx.ts b/common/web/types/src/kmx/kmx.ts index 4f53e29fde1..54732725fd5 100644 --- a/common/web/types/src/kmx/kmx.ts +++ b/common/web/types/src/kmx/kmx.ts @@ -23,9 +23,46 @@ export enum KMX_Version { VERSION_140 = 0x00000E00, VERSION_150 = 0x00000F00, VERSION_160 = 0x00001000, - VERSION_170 = 0x00001100 + VERSION_170 = 0x00001100, + VERSION_190 = 0x00001300, }; +/** + * Convert a version string from 6.0 - current Keyman version into a + * KMX_Version value. Earlier versions are not supported + * @param version + * @returns null if not matched, otherwise a valid KMX_Version + */ +export function versionStringToKmxVersion(version: string): KMX_Version { + // We allow version strings to be 'x.y' or just 'x' + if(typeof version !== 'string') { + return null; + } + if(!/^\d+(\.0)?$/.test(version)) { + return null; + } + + const major = parseInt(version, 10); + if(Number.isNaN(major)) { + return null; + } + + // assuming a reasonable range for Keyman versions for now + if(major < 6 || major > 999) { + return null; + } + + // Version number is 16 bit number with MINOR in lower 8 bits, + // MAJOR in upper 8 bits. In practice, we now only use MAJOR + // version for Keyman versions. + const num = major << 8; + + if(Object.values(KMX_Version).includes(num)) { + return num; + } + + return null; +} export class KEYBOARD { fileVersion?: number; // dwFileVersion (TSS_FILEVERSION) @@ -164,9 +201,10 @@ export class KMXFile { public static readonly VERSION_150 = KMX_Version.VERSION_150; public static readonly VERSION_160 = KMX_Version.VERSION_160; public static readonly VERSION_170 = KMX_Version.VERSION_170; + public static readonly VERSION_190 = KMX_Version.VERSION_190; public static readonly VERSION_MIN = this.VERSION_50; - public static readonly VERSION_MAX = this.VERSION_170; + public static readonly VERSION_MAX = this.VERSION_190; // // Backspace types @@ -337,8 +375,14 @@ export class KMXFile { public static readonly KF_LOGICALLAYOUT = 0x0008; public static readonly KF_AUTOMATICVERSION = 0x0010; - // 16.0: Support for LDML Keyboards in KMXPlus file format - public static readonly KF_KMXPLUS = 0x0020; + /** 16.0+: A `COMP_KEYBOARD_KMXPLUSINFO` structure is present immediately after `COMP_KEYBOARD` */ + public static readonly KF_KMXPLUS = 0x0020; + + /** + * 19.0+: The `COMP_KEYBOARD_KMXPLUSINFO` structure contains only OSK, and not a + * LDML keyboard; KF_KMXPLUS should not be set + */ + public static readonly KF_KMXPLUSOSK = 0x0040; public static readonly HK_ALT = 0x00010000; public static readonly HK_CTRL = 0x00020000; @@ -466,7 +510,7 @@ export class KMXFile { dpGroupArray: r.uint32le, // 0024 [LPGROUP] address of first item in group array StartGroup_ANSI: r.uint32le, // 0028 index of starting ANSI group - StartGroup_Unicode: r.uint32le, // 0028 index of starting Unicode groups + StartGroup_Unicode: r.uint32le, // 002C index of starting Unicode groups dwFlags: r.uint32le, // 0030 Flags for the keyboard file diff --git a/common/web/types/src/main.ts b/common/web/types/src/main.ts index 779af42ba57..924f1095626 100644 --- a/common/web/types/src/main.ts +++ b/common/web/types/src/main.ts @@ -8,9 +8,13 @@ export { default as KvkFileWriter } from './kvk/kvk-file-writer.js'; export * as KvkFile from './kvk/kvk-file.js'; -export { USVirtualKeyCodes } from './consts/virtual-key-constants.js'; +export { USVirtualKeyCodes, usVirtualKeyName } from './consts/virtual-key-constants.js'; export * as Constants from './consts/virtual-key-constants.js'; -export { ModifierKeyConstants } from './consts/modifier-key-constants.js'; +export { + ModifierKeyConstant, ModifierKeyConstants, LDML_MODIFIER_TO_KVK_MODIFIER, KVK_MODIFIER_TO_LDML_MODIFIER, + translateLdmlModifiersToVisualKeyboardShift, translateVisualKeyboardShiftToLdmlModifiers, + visualKeyboardShiftToLayerName, +} from './consts/modifier-key-constants.js'; export * as TouchLayout from './keyman-touch-layout/keyman-touch-layout-file.js'; diff --git a/common/web/types/tests/kmx/kmx-file.tests.ts b/common/web/types/tests/kmx/kmx-file.tests.ts index b50521427c6..ce6fafb56db 100644 --- a/common/web/types/tests/kmx/kmx-file.tests.ts +++ b/common/web/types/tests/kmx/kmx-file.tests.ts @@ -3,7 +3,7 @@ import 'mocha'; import { assert } from 'chai'; import { makePathToFixture } from '../helpers/index.js'; import { KmxFileReader } from "../../src/kmx/kmx-file-reader.js"; -import { KMXFile } from "../../src/kmx/kmx.js"; +import { KMX_Version, KMXFile, versionStringToKmxVersion } from "../../src/kmx/kmx.js"; describe('kmx-file-reader', function () { it('should read a valid file', function() { @@ -27,3 +27,42 @@ describe('kmx-file-reader', function () { // TODO: add header, group, key tests once we have added support in KmxFileReader }); }); + +describe('versionStringToKmxVersion', function() { + [ + // We only care about v6.0 and up these days + {s:'6.0', v:KMX_Version.VERSION_60}, + {s:'6', v:KMX_Version.VERSION_60}, + {s:'7.0', v:KMX_Version.VERSION_70}, + {s:'7', v:KMX_Version.VERSION_70}, + {s:'8.0', v:KMX_Version.VERSION_80}, + {s:'8', v:KMX_Version.VERSION_80}, + {s:'9.0', v:KMX_Version.VERSION_90}, + {s:'9', v:KMX_Version.VERSION_90}, + {s:'10.0', v:KMX_Version.VERSION_100}, + {s:'10', v:KMX_Version.VERSION_100}, + {s:'14.0', v:KMX_Version.VERSION_140}, + {s:'14', v:KMX_Version.VERSION_140}, + {s:'15.0', v:KMX_Version.VERSION_150}, + {s:'15', v:KMX_Version.VERSION_150}, + {s:'16.0', v:KMX_Version.VERSION_160}, + {s:'16', v:KMX_Version.VERSION_160}, + {s:'17.0', v:KMX_Version.VERSION_170}, + {s:'17', v:KMX_Version.VERSION_170}, + {s:'19.0', v:KMX_Version.VERSION_190}, + {s:'19', v:KMX_Version.VERSION_190}, + ].forEach(function(v) { + it(`should convert valid version string ${v.s}`, function() { + const actual = versionStringToKmxVersion(v.s); + assert.equal(actual, v.v); + }); + }); + + ['zero','six','VERSION_60','1.0','-6','','5.0.1','19.0-alpha',null,undefined,] + .forEach(function(v) { + it(`should reject invalid version string '${v}'`, function() { + const actual = versionStringToKmxVersion(v); + assert.isNull(actual); + }); + }); +}); \ No newline at end of file diff --git a/common/windows/cpp/include/legacy_kmx_file.h b/common/windows/cpp/include/legacy_kmx_file.h deleted file mode 100644 index d3bc3aed0f6..00000000000 --- a/common/windows/cpp/include/legacy_kmx_file.h +++ /dev/null @@ -1,424 +0,0 @@ -// TODO: merge and replace with kmx_file.h from core - -/* - Name: legacy_kmx_file - Copyright: Copyright (C) SIL International. - Documentation: - Description: Describes .kmx binary format. To be replaced with common/include/kmx_file.h - Create Date: 4 Jan 2007 - - Modified Date: 24 Aug 2015 - Authors: mcdurdin - Related Files: - Dependencies: - - Bugs: - Todo: - Notes: - History: 04 Jan 2007 - mcdurdin - Add CODE_NOTANY - 22 Mar 2010 - mcdurdin - Compiler tidyup - 25 May 2010 - mcdurdin - I1632 - Keyboard Options - 24 Oct 2013 - mcdurdin - I3933 - V9.0 - Keyman tray icon menu is not showing installed keyboards - 19 Mar 2014 - mcdurdin - I4140 - V9.0 - Add keyboard version information to keyboards - 16 Jun 2014 - mcdurdin - I4271 - V9.0 - Switch language for all applications is not working - 31 Dec 2014 - mcdurdin - I4548 - V9.0 - When Alt is down, release of Ctrl, Shift is not detectable within TIP in some languages - 24 Aug 2015 - mcdurdin - I4865 - Add treat hints and warnings as errors into project - 24 Aug 2015 - mcdurdin - I4866 - Add warn on deprecated features to project and compile - -*/ - -#ifndef _COMPILER_H -#define _COMPILER_H - - - -/* WM_UNICHAR */ - -#define WM_UNICHAR 0x0109 -#define UNICODE_NOCHAR 0xFFFF - -/* */ - -#define KEYMAN_LAYOUT_DEFAULT 0x000005FE - -#define KEYMANID_NONKEYMAN 0xFFFFFFFF -#define KEYMANID_IGNORE 0xFFFFFFFE -#define KEYMANID_INVALID 0xFFFFFFFD - -/* Shift flags for hotkeys (version 1.0) */ - -#define SHIFTFLAG 0x2000 -#define CTRLFLAG 0x4000 -#define ALTFLAG 0x8000 - -/* Miscellaneous flags and defines */ - -#define UM_DRAWICONS 0x01 -#define NUL '\0' - -#define MAXGROUPS 128 - -/* File version identifiers */ - -#define VERSION_30 0x00000300 -#define VERSION_31 0x00000301 -#define VERSION_32 0x00000302 -#define VERSION_40 0x00000400 -#define VERSION_50 0x00000500 -#define VERSION_501 0x00000501 -#define VERSION_60 0x00000600 -#define VERSION_70 0x00000700 -#define VERSION_80 0x00000800 -#define VERSION_90 0x00000900 -#define VERSION_100 0x00000A00 -#define VERSION_140 0x00000E00 -#define VERSION_150 0x00000F00 -#define VERSION_160 0x00001000 -#define VERSION_170 0x00001100 -#define VERSION_MIN VERSION_50 -#define VERSION_MAX VERSION_170 - -/* - Special flag for WM_CHAR/WM_KEY???/WM_SYSKEY???: says that key has been - processed by Keyman. -*/ - -//#define KEYMAN_CHARFLAG 0x0000000L -#define KEYMAN_CHARFLAG 0x02000000L - -#define CHAR_TRANSTATE 0x00000001L // Flag for WM_CHAR: key is down, first repeat -#define KEYUP_TRANSTATE 0xC0000001L // Flag for WM_KEYUP: key is up, first repeat -#define KEYDOWN_TRANSTATE 0x00000001L // Flag for WM_KEYDOWN: key is down, first rpt -#define ALT_TRANSTATE 0x20000000L // Flag for WM_KEYBOARD messages: alt is down - -#define IDM_DISABLEKEY 0xFF00 - -#define WINDOWS_VERSION_3_1 0x030A -#define WINDOWS_VERSION_3_11 0x030B -#define WINDOWS_VERSION_4_0 0x035F -#define WINDOWS_VERSION_95 0x035F - -#define HKLM HKEY_LOCAL_MACHINE -#define HKCU HKEY_CURRENT_USER - -// -// DEBUGINFO states -// -#define KDS_KEYBOARD 0x0001 -#define KDS_PROGRAM 0x0002 -#define KDS_MESSAGE 0x0004 -#define KDS_INTERNAT 0x0008 - -#define KDS_CONTROL 0x8000 - -// -// Backspace flags -// - -// Delete a deadkey from context, do not pass on to app -#define BK_DEFAULT 0 -#define BK_DEADKEY 1 - -// User pressed backspace so clear deadkeys either side of next deleted character -#define BK_BACKSPACE 2 - -// Next character to delete is a Unicode surrogate pair -#define BK_SURROGATE 4 - -/* - A blank key (in a group without "using keys") cannot be '0' as that is - used for error testing and blanking out unused keys and you don't really - want that tested! -*/ - -#define BLANKKEY 0xFF // Blank key - -// Different begin types -#define BEGIN_ANSI 0 -#define BEGIN_UNICODE 1 -#define BEGIN_NEWCONTEXT 2 -#define BEGIN_POSTKEYSTROKE 3 - -//#define lpuch (LPBYTE) - -#define TSS_NONE 0 -#define TSS_BITMAP 1 -#define TSS_COPYRIGHT 2 -#define TSS_HOTKEY 3 -#define TSS_LANGUAGE 4 -#define TSS_LAYOUT 5 -#define TSS_MESSAGE 6 -#define TSS_NAME 7 -#define TSS_VERSION 8 -#define TSS_CAPSONONLY 9 -#define TSS_CAPSALWAYSOFF 10 -#define TSS_SHIFTFREESCAPS 11 -#define TSS_LANGUAGENAME 12 - -#define TSS_CALLDEFINITION 13 -#define TSS_CALLDEFINITION_LOADFAILED 14 - -#define TSS_ETHNOLOGUECODE 15 - -#define TSS_DEBUG_LINE 16 - -#define TSS_MNEMONIC 17 - -#define TSS_INCLUDECODES 18 - -#define TSS_OLDCHARPOSMATCHING 19 - -#define TSS_COMPILEDVERSION 20 -#define TSS_KEYMANCOPYRIGHT 21 - -#define TSS_CUSTOMKEYMANEDITION 22 -#define TSS_CUSTOMKEYMANEDITIONNAME 23 - -/* Keyman 7.0 system stores */ - -#define TSS__KEYMAN_60_MAX 23 - -#define TSS_VISUALKEYBOARD 24 -#define TSS_KMW_RTL 25 -#define TSS_KMW_HELPFILE 26 -#define TSS_KMW_HELPTEXT 27 -#define TSS_KMW_EMBEDJS 28 - -#define TSS_WINDOWSLANGUAGES 29 - -#define TSS__KEYMAN_70_MAX 29 - -/* Keyman 8.0 system stores */ - -#define TSS_COMPARISON 30 - -/* Keyman 9.0 system stores */ - -#define TSS_PLATFORM 31 -#define TSS_BASELAYOUT 32 -#define TSS_LAYER 33 - -#define TSS_PLATFORM_NOMATCH 0x8001 // Reserved for internal use - after platform statement is run, set to either TSS_PLATFORM_NOMATCH or TSS_PLATFORM_MATCH -#define TSS_PLATFORM_MATCH 0x8002 // Reserved for internal use - as the result will never change for the lifetime of the process. - -#define TSS_VKDICTIONARY 34 // Dictionary of virtual key names for v9 dynamic layouts -#define TSS_LAYOUTFILE 35 // Keyman 9 layer-based JSON OSK -#define TSS_KEYBOARDVERSION 36 // &keyboardversion system store // I4140 -#define TSS_KMW_EMBEDCSS 37 - -#define TSS_TARGETS 38 - -#define TSS_CASEDKEYS 39 - -#define TSS__KEYMAN_140_MAX 39 - -#define TSS_BEGIN_NEWCONTEXT 40 -#define TSS_BEGIN_POSTKEYSTROKE 41 - -#define TSS_NEWLAYER 42 -#define TSS_OLDLAYER 43 - -#define TSS__MAX 43 - -/* wm_keyman_control_internal message control codes */ - -#define KMCI_SELECTKEYBOARD 3 // I3933 -#define KMCI_SELECTKEYBOARD_TSF 4 // I3933 -#define KMCI_GETACTIVEKEYBOARD 5 // I3933 -#define KMCI_SETFOREGROUND 6 // I3933 -#define KMCI_SELECTKEYBOARD_BACKGROUND 7 // I4271 -#define KMCI_SELECTKEYBOARD_BACKGROUND_TSF 8 // I4271 - -#define FILEID_COMPILED 0x5354584B - -#define SZMAX_LANGUAGENAME 80 -#define SZMAX_KEYBOARDNAME 80 -#define SZMAX_COPYRIGHT 256 -#define SZMAX_MESSAGE 1024 - -#define UC_SENTINEL 0xFFFF -#define UC_SENTINEL_EXTENDEDEND 0x10 // was ((CODE_LASTCODE)+1)... what was I thinking? - -/* - * VK__MAX defines the highest virtual key code defined in the system = 0xFF. Custom VK codes start at 256 - */ -#define VK__MAX 255 - -#define CODE_ANY 0x01 -#define CODE_INDEX 0x02 -#define CODE_CONTEXT 0x03 -#define CODE_NUL 0x04 -#define CODE_USE 0x05 -#define CODE_RETURN 0x06 -#define CODE_BEEP 0x07 -#define CODE_DEADKEY 0x08 -// 0x09 = bkspace.-- we don't need to keep this separate though with UC_SENTINEL -#define CODE_EXTENDED 0x0A -//#define CODE_EXTENDEDEND 0x0B deprecated -#define CODE_SWITCH 0x0C -#define CODE_KEY 0x0D -#define CODE_CLEARCONTEXT 0x0E -#define CODE_CALL 0x0F -// UC_SENTINEL_EXTENDEDEND 0x10 -#define CODE_CONTEXTEX 0x11 - -#define CODE_NOTANY 0x12 - -#define CODE_KEYMAN70_LASTCODE 0x12 - -#define CODE_SETOPT 0x13 -#define CODE_IFOPT 0x14 -#define CODE_SAVEOPT 0x15 -#define CODE_RESETOPT 0x16 - -#define CODE_KEYMAN80_LASTCODE 0x16 - -/* Keyman 9.0 codes */ - -#define CODE_IFSYSTEMSTORE 0x17 -#define CODE_SETSYSTEMSTORE 0x18 - -#define CODE_LASTCODE 0x18 - -#define KF_SHIFTFREESCAPS 0x0001 -#define KF_CAPSONONLY 0x0002 -#define KF_CAPSALWAYSOFF 0x0004 -#define KF_LOGICALLAYOUT 0x0008 -#define KF_AUTOMATICVERSION 0x0010 - -// 16.0: Support for LDML Keyboards in KMXPlus file format -#define KF_KMXPLUS 0x0020 - -#define HK_ALT 0x00010000 -#define HK_CTRL 0x00020000 -#define HK_SHIFT 0x00040000 - -#define HK_RALT_INVALID 0x00100000 -#define HK_RCTRL_INVALID 0x00200000 -#define HK_RSHIFT_INVALID 0x00400000 - -#define LCTRLFLAG 0x0001 // Left Control flag -#define RCTRLFLAG 0x0002 // Right Control flag -#define LALTFLAG 0x0004 // Left Alt flag -#define RALTFLAG 0x0008 // Right Alt flag -#define K_SHIFTFLAG 0x0010 // Either shift flag -#define K_CTRLFLAG 0x0020 // Either ctrl flag -#define K_ALTFLAG 0x0040 // Either alt flag -//#define K_METAFLAG 0x0080 // Either Meta-key flag (tentative). Not usable in keyboard rules; - // Used internally (currently, only by KMW) to ensure Meta-key - // shortcuts safely bypass rules - // Meta key = Command key on macOS, Windows key on Windows -#define CAPITALFLAG 0x0100 // Caps lock on -#define NOTCAPITALFLAG 0x0200 // Caps lock NOT on -#define NUMLOCKFLAG 0x0400 // Num lock on -#define NOTNUMLOCKFLAG 0x0800 // Num lock NOT on -#define SCROLLFLAG 0x1000 // Scroll lock on -#define NOTSCROLLFLAG 0x2000 // Scroll lock NOT on -#define ISVIRTUALKEY 0x4000 // It is a Virtual Key Sequence -#define VIRTUALCHARKEY 0x8000 // Keyman 6.0: Virtual Key Cap Sequence NOT YET - -#define K_MODIFIERFLAG 0x007F -#define K_NOTMODIFIERFLAG 0xFF00 // I4548 - -// Note: OTHER_MODIFIER = 0x10000, used by KMX+ for the -// other modifier flag in layers, > 16 bit so not available here. -// See keys_mod_other in keyman_core_ldml.ts - -/* - These sanity checks help ensure we don't - break on-disk struct sizes when we cross - compilers, bitness and platforms. They must - correspond to the equivalent constants in - kmxfile.pas. For historical reasons, these - structures have the prefix COMP_ while - the pas versions are TKeyboardFile_, names - which correspond more closely to what the - structures are for. -*/ - -#define KEYBOARDFILEHEADER_SIZE 64 -#define KEYBOARDFILESTORE_SIZE 12 -#define KEYBOARDFILEGROUP_SIZE 24 -#define KEYBOARDFILEKEY_SIZE 20 - -struct COMP_STORE { - DWORD dwSystemID; - DWORD dpName; - DWORD dpString; - }; - -static_assert(sizeof(COMP_STORE) == KEYBOARDFILESTORE_SIZE, "COMP_STORE must be KEYBOARDFILESTORE_SIZE bytes"); - -struct COMP_KEY { - WORD Key; - WORD _reserved; - DWORD Line; - DWORD ShiftFlags; - DWORD dpOutput; - DWORD dpContext; - }; - -static_assert(sizeof(COMP_KEY) == KEYBOARDFILEKEY_SIZE, "COMP_KEY must be KEYBOARDFILEKEY_SIZE bytes"); - -struct COMP_GROUP { - DWORD dpName; - DWORD dpKeyArray; // [LPKEY] address of first item in key array - DWORD dpMatch; - DWORD dpNoMatch; - DWORD cxKeyArray; // in array entries - BOOL fUsingKeys; // group(xx) [using keys] <-- specified or not - }; - -static_assert(sizeof(COMP_GROUP) == KEYBOARDFILEGROUP_SIZE, "COMP_GROUP must be KEYBOARDFILEGROUP_SIZE bytes"); - -struct COMP_KEYBOARD { - DWORD dwIdentifier; // 0000 Keyman compiled keyboard id - - DWORD dwFileVersion; // 0004 Version of the file - Keyman 4.0 is 0x0400 - - DWORD dwCheckSum; // 0008 As stored in keyboard. DEPRECATED as of 16.0 - DWORD KeyboardID; // 000C as stored in HKEY_LOCAL_MACHINE//system//currentcontrolset//control//keyboard layouts - DWORD IsRegistered; // 0010 - DWORD version; // 0014 keyboard version - - DWORD cxStoreArray; // 0018 in array entries - DWORD cxGroupArray; // 001C in array entries - - DWORD dpStoreArray; // 0020 [LPSTORE] address of first item in store array - DWORD dpGroupArray; // 0024 [LPGROUP] address of first item in group array - - DWORD StartGroup[2]; // 0028 index of starting groups [2 of them] - //DWORD StartGroupIndex; // StartGroup current index - - DWORD dwFlags; // 0030 Flags for the keyboard file - - DWORD dwHotKey; // 0034 standard windows hotkey (hiword=shift/ctrl/alt stuff, loword=vkey) - - //DWORD dpName; // offset of name - //DWORD dpLanguageName; // offset of language name; - //DWORD dpCopyright; // offset of copyright - //DWORD dpMessage; // offset of message in Keyboard About box - - DWORD dpBitmapOffset; // 0038 offset of the bitmaps in the file - DWORD dwBitmapSize; // 003C size in bytes of the bitmaps - }; - -static_assert(sizeof(COMP_KEYBOARD) == KEYBOARDFILEHEADER_SIZE, "COMP_KEYBOARD must be KEYBOARDFILEHEADER_SIZE bytes"); - -typedef COMP_KEYBOARD *PCOMP_KEYBOARD; -typedef COMP_STORE *PCOMP_STORE; -typedef COMP_KEY *PCOMP_KEY; -typedef COMP_GROUP *PCOMP_GROUP; - - -typedef struct _COMPILER_OPTIONS { - DWORD dwSize; - BOOL ShouldAddCompilerVersion; -} COMPILER_OPTIONS; - -typedef COMPILER_OPTIONS *PCOMPILER_OPTIONS; - -typedef int (CALLBACK *CompilerMessageProc)(int line, DWORD dwMsgCode, LPSTR szText); - -#endif // _COMPILER_H - diff --git a/common/windows/cpp/include/legacy_kmx_memory.h b/common/windows/cpp/include/legacy_kmx_memory.h index 771a4d57ca9..b0c03f271ea 100644 --- a/common/windows/cpp/include/legacy_kmx_memory.h +++ b/common/windows/cpp/include/legacy_kmx_memory.h @@ -21,41 +21,37 @@ typedef struct tagKEY typedef struct tagGROUP { PWSTR dpName; - LPKEY dpKeyArray; // [LPKEY] address of first item in key array + LPKEY dpKeyArray; // [LPKEY] address of first item in key array PWSTR dpMatch; PWSTR dpNoMatch; - DWORD cxKeyArray; // in array entries - BOOL fUsingKeys; // group(xx) [using keys] <-- specified or not + DWORD cxKeyArray; // in array entries + BOOL fUsingKeys; // group(xx) [using keys] <-- specified or not } GROUP, * LPGROUP; typedef struct tagKEYBOARD { - DWORD dwIdentifier; // Keyman compiled keyboard id + DWORD dwIdentifier; // Keyman compiled keyboard id - DWORD dwFileVersion; // Version of the file - Keyman 4.0 is 0x0400 + DWORD dwFileVersion; // Version of the file - Keyman 4.0 is 0x0400 - DWORD dwCheckSum; // As stored in keyboard. DEPRECATED as of 16.0 - DWORD xxkbdlayout; // as stored in HKEY_LOCAL_MACHINE//system//currentcontrolset//control//keyboard layouts - DWORD IsRegistered; // layout id, from same registry key - DWORD version; // keyboard version + DWORD dwCheckSum; // As stored in keyboard. DEPRECATED as of 16.0 + DWORD xxkbdlayout; // as stored in HKEY_LOCAL_MACHINE//system//currentcontrolset//control//keyboard layouts + DWORD IsRegistered; // layout id, from same registry key + DWORD version; // keyboard version - DWORD cxStoreArray; // in array entries - DWORD cxGroupArray; // in array entries + DWORD cxStoreArray; // in array entries + DWORD cxGroupArray; // in array entries - LPSTORE dpStoreArray; // [LPSTORE] address of first item in store array, from start of file - LPGROUP dpGroupArray; // [LPGROUP] address of first item in group array, from start of file + LPSTORE dpStoreArray; // [LPSTORE] address of first item in store array, from start of file + LPGROUP dpGroupArray; // [LPGROUP] address of first item in group array, from start of file - DWORD StartGroup[2]; // index of starting groups [2 of them] + DWORD StartGroup[2]; // index of starting groups [2 of them] // Ansi=0, Unicode=1 - DWORD dwFlags; // Flags for the keyboard file + DWORD dwFlags; // Flags for the keyboard file - DWORD dwHotKey; // standard windows hotkey (hiword=shift/ctrl/alt stuff, loword=vkey) + DWORD dwHotKey; // standard windows hotkey (hiword=shift/ctrl/alt stuff, loword=vkey) - //PWSTR dpName; // offset of name - //PWSTR dpLanguageName; // offset of language name; - //PWSTR dpCopyright; // offset of copyright - //PWSTR dpMessage; // offset of message in Keyboard About box - - HBITMAP hBitmap; // handle to the bitmap in the file; + DWORD dpBitmapOffset; // 0038 offset of the bitmaps in the file + DWORD dwBitmapSize; // 003C size in bytes of the bitmaps } KEYBOARD, * LPKEYBOARD; diff --git a/common/windows/delphi/keyboards/kmxfileconsts.pas b/common/windows/delphi/keyboards/kmxfileconsts.pas index 8e1955aaa6a..7f901f90efa 100644 --- a/common/windows/delphi/keyboards/kmxfileconsts.pas +++ b/common/windows/delphi/keyboards/kmxfileconsts.pas @@ -99,9 +99,10 @@ interface VERSION_150 = $00000F00; VERSION_160 = $00001000; VERSION_170 = $00001100; + VERSION_190 = $00001300; VERSION_MIN = VERSION_50; - VERSION_MAX = VERSION_170; + VERSION_MAX = VERSION_190; VERSION_MASK_MINOR = $00FF; VERSION_MASK_MAJOR = $FF00; @@ -216,7 +217,9 @@ interface TSS_NEWLAYER = 42; TSS_OLDLAYER = 43; - TSS__MAX = 43; + TSS_DISPLAYMAP = 44; + + TSS__MAX = 44; type TSystemStore = (ssNone = 0, ssBitmap = 1, ssCopyright = 2, ssHotkey = 3, ssLanguage = 4, ssLayout = 5, ssMessage = 6, diff --git a/core/include/ldml/keyman_core_ldml.h b/core/include/ldml/keyman_core_ldml.h index f521cb356fc..ef08f8651b6 100644 --- a/core/include/ldml/keyman_core_ldml.h +++ b/core/include/ldml/keyman_core_ldml.h @@ -21,6 +21,86 @@ #define LDML_CLDR_IMPORT_BASE "cldr" #define LDML_CLDR_TEST_VERSION_LATEST "techpreview" #define LDML_CLDR_VERSION_LATEST "46" +#define LDML_DISP_ITEM_FLAGS_IS_BLANK_KEY 0x20 +#define LDML_DISP_ITEM_FLAGS_IS_DEADKEY 0x10 +#define LDML_DISP_ITEM_FLAGS_IS_FRAME_KEY 0x4 +#define LDML_DISP_ITEM_FLAGS_IS_HIGHLIGHTED 0x8 +#define LDML_DISP_ITEM_FLAGS_IS_ID 0x1 +#define LDML_DISP_ITEM_FLAGS_IS_SVG 0x2 +#define LDML_DISP_ITEM_FLAGS_MASK_FLAGS 0xFFF +#define LDML_DISP_ITEM_FLAGS_MASK_HINT 0xF000 +#define LDML_DISP_ITEM_FLAGS_MASK_KEY_CAP_TYPE 0xFFFF0000 +#define LDML_DISP_ITEM_FLAGS_SHIFT_HINT 0xC +#define LDML_DISP_ITEM_FLAGS_SHIFT_KEY_CAP_TYPE 0x10 +#define LDML_DISP_ITEM_HINT_E 0x5 +#define LDML_DISP_ITEM_HINT_N 0x2 +#define LDML_DISP_ITEM_HINT_NE 0x3 +#define LDML_DISP_ITEM_HINT_NW 0x1 +#define LDML_DISP_ITEM_HINT_PRIMARY 0x0 +#define LDML_DISP_ITEM_HINT_S 0x7 +#define LDML_DISP_ITEM_HINT_SE 0x8 +#define LDML_DISP_ITEM_HINT_SW 0x6 +#define LDML_DISP_ITEM_HINT_W 0x4 +#define LDML_DISP_KEY_CAP_123 0x13 +#define LDML_DISP_KEY_CAP_ABC_LOWER 0x11 +#define LDML_DISP_KEY_CAP_ABC_UPPER 0x10 +#define LDML_DISP_KEY_CAP_ALT 0x19 +#define LDML_DISP_KEY_CAP_ALTCTRLSHIFT 0x66 +#define LDML_DISP_KEY_CAP_ALTGR 0x2 +#define LDML_DISP_KEY_CAP_ALTSHIFT 0x64 +#define LDML_DISP_KEY_CAP_BKSP 0x4 +#define LDML_DISP_KEY_CAP_CAPS 0x3 +#define LDML_DISP_KEY_CAP_CGJ 0x7A +#define LDML_DISP_KEY_CAP_CTRL 0x1 +#define LDML_DISP_KEY_CAP_CTRLSHIFT 0x65 +#define LDML_DISP_KEY_CAP_CURRENCY 0x14 +#define LDML_DISP_KEY_CAP_EMQ 0x85 +#define LDML_DISP_KEY_CAP_EMSP 0x87 +#define LDML_DISP_KEY_CAP_ENQ 0x84 +#define LDML_DISP_KEY_CAP_ENSP 0x86 +#define LDML_DISP_KEY_CAP_ENTER 0x5 +#define LDML_DISP_KEY_CAP_HIDE 0xA +#define LDML_DISP_KEY_CAP_HSP 0x8E +#define LDML_DISP_KEY_CAP_HTAB 0xA2 +#define LDML_DISP_KEY_CAP_LALT 0x56 +#define LDML_DISP_KEY_CAP_LALTCTRL 0x60 +#define LDML_DISP_KEY_CAP_LALTCTRLSHIFT 0x62 +#define LDML_DISP_KEY_CAP_LALTSHIFT 0x67 +#define LDML_DISP_KEY_CAP_LCTRL 0x58 +#define LDML_DISP_KEY_CAP_LCTRLSHIFT 0x69 +#define LDML_DISP_KEY_CAP_LTRBKSP 0x4 +#define LDML_DISP_KEY_CAP_LTRENTER 0x5 +#define LDML_DISP_KEY_CAP_LTRM 0x90 +#define LDML_DISP_KEY_CAP_MENU 0xB +#define LDML_DISP_KEY_CAP_NARNBSP 0x83 +#define LDML_DISP_KEY_CAP_NBSP 0x82 +#define LDML_DISP_KEY_CAP_PUNCTSP 0x8C +#define LDML_DISP_KEY_CAP_RALT 0x57 +#define LDML_DISP_KEY_CAP_RALTCTRL 0x61 +#define LDML_DISP_KEY_CAP_RALTCTRLSHIFT 0x63 +#define LDML_DISP_KEY_CAP_RALTSHIFT 0x68 +#define LDML_DISP_KEY_CAP_RCTRL 0x59 +#define LDML_DISP_KEY_CAP_RCTRLSHIFT 0x70 +#define LDML_DISP_KEY_CAP_RTLBKSP 0x72 +#define LDML_DISP_KEY_CAP_RTLENTER 0x71 +#define LDML_DISP_KEY_CAP_RTLM 0x91 +#define LDML_DISP_KEY_CAP_SH 0xA1 +#define LDML_DISP_KEY_CAP_SHIFT 0x8 +#define LDML_DISP_KEY_CAP_SHIFTED 0x9 +#define LDML_DISP_KEY_CAP_SHIFTEDLOCK 0x74 +#define LDML_DISP_KEY_CAP_SHIFTLOCK 0x73 +#define LDML_DISP_KEY_CAP_SP 0x80 +#define LDML_DISP_KEY_CAP_SYMBOL 0x15 +#define LDML_DISP_KEY_CAP_TAB 0x6 +#define LDML_DISP_KEY_CAP_TABLEFT 0x7 +#define LDML_DISP_KEY_CAP_THSP 0x8D +#define LDML_DISP_KEY_CAP_WJ 0x78 +#define LDML_DISP_KEY_CAP_ZWJ 0x77 +#define LDML_DISP_KEY_CAP_ZWNJ 0x75 +#define LDML_DISP_KEY_CAP_ZWNJANDROID 0x76 +#define LDML_DISP_KEY_CAP_ZWNJGENERIC 0x79 +#define LDML_DISP_KEY_CAP_ZWNJIOS 0x75 +#define LDML_DISP_KEY_CAP_ZWSP 0x81 #define LDML_ELEM_FLAGS_ORDER_BITSHIFT 0x10 #define LDML_ELEM_FLAGS_ORDER_MASK 0xFF0000 #define LDML_ELEM_FLAGS_PREBASE 0x8 @@ -45,7 +125,16 @@ #define LDML_KEYS_MOD_NONE 0x0 #define LDML_KEYS_MOD_OTHER 0x10000 #define LDML_KEYS_MOD_SHIFT 0x10 -#define LDML_LAYR_LIST_HARDWARE_TOUCH "touch" +#define LDML_KMXPLUS_VERSION_17 0x1100 +#define LDML_KMXPLUS_VERSION_19 0x1300 +#define LDML_LAYR_FORM_FLAGS_CHIRAL_SEPARATE 0x2 +#define LDML_LAYR_FORM_FLAGS_SHOW_BASE_LAYOUT 0x1 +#define LDML_LAYR_FORM_HARDWARE_ABNT2 "abnt2" +#define LDML_LAYR_FORM_HARDWARE_ISO "iso" +#define LDML_LAYR_FORM_HARDWARE_JIS "jis" +#define LDML_LAYR_FORM_HARDWARE_KS "ks" +#define LDML_LAYR_FORM_HARDWARE_TOUCH "touch" +#define LDML_LAYR_FORM_HARDWARE_US "us" #define LDML_LAYR_MAX_MINDEVICEWIDTH 0x3E7 #define LDML_LAYR_MIN_MINDEVICEWIDTH 0x1 #define LDML_LENGTH_BKSP 0xC @@ -57,7 +146,8 @@ #define LDML_LENGTH_ELEM_ITEM_ELEMENT 0x8 #define LDML_LENGTH_FINL 0x8 #define LDML_LENGTH_FINL_ITEM 0x10 -#define LDML_LENGTH_HEADER 0x8 +#define LDML_LENGTH_HEADER_17 0x8 +#define LDML_LENGTH_HEADER_19 0xC #define LDML_LENGTH_KEYS 0x18 #define LDML_LENGTH_KEYS_FLICK_ELEMENT 0x8 #define LDML_LENGTH_KEYS_FLICK_LIST 0xC @@ -65,8 +155,9 @@ #define LDML_LENGTH_KEYS_KMAP 0xC #define LDML_LENGTH_LAYR 0x18 #define LDML_LENGTH_LAYR_ENTRY 0x10 +#define LDML_LENGTH_LAYR_FORM_V17 0x10 +#define LDML_LENGTH_LAYR_FORM_V19 0x20 #define LDML_LENGTH_LAYR_KEY 0x4 -#define LDML_LENGTH_LAYR_LIST 0x10 #define LDML_LENGTH_LAYR_ROW 0x8 #define LDML_LENGTH_LIST 0x10 #define LDML_LENGTH_LIST_INDEX 0x4 @@ -120,6 +211,8 @@ #define LDML_SECTIONNAME_USET "uset" #define LDML_SECTIONID_VARS 0x73726176 /* "vars" */ #define LDML_SECTIONNAME_VARS "vars" +#define LDML_SECTIONID_SEC2 0x32636573 +#define LDML_SECTIONNAME_SEC2 "sec2" #define LDML_TRAN_FLAGS_ERROR 0x1 #define LDML_TRAN_GROUP_TYPE_REORDER 0x1 #define LDML_TRAN_GROUP_TYPE_TRANSFORM 0x0 diff --git a/core/include/ldml/keyman_core_ldml.ts b/core/include/ldml/keyman_core_ldml.ts index c975c94b71d..83a3f12683b 100644 --- a/core/include/ldml/keyman_core_ldml.ts +++ b/core/include/ldml/keyman_core_ldml.ts @@ -5,7 +5,6 @@ to be shared between TypeScript and C++ via the generator (below) */ - // NOTICE! // // If you update this file, you *must* be sure to re-run @@ -43,6 +42,11 @@ type SectionMap = { [id in SectionIdent]: SectionIdent; } +export enum KMXPlusVersion { + Version17 = 0x1100, // == KMXFile.VERSION_170, + Version19 = 0x1300, // == KMXFile.VERSION_190, +}; + // TODO-LDML: namespace com.keyman.core.ldml { /** * Constants for the KMXPlus data format @@ -79,11 +83,27 @@ class Constants { /** * Length of a raw section header, in bytes */ - readonly length_header = 8; + readonly length_header_17 = 8; + /** + * Length of a raw section header, in bytes + */ + readonly length_header_19 = 12; + + /** + * Version number 17 for KMX+ file format, initial release version, + * corresponds to Keyman 17.0 + */ + readonly kmxplus_version_17: KMXPlusVersion = KMXPlusVersion.Version17; + + /** + * Version number 19 for KMX+ file format, new SEC2 section and version + * header, corresponds to Keyman 19.0 + */ + readonly kmxplus_version_19: KMXPlusVersion = KMXPlusVersion.Version19; /* ------------------------------------------------------------------ - * sect section - ------------------------------------------------------------------ */ + * sect section + * ------------------------------------------------------------------ */ /** * Minimum length of the 'sect' section, not including entries @@ -95,8 +115,8 @@ class Constants { readonly length_sect_item = 8; /* ------------------------------------------------------------------ - * bksp section - ------------------------------------------------------------------ */ + * bksp section + * ------------------------------------------------------------------ */ /** * Minimum length of the 'bksp' section, not including entries @@ -112,21 +132,165 @@ class Constants { readonly bksp_flags_error = 0x0001; /* ------------------------------------------------------------------ - * disp section - ------------------------------------------------------------------ */ + * disp section + * ------------------------------------------------------------------ */ /** * Minimum length of the 'disp' section, not including entries */ - readonly length_disp = 16; - /** - * Length of each entry in the 'disp' variable part - */ - readonly length_disp_item = 12; + readonly length_disp = 16; + /** + * Length of each entry in the 'disp' variable part; note size is same in v17 and v19 + */ + readonly length_disp_item = 12; + + + /** + * in disp.item.flags, mask for flag bits (v19+) + */ + readonly disp_item_flags_mask_flags = 0x00000FFF; + + /** + * in disp.item.flags, if set, then item toId value is an id. (v19+) + */ + readonly disp_item_flags_is_id = 0x00000001; + /** + * in disp.item.flags, if set, then item display string is an SVG file. (v19+) + */ + readonly disp_item_flags_is_svg = 0x00000002; + /** + * in disp.item.flags, if set, then draw the key as a frame key (v19+) + */ + readonly disp_item_flags_is_frame_key = 0x00000004; + /** + * in disp.item.flags, if set, then draw the key as highlighted (v19+) + */ + readonly disp_item_flags_is_highlighted = 0x00000008; + /** + * in disp.item.flags, if set, then draw the key as a deadkey (v19+) + */ + readonly disp_item_flags_is_deadkey = 0x00000010; + /** + * in disp.item.flags, if set, then the key is blank and non-interactive (v19+) + */ + readonly disp_item_flags_is_blank_key = 0x00000020; + + // reserved bits 6-11 for future flags + + /** + * in disp.item.flags, mask for hint position (v19+) + */ + readonly disp_item_flags_mask_hint = 0x0000F000; + + /** + * in disp.item.flags, right shift value for masked hint position (v19+) + */ + readonly disp_item_flags_shift_hint = 12; + + /** hint position for primary key cap (v19+) */ + readonly disp_item_hint_primary = 0; + /** hint position for north-west (top left) hint (v19+) */ + readonly disp_item_hint_nw = 1; + /** hint position for north (top) hint (v19+) */ + readonly disp_item_hint_n = 2; + /** hint position for north-east (top right) hint (v19+) */ + readonly disp_item_hint_ne = 3; + /** hint position for west (left) hint (v19+) */ + readonly disp_item_hint_w = 4; + /** hint position for east (right) hint (v19+) */ + readonly disp_item_hint_e = 5; + /** hint position for south west (bottom left) hint (v19+) */ + readonly disp_item_hint_sw = 6; + /** hint position for south (bottom) hint (v19+) */ + readonly disp_item_hint_s = 7; + /** hint position for south east (bottom right) hint (v19+) */ + readonly disp_item_hint_se = 8; + + /** + * in disp.item.flags, mask for key cap type. (v19+) + */ + readonly disp_item_flags_mask_key_cap_type = 0xFFFF0000; + + /** + * in disp.item.flags, right shift value for key cap type. + */ + readonly disp_item_flags_shift_key_cap_type = 16; + + // The following values match web/.../specialCharacters.ts; see + // developer/src/kmc-kmn/test/kmw/constants.tests.ts for more information. + + /** + * dis2.item.flags key cap type, sync with specialCharacters.ts (v19+) + */ + readonly disp_key_cap_shift = 8; + readonly disp_key_cap_enter = 5; + readonly disp_key_cap_tab = 6; + readonly disp_key_cap_bksp = 4; + readonly disp_key_cap_menu = 11; + readonly disp_key_cap_hide = 10; + readonly disp_key_cap_alt = 25; + readonly disp_key_cap_ctrl = 1; + readonly disp_key_cap_caps = 3; + readonly disp_key_cap_abc_upper = 16; // differentiate '*ABC*' and '*abc*' + readonly disp_key_cap_abc_lower = 17; // differentiate '*ABC*' and '*abc*' + readonly disp_key_cap_123 = 19; + readonly disp_key_cap_symbol = 21; + readonly disp_key_cap_currency = 20; + readonly disp_key_cap_shifted = 9; + readonly disp_key_cap_altgr = 2; + readonly disp_key_cap_tableft = 7; + readonly disp_key_cap_lalt = 0x56; + readonly disp_key_cap_ralt = 0x57; + readonly disp_key_cap_lctrl = 0x58; + readonly disp_key_cap_rctrl = 0x59; + readonly disp_key_cap_laltctrl = 0x60; + readonly disp_key_cap_raltctrl = 0x61; + readonly disp_key_cap_laltctrlshift = 0x62; + readonly disp_key_cap_raltctrlshift = 0x63; + readonly disp_key_cap_altshift = 0x64; + readonly disp_key_cap_ctrlshift = 0x65; + readonly disp_key_cap_altctrlshift = 0x66; + readonly disp_key_cap_laltshift = 0x67; + readonly disp_key_cap_raltshift = 0x68; + readonly disp_key_cap_lctrlshift = 0x69; + readonly disp_key_cap_rctrlshift = 0x70; + // Added in Keyman 14.0. + readonly disp_key_cap_ltrenter = 0x05; // Default alias of '*Enter*'. + readonly disp_key_cap_ltrbksp = 0x04; // Default alias of '*BkSp*'. + readonly disp_key_cap_rtlenter = 0x71; + readonly disp_key_cap_rtlbksp = 0x72; + readonly disp_key_cap_shiftlock = 0x73; + readonly disp_key_cap_shiftedlock = 0x74; + readonly disp_key_cap_zwnj = 0x75; // If this one is specified, auto-detection will kick in. + readonly disp_key_cap_zwnjios = 0x75; // The iOS version will be used by default, but the + readonly disp_key_cap_zwnjandroid = 0x76; // Android platform has its own default glyph. + // Added in Keyman 17.0. + // Reference: https://github.com/silnrsi/font-symchar/blob/v4.000/documentation/encoding.md + readonly disp_key_cap_zwnjgeneric = 0x79; // Generic version of ZWNJ (no override) + readonly disp_key_cap_sp = 0x80; // Space + readonly disp_key_cap_nbsp = 0x82; // No-break Space + readonly disp_key_cap_narnbsp = 0x83; // Narrow No-break Space + readonly disp_key_cap_enq = 0x84; // En Quad + readonly disp_key_cap_emq = 0x85; // Em Quad + readonly disp_key_cap_ensp = 0x86; // En Space + readonly disp_key_cap_emsp = 0x87; // Em Space + // TODO: Skipping #-per-em-space + readonly disp_key_cap_punctsp = 0x8c; // Punctuation Space + readonly disp_key_cap_thsp = 0x8d; // Thin Space + readonly disp_key_cap_hsp = 0x8e; // Hair Space + readonly disp_key_cap_zwsp = 0x81; // Zero Width Space + readonly disp_key_cap_zwj = 0x77; // Zero Width Joiner + readonly disp_key_cap_wj = 0x78; // Word Joiner + readonly disp_key_cap_cgj = 0x7a; // Combining Grapheme Joiner + readonly disp_key_cap_ltrm = 0x90; // Left-to-right Mark + readonly disp_key_cap_rtlm = 0x91; // Right-to-left Mark + readonly disp_key_cap_sh = 0xa1; // Soft Hyphen + readonly disp_key_cap_htab = 0xa2; // Horizontal Tabulation + /* ------------------------------------------------------------------ - * elem section - ------------------------------------------------------------------ */ + * elem section + * ------------------------------------------------------------------ */ /** * Minimum length of the 'elem' section, not including entries @@ -214,7 +378,7 @@ class Constants { /* ------------------------------------------------------------------ * finl section - ------------------------------------------------------------------ */ + * ------------------------------------------------------------------ */ /** * Minimum length of the 'finl' section, not including entries @@ -230,8 +394,8 @@ class Constants { readonly finl_flags_error = 0x0001; /* ------------------------------------------------------------------ - * keys section is now keys.kmap - ------------------------------------------------------------------ */ + * keys section is now keys.kmap + * ------------------------------------------------------------------ */ /** * Constant for no modifiers @@ -300,7 +464,7 @@ class Constants { /* ------------------------------------------------------------------ * keys section - ------------------------------------------------------------------ */ + * ------------------------------------------------------------------ */ /** * Minimum length of the 'keys' section not including variable parts @@ -335,20 +499,46 @@ class Constants { /* ------------------------------------------------------------------ * layr section - ------------------------------------------------------------------ */ + * ------------------------------------------------------------------ */ /** * Minimum length of the 'layr' section not including variable parts */ readonly length_layr = 24; /** - * Length of each layer list in the 'layr' section variable part + * Length of each layer form (renamed from 'list' in v19) in the 'layr' section variable part (v17) + */ + readonly length_layr_form_v17 = 16; + /** + * Length of each layer form in the 'layr' section variable part (v19+) */ - readonly length_layr_list = 16; + readonly length_layr_form_v19 = 32; + /** * for the 'hardware' field indicating a touch keyboard, non-hardware */ - readonly layr_list_hardware_touch = 'touch'; + readonly layr_form_hardware_touch = 'touch'; + /** + * for the 'hardware' field indicating a Brazilian 103 key ABNT2 layout (iso + extra key near right shift) + */ + readonly layr_form_hardware_abnt2 = 'abnt2'; + /** + * for the 'hardware' field indicating a European 102 key layout (extra key near left shift) + */ + readonly layr_form_hardware_iso = 'iso'; + /** + * for the 'hardware' field indicating a Japanese 109 key layout + */ + readonly layr_form_hardware_jis = 'jis'; + /** + * for the 'hardware' field indicating a Korean KS layout + */ + readonly layr_form_hardware_ks = 'ks'; + /** + * for the 'hardware' field indicating a US ANSI 101 key keyboard + */ + readonly layr_form_hardware_us = 'us'; + /** * Length of each layer entry in the 'layr' section variable part */ @@ -361,20 +551,28 @@ class Constants { * Length of each key entry in the 'layr' section variable part */ readonly length_layr_key = 4; + /** + * in layr.form.flags, if set, then base layout key caps hints should be shown + */ + readonly layr_form_flags_show_base_layout = 0x00000001; + /** + * in layr.form.flags, if set, then left/right Ctrl and Alt keys function independently + */ + readonly layr_form_flags_chiral_separate = 0x00000002; /** - * Minimum allowed minDeviceWidth for a layer list + * Minimum allowed minDeviceWidth for a layer form */ readonly layr_min_minDeviceWidth = 1; /** - * Maximum allowed minDeviceWidth for a layer list + * Maximum allowed minDeviceWidth for a layer form */ readonly layr_max_minDeviceWidth = 999; /* ------------------------------------------------------------------ * list section - ------------------------------------------------------------------ */ + * ------------------------------------------------------------------ */ /** * Minimum length of the 'list' section not including variable parts @@ -391,7 +589,7 @@ class Constants { /* ------------------------------------------------------------------ * loca section - ------------------------------------------------------------------ */ + * ------------------------------------------------------------------ */ /** * Minimum length of the 'loca' section not including variable parts @@ -403,8 +601,8 @@ class Constants { readonly length_loca_item = 4; /* ------------------------------------------------------------------ - * meta section - ------------------------------------------------------------------ */ + * meta section + ------------------------------------------------------------------ */ /** * length of the 'meta' section @@ -416,8 +614,8 @@ class Constants { readonly meta_settings_normalization_disabled = 1; /* ------------------------------------------------------------------ - * strs section - ------------------------------------------------------------------ */ + * strs section + ------------------------------------------------------------------ */ /** * Minimum length of the 'strs' section not including variable parts @@ -429,8 +627,8 @@ class Constants { readonly length_strs_item = 8; /* ------------------------------------------------------------------ - * tran section - ------------------------------------------------------------------ */ + * tran section + * ------------------------------------------------------------------ */ /** * Minimum length of the 'tran' section, not including entries @@ -528,6 +726,10 @@ class Constants { vars: 'vars', }; + // v19+: special case for 'sect' override with 'sec2' + readonly sectionname_sec2 = 'sec2'; + readonly sectionid_sec2 = 0x32636573; + /** * Use to convert 4-char string into hex * @param id section id such as 'sect' @@ -580,6 +782,17 @@ class Constants { treatAsLatest(version: string): boolean { return cldrTreatAsLatest.has(version); } + + /** + * Difference in section header size from default v17 size + */ + headerSizeDelta(version: KMXPlusVersion): number { + if(version == KMXPlusVersion.Version17) { + return 0; + } + + return 4; /* KMXPlusVersion.Version19, additional version uint32le field */ + } }; /** There's no data or DTD change in 45, 46, 46.1, 47 so map them all to 46 at present. */ diff --git a/core/src/kmx/kmx_plus.cpp b/core/src/kmx/kmx_plus.cpp index f9aafb9d663..11a0b4b1193 100644 --- a/core/src/kmx/kmx_plus.cpp +++ b/core/src/kmx/kmx_plus.cpp @@ -92,70 +92,129 @@ validate_section_name(KMX_DWORD ident) { } /** - * @brief cast to a COMP_KMXPLUS_HEADER from bytes + * @brief fill a COMP_KMXPLUS_HEADER from bytes * * @param data * @param length * @param ident - * @return const kmx::COMP_KMXPLUS_HEADER* + * @param out + * @return true on succcess */ -static const kmx::COMP_KMXPLUS_HEADER * -header_from_bytes(const uint8_t *data, KMX_DWORD length, uint32_t ident) { +bool +header_from_bytes(const uint8_t *data, KMX_DWORD length, KMX_DWORD fileVersion, uint32_t ident, kmx::COMP_KMXPLUS_HEADER &out) { if (!data) { DebugLog("!data"); assert(false); - return nullptr; + return false; } - if (length < LDML_LENGTH_HEADER) { - DebugLog("length < LDML_LENGTH_HEADER"); - assert(false); - return nullptr; + if(fileVersion == LDML_KMXPLUS_VERSION_17) { + const COMP_KMXPLUS_HEADER_17 *all = reinterpret_cast(data); + if (!all->valid(length)) { + DebugLog("header failed validation"); + assert(false); + return false; + } + + if (all->ident != ident) { + DebugLog("header had wrong section id"); + assert(false); + return false; + } + + out.set(LDML_KMXPLUS_VERSION_17, all->ident, all->size); + } else { + const COMP_KMXPLUS_HEADER_19 *all = reinterpret_cast(data); + if (!all->valid(length)) { + DebugLog("header failed validation"); + assert(false); + return false; + } + + if(ident == LDML_SECTIONID_SECT) { + if(all->ident != LDML_SECTIONID_SEC2) { + DebugLog("header had SECT but expected SEC2"); + assert(false); + return false; + } + } else if (all->ident != ident) { + DebugLog("header had wrong section id"); + assert(false); + return false; + } + + out.set(LDML_KMXPLUS_VERSION_19, all->ident, all->size, all->version); } - const COMP_KMXPLUS_HEADER *all = reinterpret_cast(data); - if (!all->valid(length)) { - DebugLog("header failed validation"); + + return true; +} + +/** + * @brief Determines the overall version of the file based on the first four bytes: + * 'sect' = v17 file + * 'sec2' = v19+ file + * other = invalid file + * A v19+ file will include an additional version field for each section. + * @param data + * @param length + * @return KMX_DWORD a LDML_KMXPLUS_VERSION_ value, or 0 on error + */ +KMX_DWORD determine_file_version_from_bytes(const uint8_t* data, KMX_DWORD length) { + if(length < sizeof(COMP_KMXPLUS_HEADER_17)) { + DebugLog("data is too short"); assert(false); - return nullptr; + return 0; } - if (all->ident != ident) { - DebugLog("header had wrong section id"); - assert(false); - return nullptr; + COMP_KMXPLUS_HEADER_17 const* header = reinterpret_cast(data); + if(header->ident == COMP_KMXPLUS_SECT::IDENT) { + return LDML_KMXPLUS_VERSION_17; + } + if(header->ident == COMP_KMXPLUS_SECT::IDENT_V19) { + return LDML_KMXPLUS_VERSION_19; } - return all; + DebugLog("initial section is unrecognized"); + assert(false); + return 0; } - /** * @brief Accessor for a section based on bytes * * @tparam T * @param data input pointer to the section to be validated. If nullptr, 'sect' will be nullptr, but will return true (valid). - * @param length length of input data. This function will not read past the end of this length. + * @param fileLength length of input data. This function will not read past the end of this length. * @param out on exit, will be set to the new section. On any failure, null data, or invalidity, will be nullptr. * @return true if section is missing (nullptr) or valid * @return const T* */ -template bool section_from_bytes(const uint8_t* data, KMX_DWORD length, const T*& out) { +template bool section_from_bytes(const uint8_t* data, KMX_DWORD fileLength, KMX_DWORD fileVersion, COMP_KMXPLUS_HEADER& header, const T*& out) { out = nullptr; // null unless valid. if (data == nullptr) { // missing section - not an error. DebugLog("data was null (missing section)"); return true; - } else if (length < sizeof(T)) { // Does not include dynamic data. First check. - DebugLog("length < sizeof(section)"); + } else if (fileLength < sizeof(T)) { // Does not include dynamic data. First check. + DebugLog("fileLength < sizeof(section)"); assert(false); return false; } - // verify the kmx+ header - const COMP_KMXPLUS_HEADER *header = header_from_bytes(data, length, T::IDENT); + + // verify the kmx+ section header + if(!header_from_bytes(data, fileLength, fileVersion, T::IDENT, header)) { + // asserted and logged in header_from_bytes + return false; + } + + // subtract the size of the header so validation will not be affected by the + // change of header size in LDML_KMXPLUS_VERSION_19 + header.size -= header.headerSize(); + // now it is safe to cast this to a T - const T *section = reinterpret_cast(header); + const T *section = reinterpret_cast(data + header.headerSize()); if (section == nullptr) { // should not happen. DebugLog("reinterpret_cast<> failed."); assert(false); return false; - } else if (!section->valid(length)) { + } else if (!section->valid(header, fileLength)) { return false; // validation failed. } else { out = section; @@ -163,34 +222,223 @@ template bool section_from_bytes(const uint8_t* data, KMX_DWORD length } } +/** + * @brief This is a special override just for COMP_KMXPLUS_BKSP, which is identical to + * COMP_KMXPLUS_TRAN, except for ident. So we leverage TRAN functionality to + * process, but in order to keep the distinction bubbling too high, this is the + * only place we differentiate. + * @param sect this is the pointer to the 'sect' section table. Should not be null! + * @param out on exit, will be set to the requested section, or nullptr if missing or invalid + * @returns true if section is valid or missing, false only if invalid + */ +bool get_section_from_sect(const COMP_KMXPLUS_SECT* sect, const COMP_KMXPLUS_HEADER& sectHeader, COMP_KMXPLUS_Section_Helper* helper, const COMP_KMXPLUS_BKSP*& out) { + out = nullptr; + + KMX_DWORD entryLength; + const uint8_t *rawbytes = sect->get(sectHeader, COMP_KMXPLUS_BKSP::IDENT, entryLength); + if (rawbytes == nullptr) { + // just missing, not invalid + return true; + } + + if(!section_from_bytes(rawbytes, entryLength, sectHeader.fileVersion(), helper->header, out)) { + return false; + } + + if(!out) { + assert(false); + DebugLog("unexpected out == nullptr"); + return false; + } + + if(!helper->set(out)) { + return false; + } + + return true; +} + /** * @param sect this is the pointer to the 'sect' section table. Should not be null! * @param out on exit, will be set to the requested section, or nullptr if missing or invalid * @returns true if section is valid or missing, false only if invalid */ template -bool get_section_from_sect(const COMP_KMXPLUS_SECT* sect, const T*& out) { +bool get_section_from_sect(const COMP_KMXPLUS_SECT* sect, const COMP_KMXPLUS_HEADER& sectHeader, COMP_KMXPLUS_Section_Helper* helper, const T*& out) { + out = nullptr; + KMX_DWORD entryLength; - const uint8_t *rawbytes = sect->get(T::IDENT, entryLength); + const uint8_t *rawbytes = sect->get(sectHeader, T::IDENT, entryLength); if (rawbytes == nullptr) { // just missing, not invalid - out = nullptr; return true; - } else { - return section_from_bytes(rawbytes, entryLength, out); } + + if(!section_from_bytes(rawbytes, entryLength, sectHeader.fileVersion(), helper->header, out)) { + return false; + } + + if(!out) { + DebugLog("unexpected out == nullptr"); + assert(false); + return false; + } + + if(!helper->set(out)) { + return false; + } + + return true; +} + +inline bool is_block_valid(COMP_KMXPLUS_HEADER const &header, KMX_DWORD offset, KMX_DWORD size) { + if(offset < header.headerSize()) { + DebugLog("[%x] offset(%d) < header.headerSize(%d)", header.ident, offset, header.headerSize()); + assert(false); + return false; + } + if(offset - header.headerSize() > header.size) { + DebugLog("[%x] offset(%d) > header.size(%d)", header.ident, offset, header.size); + assert(false); + return false; + } + if(offset + size - header.headerSize() > header.size) { + DebugLog("[%x] offset(%d) + size(%d) > header.size(%d)", header.ident, offset, size, header.size); + assert(false); + return false; + } + return true; +} + +/** + * @brief Get file data at byte offset; cannot retrieve the initial SECT/SEC2 header + * + * @tparam U type of target data + * @param base start of SECT data, i.e. immediately after SECT header + * @param fileOffset offset in bytes from start of file + * @param fileLength + * @return U* + */ +template +const U* get_file_data_at_offset(const COMP_KMXPLUS_SECT *base, COMP_KMXPLUS_HEADER const &header, KMX_DWORD fileOffset, KMX_DWORD fileLength) { + if(fileOffset < header.headerSize()) { + // This is retrieving data based on the SECT/SEC2 table base, which points + // at the first byte after the SECT/SEC2 header. + DebugLog("Attempted to retrieve SECT/SEC2 header, which is not allowed here"); + assert(false); + return nullptr; + } + if(fileOffset >= fileLength) { + DebugLog("Attempted to retrieve data outside file boundaries"); + assert(false); + return nullptr; + } + fileOffset -= header.headerSize(); + const uint8_t* thisptr = reinterpret_cast(base); + return reinterpret_cast(thisptr+fileOffset); +} + +/** + * @brief Get the section data at offset, casting to desired type + * + * @tparam T struct type of the section + * @tparam U struct type of data to return + * @param base start of the section data, i.e. immediately after section + * header + * @param header header data for the section + * @param offset offset in bytes from the start of the section data + * @param size size of the data to return, for validation + * @return pointer to data, type U + */ +template +const U* get_section_data_at_offset(const T *base, COMP_KMXPLUS_HEADER const &header, KMX_DWORD offset, KMX_DWORD size) { + if(!is_block_valid(header, offset, size)) { + return nullptr; + } + + offset -= header.headerSize(); + const uint8_t* thisptr = reinterpret_cast(base); + const U* start = reinterpret_cast(thisptr+offset); + return start; +} + +/** + * @brief Get the section data at offset, casting to desired type, verify that + * the section is long enough for count * data, and update the offset to point + * to the next byte after the data. Allows zero-length, optional data. + * + * @tparam T struct type of the section + * @tparam U struct type of data to return + * @param base start of the section data, i.e. immediately after section + * header + * @param header header data for the section + * @param count number of U items expected + * @param offset (in, out) offset in bytes from the start of the section data, + * updated on return to next byte after data + * @param out (out) pointer to start of data + * @return bool false on error + */ +template +bool get_optional_section_data_at_offset_and_increment(const T *base, COMP_KMXPLUS_HEADER const &header, + KMX_DWORD count, KMX_DWORD& offset, const U*& out +) { + out = nullptr; + + if(count == 0) { + return true; + } + + KMX_DWORD size = count * sizeof(U); + + out = get_section_data_at_offset(base, header, offset, size); + + if(out == nullptr) { + return false; + } + + offset += size; + + return true; +} + +/** + * @brief Get the section data at offset, casting to desired type, verify that + * the section is long enough for count * data, and update the offset to point + * to the next byte after the data. Does not allow zero-length, optional data. + * + * @tparam T struct type of the section + * @tparam U struct type of data to return + * @param base start of the section data, i.e. immediately after section + * header + * @param header header data for the section + * @param count number of U items expected + * @param offset (in, out) offset in bytes from the start of the section data, + * updated on return to next byte after data + * @param out (out) pointer to start of data + * @return bool false on error or missing data + */ +template +bool get_required_section_data_at_offset_and_increment(const T *base, COMP_KMXPLUS_HEADER const &header, + KMX_DWORD count, KMX_DWORD& offset, const U*& out +) { + if(count == 0) { + out = nullptr; + return false; + } + + return get_optional_section_data_at_offset_and_increment(base, header, count, offset, out); } bool -COMP_KMXPLUS_HEADER::valid(KMX_DWORD length) const { +COMP_KMXPLUS_HEADER_17::valid(KMX_DWORD length) const { DebugLog("%c%c%c%c: (%X) size 0x%X\n", DEBUG_IDENT(ident), ident, size); - if (size < LDML_LENGTH_HEADER) { - DebugLog("size < LDML_LENGTH_HEADER"); + if (size < LDML_LENGTH_HEADER_17) { + DebugLog("size %d < LDML_LENGTH_HEADER_17 %d", size, LDML_LENGTH_HEADER_17); assert(false); return false; } if (size > length) { - DebugLog("size > length"); + DebugLog("size %d > length %d", size, length); assert(false); return false; } @@ -202,12 +450,30 @@ COMP_KMXPLUS_HEADER::valid(KMX_DWORD length) const { } bool -COMP_KMXPLUS_LOCA::valid(KMX_DWORD _kmn_unused(length)) const { - if (header.size < sizeof(*this)+(sizeof(entries[0])*count)) { - DebugLog("header.size < expected size"); +COMP_KMXPLUS_HEADER_19::valid(KMX_DWORD length) const { + DebugLog("%c%c%c%c: (%X) size 0x%X\n", DEBUG_IDENT(ident), ident, size); + if (size < LDML_LENGTH_HEADER_19) { + DebugLog("size %d < LDML_LENGTH_HEADER %d", size, LDML_LENGTH_HEADER_19); + assert(false); + return false; + } + if (size > length) { + DebugLog("size %d > length %d", size, length); assert(false); return false; } + if (!validate_section_name(ident)) { + return false; + } + DebugLog(" (header OK)"); // newline after section name + return true; +} + +bool +COMP_KMXPLUS_LOCA::valid(COMP_KMXPLUS_HEADER const &header, KMX_DWORD _kmn_unused(length)) const { + if(!is_block_valid(header, header.headerSize(), sizeof(*this)+(sizeof(entries[0])*count))) { + return false; + } for(KMX_DWORD i=0; i str0x%X", i, entries[i].id, entries[i].to, entries[i].display); - if ((entries[i].to == 0 && entries[i].id == 0) || entries[i].display == 0) { - DebugLog("disp must have either keyId/output, and must have display"); + DebugLoad("disp#%d: toId: str0x%X -> str0x%X, flags: 0x%x", i, entries[i].toId, entries[i].display, entries[i].flags); + if (entries[i].toId == 0 || entries[i].display == 0) { + DebugLog("disp must have output/keyId, and must have display"); assert(false); return false; } + //TODO-EMBED-OSK-IN-KMX: validate flags } return true; } bool -COMP_KMXPLUS_STRS::valid(KMX_DWORD _kmn_unused(length)) const { - DebugLog("strs: count 0x%X\n", count); - if (header.size < sizeof(*this)+(sizeof(entries[0])*count)) { +COMP_KMXPLUS_DISP::valid_17(COMP_KMXPLUS_HEADER const &header, KMX_DWORD _kmn_unused(length)) const { + if(header.version != LDML_KMXPLUS_VERSION_17) { + assert(false); + return false; + } + + DebugLog("disp: count 0x%X\n", count); + + if (!is_block_valid(header, header.headerSize(), sizeof(*this)+(sizeof(entries[0])*count))) { DebugLog("header.size < expected size"); assert(false); return false; } + + if (baseCharacter != 0) { + DebugLog("disp: baseCharacter str#0x%X", baseCharacter); + } + + const COMP_KMXPLUS_DISP_ENTRY_17 *entries17 = reinterpret_cast(&entries[0]); for (KMX_DWORD i=0; i header.size) { - DebugLog("#0x%X: expected end of string past header.size", i); + DebugLoad("disp#%d: id: str0x%X to: str0x%X -> str0x%X", i, entries17[i].id, entries17[i].to, entries17[i].display); + if ((entries17[i].to == 0 && entries17[i].id == 0) || entries17[i].display == 0) { + DebugLog("disp must have either keyId/output, and must have display"); assert(false); return false; } - const uint8_t* thisptr = reinterpret_cast(this); - const KMX_WCHAR* start = reinterpret_cast(thisptr+offset); + } + return true; +} + +bool +COMP_KMXPLUS_DISP_Helper::set(const COMP_KMXPLUS_DISP *newDisp) { + if(header.version != LDML_KMXPLUS_VERSION_17 && header.version != LDML_KMXPLUS_VERSION_19) { + return false; + } + + // TODO-EMBED-OSK-IN-KMX - more transforms required for in-memory representation of OSK; convert v17 to v19 in memory + + if(!COMP_KMXPLUS_Section_Helper::set(newDisp)) { + return false; + } + + return true; +} + +bool +COMP_KMXPLUS_STRS::valid(COMP_KMXPLUS_HEADER const &header, KMX_DWORD _kmn_unused(length)) const { + DebugLog("strs: count 0x%X\n", count); + + if(!is_block_valid(header, header.headerSize(), sizeof(*this)+(sizeof(entries[0])*count))) { + return false; + } + + for (KMX_DWORD i=0; i(this, header, offset, (length+1)*sizeof(KMX_WCHAR)); + if(!start) { + return false; + } if(start[length] != 0) { DebugLog("#0x%X: String of length 0x%x not null terminated", i, length); assert(start[length] == 0); @@ -330,7 +659,7 @@ bool COMP_KMXPLUS_STRS::valid_string(const KMX_WCHAR* start, KMX_DWORD length) { // else OK (good marker) } else if(!Uni_IsValid(ch)) { DebugLog("String of length 0x%x @ 0x%x: Char U+%04X is illegal char", length, n, ch); - return false; + return false; } // else OK (other char) } return true; @@ -349,12 +678,10 @@ std::u16string COMP_KMXPLUS_STRS::str_from_char(KMX_DWORD v) { bool -COMP_KMXPLUS_SECT::valid(KMX_DWORD length) const { +COMP_KMXPLUS_SECT::valid(COMP_KMXPLUS_HEADER const &header, KMX_DWORD fileLength) const { DebugLog("sect: total 0x%X\n", total); DebugLog("sect: count 0x%X\n", count); - if (header.size < sizeof(*this)+(sizeof(entries[0])*count)) { - DebugLog("header.size < expected size"); - assert(false); + if(!is_block_valid(header, header.headerSize(), sizeof(*this)+(sizeof(entries[0])*count))) { return false; } @@ -371,11 +698,12 @@ COMP_KMXPLUS_SECT::valid(KMX_DWORD length) const { overall_valid = false; continue; } - const uint8_t* data = reinterpret_cast(this); - const uint8_t* entrydata = data + entry.offset; - KMX_DWORD entrylength = length - entry.offset; + + const uint8_t* entrydata = get_file_data_at_offset(this, header, entry.offset, fileLength); + KMX_DWORD entryMaxLength = fileLength - entry.offset; // just validate header - if(header_from_bytes(entrydata, entrylength, entry.sect) == nullptr) { + COMP_KMXPLUS_HEADER localHeader; + if(!header_from_bytes(entrydata, entryMaxLength, header.version, entry.sect, localHeader)) { DebugLog("Invalid header %X", entry.sect); assert(false); overall_valid = false; @@ -394,20 +722,13 @@ KMX_DWORD COMP_KMXPLUS_SECT::find(KMX_DWORD ident) const { return 0; } -const uint8_t *COMP_KMXPLUS_SECT::get(KMX_DWORD ident, KMX_DWORD &entryLength) const { +const uint8_t *COMP_KMXPLUS_SECT::get(COMP_KMXPLUS_HEADER const& header, KMX_DWORD ident, KMX_DWORD &entryLength) const { entryLength = 0; // the section table is also the beginning of the file. - const uint8_t *rawbytes = reinterpret_cast(this); - if (rawbytes == nullptr) { - // should not get here - null this pointer. - DebugLog("section_from_sect(nullptr) == nullptr"); - assert(false); - return nullptr; - } - // lookup the offset from teh table + // lookup the offset from the table KMX_DWORD offset = find(ident); if (!offset) { - DebugLog("section_from_sect() - not found. section %c%c%c%c (0x%X)", DEBUG_IDENT(ident), ident); + DebugLog("COMP_KMXPLUS_SECT::get() - not found. section %c%c%c%c (0x%X)", DEBUG_IDENT(ident), ident); return nullptr; } // return the potential length, based on table. @@ -415,14 +736,13 @@ const uint8_t *COMP_KMXPLUS_SECT::get(KMX_DWORD ident, KMX_DWORD &entryLength) c // we could take the offset of the next section, as an improvement. entryLength = total - offset; // return the pointer to the raw bytes - return rawbytes+offset; + return get_file_data_at_offset(this, header, offset, total); } // ---- transform related fcns bool -COMP_KMXPLUS_ELEM::valid(KMX_DWORD _kmn_unused(length)) const { - if (header.size < sizeof(*this)+(sizeof(entries[0])*count)) { - DebugLog("header.size 0x%X < expected size"); +COMP_KMXPLUS_ELEM::valid(COMP_KMXPLUS_HEADER const &header, KMX_DWORD _kmn_unused(length)) const { + if(!is_block_valid(header, header.headerSize(), sizeof(*this)+(sizeof(entries[0])*count))) { return false; } const COMP_KMXPLUS_ELEM_ENTRY &firstEntry = entries[0]; @@ -434,7 +754,7 @@ COMP_KMXPLUS_ELEM::valid(KMX_DWORD _kmn_unused(length)) const { for (KMX_DWORD e = 1; e < count; e++) { // Don't need to recheck the first entry hbere. KMX_DWORD listLength; - if (getElementList(e, listLength) == nullptr) { + if (getElementList(header, e, listLength) == nullptr) { return false; } if (listLength == 0) { // only the first element should have length zero @@ -446,7 +766,14 @@ COMP_KMXPLUS_ELEM::valid(KMX_DWORD _kmn_unused(length)) const { } const COMP_KMXPLUS_ELEM_ELEMENT * -COMP_KMXPLUS_ELEM::getElementList(KMX_DWORD elementNumber, KMX_DWORD &length) const { +COMP_KMXPLUS_ELEM_Helper::getElementList(KMX_DWORD elementNumber, KMX_DWORD &length) const { + assert(data()); + if(!data()) return nullptr; + return data()->getElementList(header, elementNumber, length); +} + +const COMP_KMXPLUS_ELEM_ELEMENT * +COMP_KMXPLUS_ELEM::getElementList(const COMP_KMXPLUS_HEADER &header, KMX_DWORD elementNumber, KMX_DWORD &length) const { if (elementNumber >= count) { DebugLog("ERROR: COMP_KMXPLUS_ELEM::getElementList(%d) >= count %d", elementNumber, count); assert(false); @@ -457,15 +784,15 @@ COMP_KMXPLUS_ELEM::getElementList(KMX_DWORD elementNumber, KMX_DWORD &length) co if (length == 0) { return nullptr; // Normal case for first element } - if (entry.offset + (entry.length * sizeof(COMP_KMXPLUS_ELEM_ELEMENT)) > header.size) { + if (entry.offset - header.headerSize() + (entry.length * sizeof(COMP_KMXPLUS_ELEM_ELEMENT)) > header.size) { DebugLog("ERROR: !! COMP_KMXPLUS_ELEM::getElementList(%d) would be off end of data area", elementNumber); assert(false); return nullptr; } - // pointer to beginning of elem section - const uint8_t *rawdata = reinterpret_cast(this); + // pointer to specified entry - return reinterpret_cast(rawdata + entry.offset); + return get_section_data_at_offset(this, header, entry.offset, + entry.length * sizeof(COMP_KMXPLUS_ELEM_ELEMENT)); } @@ -476,7 +803,7 @@ COMP_KMXPLUS_ELEM_ELEMENT::get_element_string() const { } std::deque -COMP_KMXPLUS_ELEM_ELEMENT::loadAsStringList(KMX_DWORD length, const COMP_KMXPLUS_STRS &strs) const { +COMP_KMXPLUS_ELEM_ELEMENT::loadAsStringList(KMX_DWORD length, const COMP_KMXPLUS_STRS_Helper &strs) const { std::deque list; for (KMX_DWORD i = 0; i= tran->groupCount) { + if (!valid() || group >= data()->groupCount) { assert(false); return nullptr; } return groups + group; } - const COMP_KMXPLUS_TRAN_TRANSFORM * COMP_KMXPLUS_TRAN_Helper::getTransform(KMX_DWORD transform) const { - if (!valid() || transform >= tran->transformCount) { + if (!valid() || transform >= data()->transformCount) { assert(false); return nullptr; } return transforms + transform; } - const COMP_KMXPLUS_TRAN_REORDER * COMP_KMXPLUS_TRAN_Helper::getReorder(KMX_DWORD reorder) const { - if (!valid() || reorder >= tran->reorderCount) { + if (!valid() || reorder >= data()->reorderCount) { assert(false); return nullptr; } return reorders + reorder; } - bool -COMP_KMXPLUS_TRAN_Helper::setTran(const COMP_KMXPLUS_TRAN *newTran) { +COMP_KMXPLUS_TRAN_Helper::set(const COMP_KMXPLUS_TRAN *newTran) { + is_valid = false; + groups = nullptr; + transforms = nullptr; + reorders = nullptr; + + if(!COMP_KMXPLUS_Section_Helper::set(newTran)) { + return false; + } is_valid = true; if (newTran == nullptr) { DebugLog("tran helper: missing, newTran=%p", newTran); @@ -560,44 +891,26 @@ COMP_KMXPLUS_TRAN_Helper::setTran(const COMP_KMXPLUS_TRAN *newTran) { // No assert here: just a missing section return true; } - DebugLog("tran helper: validating '%c%c%c%c' newTran=%p", DEBUG_IDENT(newTran->header.ident), newTran); - tran = newTran; - const uint8_t *rawdata = reinterpret_cast(tran); - rawdata += LDML_LENGTH_TRAN; // skip past non-dynamic portion + KMX_DWORD offset = this->header.calculateBaseSize(LDML_LENGTH_TRAN); - // groups - if (tran->groupCount > 0) { - groups = reinterpret_cast(rawdata); - } else { - groups = nullptr; - is_valid = false; - assert(is_valid); - } - rawdata += sizeof(COMP_KMXPLUS_TRAN_GROUP) * tran->groupCount; + // groups (required) + is_valid = is_valid && get_required_section_data_at_offset_and_increment( + data(), header, data()->groupCount, offset, groups); + assert(is_valid); - // transforms - if (tran->transformCount > 0) { - transforms = reinterpret_cast(rawdata); - } else { - transforms = nullptr; - // is_valid = false; - // assert(is_valid); - } - rawdata += sizeof(COMP_KMXPLUS_TRAN_TRANSFORM) * tran->transformCount; + // transforms (optional) + is_valid = is_valid && get_optional_section_data_at_offset_and_increment( + data(), header, data()->transformCount, offset, transforms); + assert(is_valid); - // reorders - if (tran->reorderCount > 0) { - reorders = reinterpret_cast(rawdata); - } else { - reorders = nullptr; - // is_valid = false; - // assert(is_valid); - } - // rawdata += sizeof(COMP_KMXPLUS_TRAN_REORDER) * tran->reorderCount; + // reorders (optional) + is_valid = is_valid && get_optional_section_data_at_offset_and_increment( + data(), header, data()->reorderCount, offset, reorders); + assert(is_valid); // Now, validate offsets by walking if (is_valid) { - for(KMX_DWORD i = 0; is_valid && i < tran->groupCount; i++) { + for(KMX_DWORD i = 0; is_valid && i < data()->groupCount; i++) { const COMP_KMXPLUS_TRAN_GROUP &group = groups[i]; // is the count off the end? DebugLog( @@ -605,9 +918,9 @@ COMP_KMXPLUS_TRAN_Helper::setTran(const COMP_KMXPLUS_TRAN *newTran) { group.index + group.count - 1); if (group.type == LDML_TRAN_GROUP_TYPE_TRANSFORM) { DebugLog(" .. type=transform"); - if ((group.index >= tran->transformCount) || (group.index + group.count > tran->transformCount)) { + if ((group.index >= data()->transformCount) || (group.index + group.count > data()->transformCount)) { DebugLog("COMP_KMXPLUS_TRAN_Helper: group[%d] would access transform %d+%d, > count %d", - i, group.index, group.count, tran->transformCount); + i, group.index, group.count, data()->transformCount); is_valid = false; assert(is_valid); } @@ -625,9 +938,9 @@ COMP_KMXPLUS_TRAN_Helper::setTran(const COMP_KMXPLUS_TRAN *newTran) { } } else if (group.type == LDML_TRAN_GROUP_TYPE_REORDER) { DebugLog(" .. type=reorder"); - if ((group.index >= tran->reorderCount) || (group.index + group.count > tran->reorderCount)) { + if ((group.index >= data()->reorderCount) || (group.index + group.count > data()->reorderCount)) { DebugLog("COMP_KMXPLUS_TRAN_Helper: group[%d] would access reorder %d+%d, > count %d", - i, group.index, group.count, tran->reorderCount); + i, group.index, group.count, data()->reorderCount); is_valid = false; assert(is_valid); } @@ -648,36 +961,87 @@ COMP_KMXPLUS_TRAN_Helper::setTran(const COMP_KMXPLUS_TRAN *newTran) { } } // Return results - DebugLog("COMP_KMXPLUS_TRAN_Helper.setTran(): %s", is_valid ? "valid" : "invalid"); + DebugLog("COMP_KMXPLUS_TRAN_Helper.set(): %s", is_valid ? "valid" : "invalid"); assert(is_valid); return is_valid; } - +bool +COMP_KMXPLUS_LAYR::valid(COMP_KMXPLUS_HEADER const &header, KMX_DWORD length) const { + if(header.version == LDML_KMXPLUS_VERSION_17) { + return valid_17(header, length); + } + if(header.version == LDML_KMXPLUS_VERSION_19) { + return valid_19(header, length); + } + assert(false); + return false; +} bool -COMP_KMXPLUS_LAYR::valid(KMX_DWORD _kmn_unused(length)) const { - if (header.size < sizeof(*this) - + (listCount * sizeof(COMP_KMXPLUS_LAYR_LIST)) +COMP_KMXPLUS_LAYR::valid_19(COMP_KMXPLUS_HEADER const &header, KMX_DWORD _kmn_unused(length)) const { + if(header.version != LDML_KMXPLUS_VERSION_19) { + assert(false); + return false; + } + + if(!is_block_valid(header, header.headerSize(), + sizeof(*this) + + (formCount * sizeof(COMP_KMXPLUS_LAYR_FORM_V19)) + (layerCount * sizeof(COMP_KMXPLUS_LAYR_ENTRY)) + (rowCount * sizeof(COMP_KMXPLUS_LAYR_ROW)) - + (keyCount * sizeof(COMP_KMXPLUS_LAYR_KEY))) { - DebugLog("header.size < expected size"); + + (keyCount * sizeof(COMP_KMXPLUS_LAYR_KEY)))) { + return false; + } + + DebugLog("layr header is valid"); + // Note: We only do minimal validation here because of the + // dynamic structure. See COMP_KMXPLUS_LAYR_Helper.set() (below) + // all remaining checks + return true; +} + +bool +COMP_KMXPLUS_LAYR::valid_17(COMP_KMXPLUS_HEADER const &header, KMX_DWORD _kmn_unused(length)) const { + if(header.version != LDML_KMXPLUS_VERSION_17) { assert(false); return false; } + + if(!is_block_valid(header, header.headerSize(), + sizeof(*this) + + (formCount * sizeof(COMP_KMXPLUS_LAYR_FORM_V17)) + + (layerCount * sizeof(COMP_KMXPLUS_LAYR_ENTRY)) + + (rowCount * sizeof(COMP_KMXPLUS_LAYR_ROW)) + + (keyCount * sizeof(COMP_KMXPLUS_LAYR_KEY)))) { + return false; + } + DebugLog("layr header is valid"); // Note: We only do minimal validation here because of the - // dynamic structure. See COMP_KMXPLUS_LAYR_Helper.setLayr() (below) + // dynamic structure. See COMP_KMXPLUS_LAYR_Helper.set() (below) // all remaining checks return true; } -COMP_KMXPLUS_LAYR_Helper::COMP_KMXPLUS_LAYR_Helper() : layr(nullptr), is_valid(false) { +COMP_KMXPLUS_LAYR_Helper::COMP_KMXPLUS_LAYR_Helper() : is_valid(false), own_forms(false) { } bool -COMP_KMXPLUS_LAYR_Helper::setLayr(const COMP_KMXPLUS_LAYR *newLayr) { +COMP_KMXPLUS_LAYR_Helper::set(const COMP_KMXPLUS_LAYR *newLayr) { + is_valid = false; + forms = nullptr; + entries = nullptr; + rows = nullptr; + keys = nullptr; + + if(header.version != LDML_KMXPLUS_VERSION_17 && header.version != LDML_KMXPLUS_VERSION_19) { + return false; + } + + if(!COMP_KMXPLUS_Section_Helper::set(newLayr)) { + return false; + } DebugLog("validating newLayr=%p", newLayr); is_valid = true; if (newLayr == nullptr) { @@ -685,68 +1049,75 @@ COMP_KMXPLUS_LAYR_Helper::setLayr(const COMP_KMXPLUS_LAYR *newLayr) { // which validates this section's length. Will be nullptr here if invalid. return true; // not invalid, just missing } - layr = newLayr; - const uint8_t *rawdata = reinterpret_cast(newLayr); - rawdata += LDML_LENGTH_LAYR; // skip past non-dynamic portion - // lists - if (layr->listCount > 0) { - lists = reinterpret_cast(rawdata); - } else { - lists = nullptr; - is_valid = false; - assert(is_valid); - } - rawdata += sizeof(COMP_KMXPLUS_LAYR_LIST) * layr->listCount; - // entries - if (layr->layerCount > 0) { - entries = reinterpret_cast(rawdata); - } else { - entries = nullptr; - is_valid = false; - assert(is_valid); - } - rawdata += sizeof(COMP_KMXPLUS_LAYR_ENTRY) * layr->layerCount; - // rows - if (layr->rowCount > 0) { - rows = reinterpret_cast(rawdata); - } else { - rows = nullptr; - is_valid = false; + KMX_DWORD offset = this->header.calculateBaseSize(LDML_LENGTH_LAYR); // skip past non-dynamic portion + + own_forms = false; + if(header.version == LDML_KMXPLUS_VERSION_17) { + // forms (required) + const COMP_KMXPLUS_LAYR_FORM_V17 *forms_v17; + is_valid = is_valid && get_required_section_data_at_offset_and_increment( + data(), header, data()->formCount, offset, forms_v17); assert(is_valid); - } - rawdata += sizeof(COMP_KMXPLUS_LAYR_ROW) * layr->rowCount; - // keys - if (layr->keyCount > 0) { - keys = reinterpret_cast(rawdata); - } else { - keys = nullptr; - is_valid = false; + if(is_valid) { + own_forms = true; + COMP_KMXPLUS_LAYR_FORM_V19 *localForms = new COMP_KMXPLUS_LAYR_FORM_V19[data()->formCount]; + for(KMX_DWORD i = 0; i < data()->formCount; i++) { + localForms[i].hardware = forms_v17[i].hardware; + localForms[i].layer = forms_v17[i].layer; + localForms[i].count = forms_v17[i].count; + localForms[i].minDeviceWidth = forms_v17[i].minDeviceWidth; + + localForms[i].baseLayout = 0; + localForms[i].fontFaceName = 0; + localForms[i].fontSizePct = 100; + localForms[i].flags = 0; + } + forms = localForms; + } + } else { // header.version == LDML_KMXPLUS_VERSION_19 + is_valid = is_valid && get_required_section_data_at_offset_and_increment( + data(), header, data()->formCount, offset, forms); assert(is_valid); } + // entries (required) - note, "entryCount" is called "layerCount" in COMP_KMXPLUS_LAYR + is_valid = is_valid && get_required_section_data_at_offset_and_increment( + data(), header, data()->layerCount, offset, entries); + assert(is_valid); + + // rows (required) + is_valid = is_valid && get_required_section_data_at_offset_and_increment( + data(), header, data()->rowCount, offset, rows); + assert(is_valid); + + // keys (required) + is_valid = is_valid && get_required_section_data_at_offset_and_increment( + data(), header, data()->keyCount, offset, keys); + assert(is_valid); // Now, validate offsets by walking if (is_valid) { - for(KMX_DWORD i = 0; is_valid && i < layr->listCount; i++) { - const COMP_KMXPLUS_LAYR_LIST &list = lists[i]; + for(KMX_DWORD i = 0; is_valid && i < data()->formCount; i++) { + const COMP_KMXPLUS_LAYR_FORM_V19 &form = forms[i]; // is the count off the end? DebugLog( - " %d: hardware s#0x%X, layers %d..%d, minDeviceWidth %.1fmm", i, list.hardware, list.layer, - list.layer + list.count - 1, list.minDeviceWidth * (double)0.1); - if ((list.layer >= layr->layerCount) || (list.layer + list.count > layr->layerCount)) { - DebugLog("COMP_KMXPLUS_LAYR_Helper: list[%d] would access layer %d+%d, > count %d", - i, list.layer, list.count, layr->layerCount); + " %d: hardware s#0x%X, layers %d..%d, minDeviceWidth %.1fmm", i, form.hardware, form.layer, + form.layer + form.count - 1, form.minDeviceWidth * (double)0.1); + if ((form.layer >= data()->layerCount) || (form.layer + form.count > data()->layerCount)) { + DebugLog("COMP_KMXPLUS_LAYR_Helper: form[%d] would access layer %d+%d, > count %d", + i, form.layer, form.count, data()->layerCount); is_valid = false; assert(is_valid); } + // TODO-EMBED-OSK-IN-KMX: other validations needed? } - for(KMX_DWORD i = 0; is_valid && i < layr->layerCount; i++) { + for(KMX_DWORD i = 0; is_valid && i < data()->layerCount; i++) { const COMP_KMXPLUS_LAYR_ENTRY &entry = entries[i]; // is the count off the end? DebugLog( " %d: id s#0x%X, rows %d..%d, modifier=0x%X", i, entry.id, entry.row, entry.row+entry.count-1, entry.mod); - if ((entry.row >= layr->rowCount) || (entry.row + entry.count > layr->rowCount)) { + if ((entry.row >= data()->rowCount) || (entry.row + entry.count > data()->rowCount)) { DebugLog("COMP_KMXPLUS_LAYR_Helper: entry[%d] would access row %d+%d, > count %d", - i, entry.row, entry.count, layr->rowCount); + i, entry.row, entry.count, data()->rowCount); is_valid = false; assert(is_valid); } @@ -756,19 +1127,19 @@ COMP_KMXPLUS_LAYR_Helper::setLayr(const COMP_KMXPLUS_LAYR *newLayr) { return false; } } - for(KMX_DWORD i = 0; is_valid && i < layr->rowCount; i++) { + for(KMX_DWORD i = 0; is_valid && i < data()->rowCount; i++) { const COMP_KMXPLUS_LAYR_ROW &row = rows[i]; // is the count off the end? - if ((row.key >= layr->keyCount) || (row.key + row.count > layr->keyCount)) { + if ((row.key >= data()->keyCount) || (row.key + row.count > data()->keyCount)) { DebugLog("COMP_KMXPLUS_LAYR_Helper: row[%d] would access key %d+%d, > count %d", - i, row.key, row.count, layr->keyCount); + i, row.key, row.count, data()->keyCount); is_valid = false; assert(is_valid); } } } // Return results - DebugLog("COMP_KMXPLUS_LAYR_Helper.setLayr(): %s", is_valid ? "valid" : "invalid"); + DebugLog("COMP_KMXPLUS_LAYR_Helper.set(): %s", is_valid ? "valid" : "invalid"); assert(is_valid); return is_valid; } @@ -777,18 +1148,18 @@ bool COMP_KMXPLUS_LAYR_Helper::valid() const { return is_valid; } -const COMP_KMXPLUS_LAYR_LIST * -COMP_KMXPLUS_LAYR_Helper::getList(KMX_DWORD list) const { - if (!valid() || list >= layr->listCount) { +const COMP_KMXPLUS_LAYR_FORM_V19 * +COMP_KMXPLUS_LAYR_Helper::getForm(KMX_DWORD form) const { + if (!valid() || form >= data()->formCount) { assert(false); return nullptr; } - return lists + list; + return forms + form; } const COMP_KMXPLUS_LAYR_ENTRY * COMP_KMXPLUS_LAYR_Helper::getEntry(KMX_DWORD entry) const { - if (!valid() || entry >= layr->layerCount) { + if (!valid() || entry >= data()->layerCount) { assert(false); return nullptr; } @@ -797,7 +1168,7 @@ COMP_KMXPLUS_LAYR_Helper::getEntry(KMX_DWORD entry) const { const COMP_KMXPLUS_LAYR_ROW * COMP_KMXPLUS_LAYR_Helper::getRow(KMX_DWORD row) const { - if (!valid() || row >= layr->rowCount) { + if (!valid() || row >= data()->rowCount) { assert(false); return nullptr; } @@ -806,7 +1177,7 @@ COMP_KMXPLUS_LAYR_Helper::getRow(KMX_DWORD row) const { const COMP_KMXPLUS_LAYR_KEY * COMP_KMXPLUS_LAYR_Helper::getKey(KMX_DWORD key) const { - if (!valid() || key >= layr->keyCount) { + if (!valid() || key >= data()->keyCount) { assert(false); return nullptr; } @@ -814,14 +1185,13 @@ COMP_KMXPLUS_LAYR_Helper::getKey(KMX_DWORD key) const { } bool -COMP_KMXPLUS_KEYS::valid(KMX_DWORD _kmn_unused(length)) const { - if (header.size < sizeof(*this) +COMP_KMXPLUS_KEYS::valid(COMP_KMXPLUS_HEADER const &header, KMX_DWORD _kmn_unused(length)) const { + if(!is_block_valid(header, header.headerSize(), + sizeof(*this) + (keyCount * sizeof(COMP_KMXPLUS_KEYS_KEY)) + (flicksCount * sizeof(COMP_KMXPLUS_KEYS_FLICK_LIST)) + (flickCount * sizeof(COMP_KMXPLUS_KEYS_FLICK_ELEMENT)) - + (kmapCount * sizeof(COMP_KMXPLUS_KEYS_KMAP))) { - DebugLog("header.size < expected size"); - assert(false); + + (kmapCount * sizeof(COMP_KMXPLUS_KEYS_KMAP)))) { return false; } // further validation in the COMP_KMXPLUS_KEYS_Helper helper obj @@ -829,11 +1199,20 @@ COMP_KMXPLUS_KEYS::valid(KMX_DWORD _kmn_unused(length)) const { } -COMP_KMXPLUS_KEYS_Helper::COMP_KMXPLUS_KEYS_Helper() : key2(nullptr), is_valid(false) { +COMP_KMXPLUS_KEYS_Helper::COMP_KMXPLUS_KEYS_Helper() : is_valid(false) { } bool -COMP_KMXPLUS_KEYS_Helper::setKeys(const COMP_KMXPLUS_KEYS *newKeys) { +COMP_KMXPLUS_KEYS_Helper::set(const COMP_KMXPLUS_KEYS *newKeys) { + is_valid = false; + keys = nullptr; + flickLists = nullptr; + flickElements = nullptr; + kmap = nullptr; + + if(!COMP_KMXPLUS_Section_Helper::set(newKeys)) { + return false; + } DebugLog("validating newKeys=%p", newKeys); is_valid = true; if (newKeys == nullptr) { @@ -841,46 +1220,36 @@ COMP_KMXPLUS_KEYS_Helper::setKeys(const COMP_KMXPLUS_KEYS *newKeys) { // which validates this section's length. Will be nullptr here if invalid. return true; // not invalid, just missing } - key2 = newKeys; - const uint8_t *rawdata = reinterpret_cast(newKeys); - rawdata += LDML_LENGTH_KEYS; // skip past non-dynamic portion - // keys - if (key2->keyCount > 0) { - keys = reinterpret_cast(rawdata); - } else { - keys = nullptr; - is_valid = false; - assert(is_valid); - } - rawdata += sizeof(COMP_KMXPLUS_KEYS_KEY) * key2->keyCount; - // flicks - if (key2->flicksCount > 0) { - flickLists = reinterpret_cast(rawdata); - } else { - flickLists = nullptr; // not an error - } - rawdata += sizeof(COMP_KMXPLUS_KEYS_FLICK_LIST) * key2->flicksCount; - // flick - if (key2->flickCount > 0) { - flickElements = reinterpret_cast(rawdata); - } else { - flickElements = nullptr; // not an error - } - rawdata += sizeof(COMP_KMXPLUS_KEYS_FLICK_ELEMENT) * key2->flickCount; - // kmap - if (key2->kmapCount > 0) { - kmap = reinterpret_cast(rawdata); - } else { - kmap = nullptr; // not an error - } + + KMX_DWORD offset = this->header.calculateBaseSize(LDML_LENGTH_KEYS); + + // keys (required) + is_valid = is_valid && get_required_section_data_at_offset_and_increment( + data(), header, data()->keyCount, offset, keys); + assert(is_valid); + + // flicks (optional) - note tricky "flicksCount" vs "flickCount" in COMP_KMXPLUS_KEYS + is_valid = is_valid && get_optional_section_data_at_offset_and_increment( + data(), header, data()->flicksCount, offset, flickLists); + assert(is_valid); + + // flick (optional) - note tricky "flicksCount" vs "flickCount" in COMP_KMXPLUS_KEYS + is_valid = is_valid && get_optional_section_data_at_offset_and_increment( + data(), header, data()->flickCount, offset, flickElements); + assert(is_valid); + + // kmap (optional) + is_valid = is_valid && get_optional_section_data_at_offset_and_increment( + data(), header, data()->kmapCount, offset, kmap); + assert(is_valid); // Now, validate offsets by walking if (is_valid) { - for(KMX_DWORD i = 0; is_valid && i < key2->keyCount; i++) { + for(KMX_DWORD i = 0; is_valid && i < data()->keyCount; i++) { const auto &key = keys[i]; // is the count off the end? DebugLoad( " id=0x%X, to=0x%X, flicks=%d", i, key.id, key.to, key.flicks); // TODO-LDML: could dump more fields here - if (key.flicks >0 && key.flicks >= key2->flicksCount) { + if (key.flicks >0 && key.flicks >= data()->flicksCount) { DebugLog("key[%d] has invalid flicks index %d", i, key.flicks); is_valid = false; assert(is_valid); @@ -894,7 +1263,7 @@ COMP_KMXPLUS_KEYS_Helper::setKeys(const COMP_KMXPLUS_KEYS *newKeys) { } } } - for(KMX_DWORD i = 0; is_valid && i < key2->flicksCount; i++) { + for(KMX_DWORD i = 0; is_valid && i < data()->flicksCount; i++) { const auto &e = flickLists[i]; // is the count off the end? DebugLoad(" %d: index %d, count %d", i, e.flick, e.count); @@ -904,13 +1273,13 @@ COMP_KMXPLUS_KEYS_Helper::setKeys(const COMP_KMXPLUS_KEYS *newKeys) { is_valid = false; assert(is_valid); } - } else if ((e.flick >= key2->flickCount) || (e.flick + e.count > key2->flickCount)) { - DebugLog("flicks[%d] would access flick %d+%d, > count %d", i, e.flick, e.count, key2->flickCount); + } else if ((e.flick >= data()->flickCount) || (e.flick + e.count > data()->flickCount)) { + DebugLog("flicks[%d] would access flick %d+%d, > count %d", i, e.flick, e.count, data()->flickCount); is_valid = false; assert(is_valid); } } - for(KMX_DWORD i = 0; is_valid && i < key2->flickCount; i++) { + for(KMX_DWORD i = 0; is_valid && i < data()->flickCount; i++) { const auto &e = flickElements[i]; // validate to is present if (e.to == 0 || e.directions == 0) { @@ -921,8 +1290,8 @@ COMP_KMXPLUS_KEYS_Helper::setKeys(const COMP_KMXPLUS_KEYS *newKeys) { DebugLoad(" %d: to=0x%X, directions=0x%X", i, e.to, e.directions); } // now the kmap - DebugLoad(" kmap count: #0x%X", key2->kmapCount); - for (KMX_DWORD i = 0; i < key2->kmapCount; i++) { + DebugLoad(" kmap count: #0x%X", data()->kmapCount); + for (KMX_DWORD i = 0; i < data()->kmapCount; i++) { DebugLoad(" #0x%d\n", i); auto &entry = kmap[i]; DebugLoad(" vkey\t0x%X", entry.vkey); @@ -933,22 +1302,22 @@ COMP_KMXPLUS_KEYS_Helper::setKeys(const COMP_KMXPLUS_KEYS *newKeys) { assert(false); is_valid = false; } - if (entry.key >= key2->keyCount) { + if (entry.key >= data()->keyCount) { // preposterous key # - DebugLog("kmap[0x%X].key = #0x%X, but that is >= keyCount 0x%X", i, entry.key, key2->keyCount); + DebugLog("kmap[0x%X].key = #0x%X, but that is >= keyCount 0x%X", i, entry.key, data()->keyCount); assert(false); is_valid = false; } } } // Return results - DebugLog("COMP_KMXPLUS_KEYS_Helper.setKeys(): %s", is_valid ? "valid" : "invalid"); + DebugLog("COMP_KMXPLUS_KEYS_Helper.set(): %s", is_valid ? "valid" : "invalid"); return is_valid; } const COMP_KMXPLUS_KEYS_KEY * COMP_KMXPLUS_KEYS_Helper::getKeys(KMX_DWORD i) const { - if (!valid() || i >= key2->keyCount) { + if (!valid() || i >= data()->keyCount) { assert(false); return nullptr; } @@ -957,7 +1326,7 @@ COMP_KMXPLUS_KEYS_Helper::getKeys(KMX_DWORD i) const { const COMP_KMXPLUS_KEYS_KEY* COMP_KMXPLUS_KEYS_Helper::findKeyByStringId(KMX_DWORD strId, KMX_DWORD &i) const { - for (; i < key2->keyCount; i++) { + for (; i < data()->keyCount; i++) { if (keys[i].id == strId) { return &keys[i]; } @@ -967,7 +1336,7 @@ COMP_KMXPLUS_KEYS_Helper::findKeyByStringId(KMX_DWORD strId, KMX_DWORD &i) const const COMP_KMXPLUS_KEYS_KEY* COMP_KMXPLUS_KEYS_Helper::findKeyByStringTo(const std::u16string& str, KMX_DWORD strId, KMX_DWORD &i) const { - for (; i < key2->keyCount; i++) { + for (; i < data()->keyCount; i++) { if (keys[i].flags & LDML_KEYS_KEY_FLAGS_EXTEND) { if (strId != 0 && keys[i].to == strId) { return &keys[i]; @@ -981,7 +1350,7 @@ COMP_KMXPLUS_KEYS_Helper::findKeyByStringTo(const std::u16string& str, KMX_DWORD const COMP_KMXPLUS_KEYS_FLICK_LIST * COMP_KMXPLUS_KEYS_Helper::getFlickLists(KMX_DWORD i) const { - if (!valid() || i >= key2->flicksCount) { + if (!valid() || i >= data()->flicksCount) { assert(false); return nullptr; } @@ -990,7 +1359,7 @@ COMP_KMXPLUS_KEYS_Helper::getFlickLists(KMX_DWORD i) const { const COMP_KMXPLUS_KEYS_FLICK_ELEMENT * COMP_KMXPLUS_KEYS_Helper::getFlickElements(KMX_DWORD i) const { - if (!valid() || i >= key2->flickCount) { + if (!valid() || i >= data()->flickCount) { assert(false); return nullptr; } @@ -999,7 +1368,7 @@ COMP_KMXPLUS_KEYS_Helper::getFlickElements(KMX_DWORD i) const { const COMP_KMXPLUS_KEYS_KMAP * COMP_KMXPLUS_KEYS_Helper::getKmap(KMX_DWORD i) const { - if (!valid() || i >= key2->kmapCount) { + if (!valid() || i >= data()->kmapCount) { assert(false); return nullptr; } @@ -1015,23 +1384,29 @@ COMP_KMXPLUS_KEYS_KEY::get_to_string() const { // LIST bool -COMP_KMXPLUS_LIST::valid(KMX_DWORD _kmn_unused(length)) const { - if (header.size < sizeof(*this) +COMP_KMXPLUS_LIST::valid(COMP_KMXPLUS_HEADER const &header, KMX_DWORD _kmn_unused(length)) const { + if(!is_block_valid(header, header.headerSize(), + sizeof(*this) + (listCount * sizeof(COMP_KMXPLUS_LIST_ITEM)) - + (indexCount * sizeof(COMP_KMXPLUS_LIST_INDEX))) { - DebugLog("header.size < expected size"); - assert(false); + + (indexCount * sizeof(COMP_KMXPLUS_LIST_INDEX)))) { return false; } return true; } -COMP_KMXPLUS_LIST_Helper::COMP_KMXPLUS_LIST_Helper() : list(nullptr), is_valid(false) { +COMP_KMXPLUS_LIST_Helper::COMP_KMXPLUS_LIST_Helper() : is_valid(false) { } bool -COMP_KMXPLUS_LIST_Helper::setList(const COMP_KMXPLUS_LIST *newList) { +COMP_KMXPLUS_LIST_Helper::set(const COMP_KMXPLUS_LIST *newList) { + is_valid = false; + lists = nullptr; + indices = nullptr; + + if(!COMP_KMXPLUS_Section_Helper::set(newList)) { + return false; + } DebugLog("validating newList=%p", newList); is_valid = true; if (newList == nullptr) { @@ -1039,28 +1414,22 @@ COMP_KMXPLUS_LIST_Helper::setList(const COMP_KMXPLUS_LIST *newList) { // which validates this section's length. Will be nullptr here if invalid. return true; // not invalid, just missing } - list = newList; - const uint8_t *rawdata = reinterpret_cast(newList); - rawdata += LDML_LENGTH_LIST; // skip past non-dynamic portion - // lists - if (list->listCount > 0) { - lists = reinterpret_cast(rawdata); - } else { - lists = nullptr; - // not invalid, just empty. - } - rawdata += sizeof(COMP_KMXPLUS_LIST_ITEM) * list->listCount; - // entries - if (list->indexCount > 0) { - indices = reinterpret_cast(rawdata); - } else { - indices = nullptr; - } - // rawdata += sizeof(COMP_KMXPLUS_LIST_INDEX) * list->indexCount; + + KMX_DWORD offset = this->header.calculateBaseSize(LDML_LENGTH_LIST); // skip past non-dynamic portion + + // lists (optional) + is_valid = is_valid && get_optional_section_data_at_offset_and_increment( + data(), header, data()->listCount, offset, lists); + assert(is_valid); + + // indices (optional) + is_valid = is_valid && get_optional_section_data_at_offset_and_increment( + data(), header, data()->indexCount, offset, indices); + assert(is_valid); // Now, validate offsets by walking if (is_valid) { - for (KMX_DWORD i = 0; is_valid && i < list->listCount; i++) { + for (KMX_DWORD i = 0; is_valid && i < data()->listCount; i++) { const auto &e = lists[i]; // is the count off the end? DebugLog("list 0x%X: index %d, count %d", i, e.index, e.count); @@ -1070,28 +1439,28 @@ COMP_KMXPLUS_LIST_Helper::setList(const COMP_KMXPLUS_LIST *newList) { is_valid = false; assert(is_valid); } - } else if ((e.index >= list->indexCount) || (e.index + e.count > list->indexCount)) { - DebugLog("list[%d] would access index %d+%d, > count %d", i, e.index, e.count, list->indexCount); + } else if ((e.index >= data()->indexCount) || (e.index + e.count > data()->indexCount)) { + DebugLog("list[%d] would access index %d+%d, > count %d", i, e.index, e.count, data()->indexCount); is_valid = false; assert(is_valid); } } #if KMXPLUS_DEBUG_LOAD - for (KMX_DWORD i = 0; is_valid && i < list->indexCount; i++) { + for (KMX_DWORD i = 0; is_valid && i < data()->indexCount; i++) { const auto &e = indices[i]; DebugLoad(" index %d: str 0x%X", i, e); } #endif } // Return results - DebugLog("COMP_KMXPLUS_LIST_Helper.setList(): %s", is_valid ? "valid" : "invalid"); + DebugLog("COMP_KMXPLUS_LIST_Helper.set(): %s", is_valid ? "valid" : "invalid"); assert(is_valid); return is_valid; } const COMP_KMXPLUS_LIST_ITEM * COMP_KMXPLUS_LIST_Helper::getList(KMX_DWORD i) const { - if (!valid() || i >= list->listCount) { + if (!valid() || i >= data()->listCount) { assert(false); return nullptr; } @@ -1100,7 +1469,7 @@ COMP_KMXPLUS_LIST_Helper::getList(KMX_DWORD i) const { const COMP_KMXPLUS_LIST_INDEX * COMP_KMXPLUS_LIST_Helper::getIndex(KMX_DWORD i) const { - if (!valid() || i >= list->indexCount) { + if (!valid() || i >= data()->indexCount) { assert(false); return nullptr; } @@ -1111,12 +1480,10 @@ COMP_KMXPLUS_LIST_Helper::getIndex(KMX_DWORD i) const { // USET bool -COMP_KMXPLUS_USET::valid(KMX_DWORD _kmn_unused(length)) const { - if (header.size < sizeof(*this) +COMP_KMXPLUS_USET::valid(COMP_KMXPLUS_HEADER const &header, KMX_DWORD _kmn_unused(length)) const { + if(!is_block_valid(header, header.headerSize(), sizeof(*this) + (usetCount * sizeof(COMP_KMXPLUS_USET_USET)) - + (rangeCount * sizeof(COMP_KMXPLUS_USET_RANGE))) { - DebugLog("header.size < expected size"); - assert(false); + + (rangeCount * sizeof(COMP_KMXPLUS_USET_RANGE)))) { return false; } return true; // see helper @@ -1128,11 +1495,18 @@ COMP_KMXPLUS_USET_RANGE::COMP_KMXPLUS_USET_RANGE(KMX_DWORD s, KMX_DWORD e) : sta COMP_KMXPLUS_USET_RANGE::COMP_KMXPLUS_USET_RANGE(const COMP_KMXPLUS_USET_RANGE &other) : start(other.start), end(other.end) { } -COMP_KMXPLUS_USET_Helper::COMP_KMXPLUS_USET_Helper() : uset(nullptr), is_valid(false), usets(nullptr), ranges(nullptr) { +COMP_KMXPLUS_USET_Helper::COMP_KMXPLUS_USET_Helper() : is_valid(false), usets(nullptr), ranges(nullptr) { } bool -COMP_KMXPLUS_USET_Helper::setUset(const COMP_KMXPLUS_USET *newUset) { +COMP_KMXPLUS_USET_Helper::set(const COMP_KMXPLUS_USET *newUset) { + is_valid = false; + usets = nullptr; + ranges = nullptr; + + if(!COMP_KMXPLUS_Section_Helper::set(newUset)) { + return false; + } DebugLoad("validating newUset=%p", newUset); is_valid = true; if (newUset == nullptr) { @@ -1140,32 +1514,26 @@ COMP_KMXPLUS_USET_Helper::setUset(const COMP_KMXPLUS_USET *newUset) { // which validates this section's length. Will be nullptr here if invalid. return true; // not invalid, just missing } - uset = newUset; - const uint8_t *rawdata = reinterpret_cast(newUset); - rawdata += LDML_LENGTH_USET; // skip past non-dynamic portion - // usets - if (uset->usetCount > 0) { - usets = reinterpret_cast(rawdata); - } else { - usets = nullptr; - // not invalid, just empty. - } - rawdata += sizeof(COMP_KMXPLUS_USET_USET) * uset->usetCount; - // entries - if (uset->rangeCount > 0) { - ranges = reinterpret_cast(rawdata); - } else { - ranges = nullptr; - } + KMX_DWORD offset = this->header.calculateBaseSize(LDML_LENGTH_USET); // skip past non-dynamic portion + + // usets (optional) + is_valid = is_valid && get_optional_section_data_at_offset_and_increment( + data(), header, data()->usetCount, offset, usets); + assert(is_valid); + + // ranges (optional) + is_valid = is_valid && get_optional_section_data_at_offset_and_increment( + data(), header, data()->rangeCount, offset, ranges); + assert(is_valid); // Now, validate offsets by walking // is_valid must be true at this point. - for (KMX_DWORD i = 0; is_valid && i < uset->usetCount; i++) { + for (KMX_DWORD i = 0; is_valid && i < data()->usetCount; i++) { const auto &e = usets[i]; // is the count off the end? DebugLog("uset 0x%X: range %d, count %d, pattern 0x%X", i, e.range, e.count, e.pattern); - if ((e.range >= uset->rangeCount) || (e.range + e.count > uset->rangeCount)) { - DebugLog("uset[%d] would access range %d+%d, > count %d", i, e.range, e.count, uset->rangeCount); + if ((e.range >= data()->rangeCount) || (e.range + e.count > data()->rangeCount)) { + DebugLog("uset[%d] would access range %d+%d, > count %d", i, e.range, e.count, data()->rangeCount); is_valid = false; assert(is_valid); } else { @@ -1195,7 +1563,7 @@ COMP_KMXPLUS_USET_Helper::setUset(const COMP_KMXPLUS_USET *newUset) { } } // Return results - DebugLog("COMP_KMXPLUS_USET_Helper.setUset(): %s", is_valid ? "valid" : "invalid"); + DebugLog("COMP_KMXPLUS_USET_Helper.set(): %s", is_valid ? "valid" : "invalid"); assert(is_valid); return is_valid; } @@ -1244,7 +1612,7 @@ SimpleUSet::dump() const { SimpleUSet COMP_KMXPLUS_USET_Helper::getUset(KMXPLUS_USET i) const { - if (!valid() || i >= uset->usetCount) { + if (!valid() || i >= data()->usetCount) { assert(false); return SimpleUSet(nullptr, 0); // empty set } @@ -1254,7 +1622,7 @@ COMP_KMXPLUS_USET_Helper::getUset(KMXPLUS_USET i) const { const COMP_KMXPLUS_USET_RANGE * COMP_KMXPLUS_USET_Helper::getRange(KMX_DWORD i) const { - if (!valid() || i >= uset->rangeCount) { + if (!valid() || i >= data()->rangeCount) { assert(false); return nullptr; } @@ -1285,81 +1653,110 @@ kmx_plus::kmx_plus(const COMP_KEYBOARD *keyboard, size_t length) assert(valid); return; } - if ( ex->kmxplus.dpKMXPlus + ex->kmxplus.dwKMXPlusSize > length) { + // check individual components to avoid overflow on sum (we'll never get even + // a 2GB file so if both components are < length then we are okay to sum) + if (ex->kmxplus.dpKMXPlus > length || + ex->kmxplus.dwKMXPlusSize > length || + ex->kmxplus.dpKMXPlus + ex->kmxplus.dwKMXPlusSize > length) { DebugLog("dpKMXPlus + dwKMXPlusSize is past the end of the file"); valid = false; assert(valid); return; } + if( ex->kmxplus.dwKMXPlusSize < sizeof(COMP_KMXPLUS_HEADER_17)) { + DebugLog("dwKMXPlusSize is too small to contain a section"); + valid = false; + assert(valid); + return; + } + const uint8_t* rawdata = reinterpret_cast(keyboard); valid = true; - valid = section_from_bytes(rawdata+ex->kmxplus.dpKMXPlus, ex->kmxplus.dwKMXPlusSize, sect) && valid; + COMP_KMXPLUS_HEADER sect_header; + KMX_DWORD fileVersion = determine_file_version_from_bytes(rawdata+ex->kmxplus.dpKMXPlus, ex->kmxplus.dwKMXPlusSize); + if(fileVersion == 0) { + valid = false; + assert(valid); + return; + } + + valid = section_from_bytes(rawdata+ex->kmxplus.dpKMXPlus, ex->kmxplus.dwKMXPlusSize, fileVersion, sect_header, sect) && valid; if (sect == nullptr) { DebugLog("kmx_plus(): 'sect' missing or did not validate"); valid = false; - } else { - // load other sections, validating as we go - // each field will be set to nullptr if validation fails or if the section is missing - valid = get_section_from_sect(sect, bksp) && valid; - valid = get_section_from_sect(sect, disp) && valid; - valid = get_section_from_sect(sect, elem) && valid; - valid = get_section_from_sect(sect, key2) && valid; - valid = get_section_from_sect(sect, layr) && valid; - valid = get_section_from_sect(sect, list) && valid; - valid = get_section_from_sect(sect, loca) && valid; - valid = get_section_from_sect(sect, meta) && valid; - valid = get_section_from_sect(sect, strs) && valid; - valid = get_section_from_sect(sect, tran) && valid; - valid = get_section_from_sect(sect, uset) && valid; - valid = get_section_from_sect(sect, vars) && valid; - - // Initialize the helper objects for sections with dynamic parts. - // Note: all of these setters will be passed 'nullptr' - // if any section had failed validation, or was missing. - // A missing section does not invalidate the kmxplus. - // we attempt to initialize each one - - valid = bkspHelper.setTran(bksp) && valid; // bksp handled by …TRAN_Helper - valid = key2Helper.setKeys(key2) && valid; - valid = layrHelper.setLayr(layr) && valid; - valid = listHelper.setList(list) && valid; - valid = tranHelper.setTran(tran) && valid; - valid = usetHelper.setUset(uset) && valid; + assert(valid); + return; } + + // load other sections, validating as we go each field will be set to nullptr + // if validation fails or if the section is missing and then initialize the + // helper objects for sections with dynamic parts. Note: all of the helper + // setters will be passed 'nullptr' if any section had failed validation, or + // was missing. + // + // A missing section does not invalidate the kmxplus. We attempt to initialize + // each one + valid = get_section_from_sect(sect, sect_header, &bkspHelper, bksp) && valid; // note overload + valid = get_section_from_sect(sect, sect_header, &dispHelper, disp) && valid; + valid = get_section_from_sect(sect, sect_header, &elemHelper, elem) && valid; + valid = get_section_from_sect(sect, sect_header, &key2Helper, key2) && valid; + valid = get_section_from_sect(sect, sect_header, &layrHelper, layr) && valid; + valid = get_section_from_sect(sect, sect_header, &listHelper, list) && valid; + valid = get_section_from_sect(sect, sect_header, &locaHelper, loca) && valid; + valid = get_section_from_sect(sect, sect_header, &metaHelper, meta) && valid; + valid = get_section_from_sect(sect, sect_header, &strsHelper, strs) && valid; + valid = get_section_from_sect(sect, sect_header, &tranHelper, tran) && valid; + valid = get_section_from_sect(sect, sect_header, &usetHelper, uset) && valid; + valid = get_section_from_sect(sect, sect_header, &varsHelper, vars) && valid; } std::u16string -COMP_KMXPLUS_STRS::get(KMX_DWORD entry) const { +COMP_KMXPLUS_STRS::get(const COMP_KMXPLUS_HEADER& header, KMX_DWORD entry) const { assert(entry < count); if (entry >= count) { return std::u16string(); // Fallback: empty string } const KMX_DWORD offset = entries[entry].offset; const KMX_DWORD length = entries[entry].length; - assert(offset+((length+1)*2) <= header.size); // assert not out of bounds - const uint8_t* thisptr = reinterpret_cast(this); - const KMX_WCHAR* start = reinterpret_cast(thisptr+offset); + + // the string is null terminated in the data file, thus length + 1 + auto start = get_section_data_at_offset(this, header, offset, (length + 1) * sizeof(KMX_WCHAR)); + if(!start) { + return std::u16string(); + } return std::u16string(start, length); } -KMX_DWORD COMP_KMXPLUS_STRS::find(const std::u16string& s) const { +std::u16string COMP_KMXPLUS_STRS_Helper::get(KMX_DWORD entry) const { + assert(data()); + if(!data()) return std::u16string(); + return data()->get(header, entry); +} + +KMX_DWORD COMP_KMXPLUS_STRS::find(const COMP_KMXPLUS_HEADER& header, const std::u16string& s) const { if (s.empty()) { return 0; // shortcut } // TODO-LDML: suboptimal, but currently only run from the test runner. Could be a binary search since the strings are already in codepoint order. for (KMX_DWORD i = 0; ifind(header, s); +} + bool -COMP_KMXPLUS_VARS::valid(KMX_DWORD _kmn_unused(length)) const { - if (header.size < sizeof(*this) - + (varCount * sizeof(COMP_KMXPLUS_VARS_ITEM))) { +COMP_KMXPLUS_VARS::valid(COMP_KMXPLUS_HEADER const &header, KMX_DWORD _kmn_unused(length)) const { + if(!is_block_valid(header, header.headerSize(), sizeof(*this) + + (varCount * sizeof(COMP_KMXPLUS_VARS_ITEM)))) { DebugLog("header.size < expected size"); assert(false); return false; @@ -1380,7 +1777,6 @@ const COMP_KMXPLUS_VARS_ITEM *COMP_KMXPLUS_VARS::findByStringId(KMX_DWORD strId) } - } // namespace kmx } // namespace core } // namespace km diff --git a/core/src/kmx/kmx_plus.h b/core/src/kmx/kmx_plus.h index 03738ab578a..ca73505680d 100644 --- a/core/src/kmx/kmx_plus.h +++ b/core/src/kmx/kmx_plus.h @@ -49,15 +49,99 @@ struct COMP_KMXPLUS_TRAN_GROUP; struct COMP_KMXPLUS_TRAN_TRANSFORM; struct COMP_KMXPLUS_TRAN_REORDER; struct COMP_KMXPLUS_STRS; +class COMP_KMXPLUS_STRS_Helper; -struct COMP_KMXPLUS_HEADER { +/** + * @brief Binary-backed version of section header, v17, to be used internally + * only when loading; copied into COMP_KMXPLUS_HEADER for use + */ +struct COMP_KMXPLUS_HEADER_17 { + KMXPLUS_IDENT ident; // 0000 Section name + KMX_DWORD_unaligned size; // 0004 Section length + bool valid(KMX_DWORD length) const; +}; + +/** + * @brief Binary-backed version of section header, v19, to be used internally + * only when loading; copied into COMP_KMXPLUS_HEADER for use + */ +struct COMP_KMXPLUS_HEADER_19 { KMXPLUS_IDENT ident; // 0000 Section name KMX_DWORD_unaligned size; // 0004 Section length + KMX_DWORD_unaligned version; // 0008 Section version bool valid(KMX_DWORD length) const; }; +/** + * @brief In-memory version of section header, copied from binary data e.g. + * COMP_KMXPLUS_HEADER_19, and including file version and other useful metadata + */ +struct COMP_KMXPLUS_HEADER { +private: + KMX_DWORD _fileVersion; + KMX_DWORD _headerSize; +public: + // TODO: read only + KMXPLUS_IDENT ident; + KMX_DWORD size; + KMX_DWORD version; + + inline KMX_DWORD fileVersion() const { + return _fileVersion; + } + + inline KMX_DWORD headerSize() const { + return _headerSize; + } + + /** + * @brief Calculate element size, taking into account change in header size + * from v17 to v19 + * + * @param size + * @return KMX_DWORD + */ + inline KMX_DWORD calculateBaseSize(KMX_DWORD elementSize) { + return elementSize - LDML_LENGTH_HEADER_17 + this->_headerSize; + } + + inline void set(KMX_DWORD fileVersion, KMXPLUS_IDENT identIn, KMX_DWORD sizeIn, KMX_DWORD versionIn = LDML_KMXPLUS_VERSION_17) { + this->_fileVersion = fileVersion; + this->_headerSize = fileVersion == LDML_KMXPLUS_VERSION_17 ? LDML_LENGTH_HEADER_17 : LDML_LENGTH_HEADER_19; + this->ident = identIn; + this->size = sizeIn; + this->version = versionIn; + } +}; + +// Assert that the length matches the declared length +static_assert(sizeof(struct COMP_KMXPLUS_HEADER_17) == LDML_LENGTH_HEADER_17, "mismatched size of section header"); // Assert that the length matches the declared length -static_assert(sizeof(struct COMP_KMXPLUS_HEADER) == LDML_LENGTH_HEADER, "mismatched size of section header"); +static_assert(sizeof(struct COMP_KMXPLUS_HEADER_19) == LDML_LENGTH_HEADER_19, "mismatched size of section header"); + +/* ------------------------------------------------------------------ + * Section helper base class - all sections have a helper class + ------------------------------------------------------------------ */ + +template +class COMP_KMXPLUS_Section_Helper { +private: + const T* _data; + +protected: + const T* data() const { + return _data; + } + +public: + COMP_KMXPLUS_HEADER header; + COMP_KMXPLUS_Section_Helper() : _data(nullptr) {} + virtual ~COMP_KMXPLUS_Section_Helper() {} + virtual bool set(const T* section) { + _data = section; + return true; + } +}; /* ------------------------------------------------------------------ * sect section @@ -69,37 +153,43 @@ struct COMP_KMXPLUS_SECT_ENTRY { }; struct COMP_KMXPLUS_SECT { + // static_assert(std::is_base_of::value, "header must be a descendant of COMP_KMXPLUS_HEADER"); static const KMXPLUS_IDENT IDENT = LDML_SECTIONID_SECT; - COMP_KMXPLUS_HEADER header; + static const KMXPLUS_IDENT IDENT_V19 = LDML_SECTIONID_SEC2; KMX_DWORD_unaligned total; // 0008 KMXPlus entire length KMX_DWORD_unaligned count; // 000C number of section headers COMP_KMXPLUS_SECT_ENTRY entries[]; // 0010 section entries /** * @brief Get the offset of a section - * + * * @param ident section id such as 'strs'. Never 'sect' (the sect table does not list itself!) * @return KMX_DWORD offset from beginning of kmxplus, or 0 if not found */ KMX_DWORD find(KMX_DWORD ident) const; /** * @brief Get the pointer to a specific section - * - * @param ident section id such as 'strs'. Never 'sect' (the sect table does not list itself!) - * @param entryLength on exit, will be set to the possible length of the section (based on the remainder of the KMX+ file) + * + * @param header reference to the 'sect' section header for the file + * @param ident section id such as 'strs'. Never 'sect' (the sect table does not list itself!) + * @param entryLength on exit, will be set to the possible length of the section (based on the remainder of the KMX+ file) * @return pointer to raw bytes of requested section, or nullptr if not found */ - const uint8_t *get(KMX_DWORD ident, KMX_DWORD &entryLength) const; + const uint8_t *get(COMP_KMXPLUS_HEADER const& header, KMX_DWORD ident, KMX_DWORD &entryLength) const; /** * @brief True if section is valid. + * @param header reference to the 'sect' section header for the file + * @param fileLength length of the KMX+ file data (not including KMX data, if KMX+ is embedded) * Does not validate the entire file. */ - bool valid(KMX_DWORD length) const; + bool valid(COMP_KMXPLUS_HEADER const &header, KMX_DWORD fileLength) const; }; // Assert that the length matches the declared length -static_assert(sizeof(struct COMP_KMXPLUS_SECT) == LDML_LENGTH_SECT, "mismatched size of section sect"); +static_assert(sizeof(struct COMP_KMXPLUS_SECT) == LDML_LENGTH_SECT - LDML_LENGTH_HEADER_17, "mismatched size of section sect"); static_assert(sizeof(struct COMP_KMXPLUS_SECT) % 0x4 == 0, "Structs prior to variable part should align to 32-bit boundary"); +class COMP_KMXPLUS_SECT_Helper : public COMP_KMXPLUS_Section_Helper {}; + /* ------------------------------------------------------------------ * elem section ------------------------------------------------------------------ */ @@ -119,7 +209,7 @@ struct COMP_KMXPLUS_ELEM_ELEMENT { * @param length number of elements, including this one * @return the string elements as a string array */ - std::deque loadAsStringList(KMX_DWORD length, const km::core::kmx::COMP_KMXPLUS_STRS &strs) const; + std::deque loadAsStringList(KMX_DWORD length, const km::core::kmx::COMP_KMXPLUS_STRS_Helper &strs) const; /** @return element type */ KMX_DWORD type() const; @@ -132,25 +222,45 @@ struct COMP_KMXPLUS_ELEM_ENTRY { struct COMP_KMXPLUS_ELEM { static const KMXPLUS_IDENT IDENT = LDML_SECTIONID_ELEM; - COMP_KMXPLUS_HEADER header; KMX_DWORD_unaligned count; // 0008 count of str entries COMP_KMXPLUS_ELEM_ENTRY entries[]; // 000C+ entries /** * @brief True if section is valid. + * @param header reference to the 'elem' section header + * @param length length of the section in bytes */ - bool valid(KMX_DWORD length) const; + bool valid(COMP_KMXPLUS_HEADER const &header, KMX_DWORD length) const; + /** + * @brief + * @param header reference to the 'elem' section header * @param elementNumber element number, 0..count-1 - * @param length fillin: length of list + * @param length fillin: length of list * @return pointer to first element of list of length length. or nullptr */ - const COMP_KMXPLUS_ELEM_ELEMENT *getElementList(KMX_DWORD elementNumber, - KMX_DWORD &length) const; + const COMP_KMXPLUS_ELEM_ELEMENT *getElementList( + COMP_KMXPLUS_HEADER const &header, + KMX_DWORD elementNumber, + KMX_DWORD &length + ) const; }; static_assert(sizeof(struct COMP_KMXPLUS_ELEM) % 0x4 == 0, "Structs prior to variable part should align to 32-bit boundary"); -static_assert(sizeof(struct COMP_KMXPLUS_ELEM) == LDML_LENGTH_ELEM, "mismatched size of section elem"); +static_assert(sizeof(struct COMP_KMXPLUS_ELEM) == LDML_LENGTH_ELEM - LDML_LENGTH_HEADER_17, "mismatched size of section elem"); + +class COMP_KMXPLUS_ELEM_Helper : public COMP_KMXPLUS_Section_Helper { +public: + /** + * @param elementNumber element number, 0..count-1 + * @param length fillin: length of list + * @return pointer to first element of list of length length. or nullptr + */ + const COMP_KMXPLUS_ELEM_ELEMENT *getElementList( + KMX_DWORD elementNumber, + KMX_DWORD &length + ) const; +}; /* ------------------------------------------------------------------ * finl section is no more @@ -170,17 +280,20 @@ struct COMP_KMXPLUS_LOCA_ENTRY { struct COMP_KMXPLUS_LOCA { static const KMXPLUS_IDENT IDENT = LDML_SECTIONID_LOCA; - COMP_KMXPLUS_HEADER header; KMX_DWORD_unaligned count; // 0008 number of locales COMP_KMXPLUS_LOCA_ENTRY entries[]; /** * @brief True if section is valid. + * @param header reference to the 'loca' section header + * @param length length of the section in bytes */ - bool valid(KMX_DWORD length) const; + bool valid(COMP_KMXPLUS_HEADER const &header, KMX_DWORD length) const; }; static_assert(sizeof(struct COMP_KMXPLUS_LOCA) % 0x4 == 0, "Structs prior to variable part should align to 32-bit boundary"); -static_assert(sizeof(struct COMP_KMXPLUS_LOCA) == LDML_LENGTH_LOCA, "mismatched size of section loca"); +static_assert(sizeof(struct COMP_KMXPLUS_LOCA) == LDML_LENGTH_LOCA - LDML_LENGTH_HEADER_17, "mismatched size of section loca"); + +class COMP_KMXPLUS_LOCA_Helper : public COMP_KMXPLUS_Section_Helper {}; /* ------------------------------------------------------------------ * meta section @@ -188,7 +301,6 @@ static_assert(sizeof(struct COMP_KMXPLUS_LOCA) == LDML_LENGTH_LOCA, "mismatched struct COMP_KMXPLUS_META { static const KMXPLUS_IDENT IDENT = LDML_SECTIONID_META; - COMP_KMXPLUS_HEADER header; KMXPLUS_STR author; KMXPLUS_STR conform; KMXPLUS_STR layout; @@ -198,8 +310,10 @@ struct COMP_KMXPLUS_META { KMX_DWORD_unaligned settings; /** * @brief True if section is valid. + * @param header reference to the 'meta' section header + * @param length length of the section in bytes */ - bool valid(KMX_DWORD length) const; + bool valid(COMP_KMXPLUS_HEADER const &header, KMX_DWORD length) const; /** @brief True if normalization disabled*/ bool normalization_disabled() const { @@ -207,7 +321,9 @@ struct COMP_KMXPLUS_META { } }; -static_assert(sizeof(struct COMP_KMXPLUS_META) == LDML_LENGTH_META, "mismatched size of section meta"); +static_assert(sizeof(struct COMP_KMXPLUS_META) == LDML_LENGTH_META - LDML_LENGTH_HEADER_17, "mismatched size of section meta"); + +class COMP_KMXPLUS_META_Helper : public COMP_KMXPLUS_Section_Helper {}; /* ------------------------------------------------------------------ * strs section @@ -220,28 +336,29 @@ struct COMP_KMXPLUS_STRS_ENTRY { struct COMP_KMXPLUS_STRS { static const KMXPLUS_IDENT IDENT = LDML_SECTIONID_STRS; - COMP_KMXPLUS_HEADER header; KMX_DWORD_unaligned count; // 0008 count of str entries COMP_KMXPLUS_STRS_ENTRY entries[]; // 0010+ entries /** * @brief Get a string entry * - * @param entry entry number - * @param buf output buffer - * @param bufsiz buffer size in bytes + * @param header reference to the 'strs' section header + * @param entry entry number * @return nullptr or a pointer to the output buffer */ - std::u16string get(KMX_DWORD entry) const; + std::u16string get(const COMP_KMXPLUS_HEADER& header, KMX_DWORD entry) const; /** * Slow search + * @param header reference to the 'strs' section header */ - KMX_DWORD find(const std::u16string&) const; + KMX_DWORD find(const COMP_KMXPLUS_HEADER& header, const std::u16string&) const; /** * @brief True if section is valid. + * @param header reference to the 'strs' section header + * @param length length of the section in bytes */ - bool valid(KMX_DWORD length) const; + bool valid(const COMP_KMXPLUS_HEADER& header, KMX_DWORD length) const; /** convert a single char to a string*/ static std::u16string str_from_char(KMX_DWORD v); @@ -250,7 +367,13 @@ struct COMP_KMXPLUS_STRS { }; static_assert(sizeof(struct COMP_KMXPLUS_STRS) % 0x4 == 0, "Structs prior to variable part should align to 32-bit boundary"); -static_assert(sizeof(struct COMP_KMXPLUS_STRS) == LDML_LENGTH_STRS, "mismatched size of section strs"); +static_assert(sizeof(struct COMP_KMXPLUS_STRS) == LDML_LENGTH_STRS - LDML_LENGTH_HEADER_17, "mismatched size of section strs"); + +class COMP_KMXPLUS_STRS_Helper : public COMP_KMXPLUS_Section_Helper { +public: + std::u16string get(KMX_DWORD entry) const; + KMX_DWORD find(const std::u16string&) const; +}; /* ------------------------------------------------------------------ * tran section @@ -276,7 +399,6 @@ struct COMP_KMXPLUS_TRAN_REORDER { struct COMP_KMXPLUS_TRAN { static const KMXPLUS_IDENT IDENT = LDML_SECTIONID_TRAN; - COMP_KMXPLUS_HEADER header; KMX_DWORD_unaligned groupCount; KMX_DWORD_unaligned transformCount; KMX_DWORD_unaligned reorderCount; @@ -286,35 +408,35 @@ struct COMP_KMXPLUS_TRAN { // COMP_KMXPLUS_TRAN_REORDER reorders[] /** * @brief True if section is valid. + * @param header reference to the 'tran' section header + * @param length length of the section in bytes */ - bool valid(KMX_DWORD length) const; + bool valid(COMP_KMXPLUS_HEADER const &header, KMX_DWORD length) const; }; -class COMP_KMXPLUS_TRAN_Helper { +class COMP_KMXPLUS_TRAN_Helper : public COMP_KMXPLUS_Section_Helper { public: COMP_KMXPLUS_TRAN_Helper(); /** - * Initialize the helper to point at a layr section. + * Initialize the helper to point at a tran or bksp section. * @return true if valid */ - bool setTran(const COMP_KMXPLUS_TRAN *newTran); - bool valid() const; + virtual bool set(const COMP_KMXPLUS_TRAN *section); + virtual bool valid() const; const COMP_KMXPLUS_TRAN_GROUP *getGroup(KMX_DWORD n) const; const COMP_KMXPLUS_TRAN_TRANSFORM *getTransform(KMX_DWORD n) const; const COMP_KMXPLUS_TRAN_REORDER *getReorder(KMX_DWORD n) const; private: - const COMP_KMXPLUS_TRAN *tran; bool is_valid; const COMP_KMXPLUS_TRAN_GROUP *groups; const COMP_KMXPLUS_TRAN_TRANSFORM *transforms; const COMP_KMXPLUS_TRAN_REORDER *reorders; }; - static_assert(sizeof(struct COMP_KMXPLUS_TRAN) % 0x4 == 0, "Structs prior to variable part should align to 32-bit boundary"); -static_assert(sizeof(struct COMP_KMXPLUS_TRAN) == LDML_LENGTH_TRAN, "mismatched size of section tran"); +static_assert(sizeof(struct COMP_KMXPLUS_TRAN) == LDML_LENGTH_TRAN - LDML_LENGTH_HEADER_17, "mismatched size of section tran"); static_assert(sizeof(struct COMP_KMXPLUS_TRAN_GROUP) == LDML_LENGTH_TRAN_GROUP, "mismatched size of tran group"); static_assert(sizeof(struct COMP_KMXPLUS_TRAN_TRANSFORM) == LDML_LENGTH_TRAN_TRANSFORM, "mismatched size of tran transform"); static_assert(sizeof(struct COMP_KMXPLUS_TRAN_REORDER) == LDML_LENGTH_TRAN_REORDER, "mismatched size of tran reorder"); @@ -333,12 +455,11 @@ static inline bool is_valid_marker(KMX_DWORD marker_no) { * bksp section ------------------------------------------------------------------ */ -typedef COMP_KMXPLUS_TRAN_Helper COMP_KMXPLUS_BKSP_Helper; - struct COMP_KMXPLUS_BKSP : public COMP_KMXPLUS_TRAN { static const KMXPLUS_IDENT IDENT = LDML_SECTIONID_BKSP; }; +typedef COMP_KMXPLUS_TRAN_Helper COMP_KMXPLUS_BKSP_Helper; /* ------------------------------------------------------------------ * vars section @@ -353,61 +474,87 @@ struct COMP_KMXPLUS_VARS_ITEM { struct COMP_KMXPLUS_VARS { static const KMXPLUS_IDENT IDENT = LDML_SECTIONID_VARS; - COMP_KMXPLUS_HEADER header; KMXPLUS_LIST markers; KMX_DWORD_unaligned varCount; COMP_KMXPLUS_VARS_ITEM varEntries[]; /** * @brief True if section is valid. + * @param header reference to the 'vars' section header + * @param length length of the section in bytes */ - bool valid(KMX_DWORD length) const; + bool valid(COMP_KMXPLUS_HEADER const &header, KMX_DWORD length) const; const COMP_KMXPLUS_VARS_ITEM *findByStringId(KMX_DWORD strId) const; }; static_assert(sizeof(struct COMP_KMXPLUS_VARS) % 0x4 == 0, "Structs prior to variable part should align to 32-bit boundary"); -static_assert(sizeof(struct COMP_KMXPLUS_VARS) == LDML_LENGTH_VARS, "mismatched size of section vars"); +static_assert(sizeof(struct COMP_KMXPLUS_VARS) == LDML_LENGTH_VARS - LDML_LENGTH_HEADER_17, "mismatched size of section vars"); static_assert(sizeof(struct COMP_KMXPLUS_VARS_ITEM) == LDML_LENGTH_VARS_ITEM, "mismatched size of vars item"); +class COMP_KMXPLUS_VARS_Helper : public COMP_KMXPLUS_Section_Helper {}; + /* ------------------------------------------------------------------ * disp section ------------------------------------------------------------------ */ -struct COMP_KMXPLUS_DISP_ENTRY { +struct COMP_KMXPLUS_DISP_ENTRY_17 { KMXPLUS_STR to; KMXPLUS_STR id; KMXPLUS_STR display; }; +struct COMP_KMXPLUS_DISP_ENTRY_19 { + KMXPLUS_STR toId; + KMXPLUS_STR display; + KMX_DWORD_unaligned flags; +}; + struct COMP_KMXPLUS_DISP { static const KMXPLUS_IDENT IDENT = LDML_SECTIONID_DISP; - COMP_KMXPLUS_HEADER header; KMX_DWORD_unaligned count; KMXPLUS_STR baseCharacter; - COMP_KMXPLUS_DISP_ENTRY entries[]; + COMP_KMXPLUS_DISP_ENTRY_19 entries[]; /** * @brief True if section is valid. + * @param header reference to the 'disp' section header + * @param length length of the section in bytes */ - bool valid(KMX_DWORD length) const; + bool valid(COMP_KMXPLUS_HEADER const &header, KMX_DWORD length) const; + bool valid_19(COMP_KMXPLUS_HEADER const &header, KMX_DWORD length) const; + bool valid_17(COMP_KMXPLUS_HEADER const &header, KMX_DWORD length) const; }; static_assert(sizeof(struct COMP_KMXPLUS_DISP) % 0x4 == 0, "Structs prior to variable part should align to 32-bit boundary"); -static_assert(sizeof(struct COMP_KMXPLUS_DISP) == LDML_LENGTH_DISP, "mismatched size of section disp"); - +static_assert(sizeof(struct COMP_KMXPLUS_DISP) == LDML_LENGTH_DISP - LDML_LENGTH_HEADER_17, "mismatched size of section disp"); +class COMP_KMXPLUS_DISP_Helper : public COMP_KMXPLUS_Section_Helper { + virtual bool set(const COMP_KMXPLUS_DISP *newDisp); +}; /* ------------------------------------------------------------------ * layr section ------------------------------------------------------------------ */ -struct COMP_KMXPLUS_LAYR_LIST { +struct COMP_KMXPLUS_LAYR_FORM_V17 { + KMX_DWORD_unaligned hardware; + KMX_DWORD_unaligned layer; + KMX_DWORD_unaligned count; + KMX_DWORD_unaligned minDeviceWidth; +}; + +struct COMP_KMXPLUS_LAYR_FORM_V19 { KMX_DWORD_unaligned hardware; KMX_DWORD_unaligned layer; KMX_DWORD_unaligned count; KMX_DWORD_unaligned minDeviceWidth; + KMXPLUS_STR baseLayout; // v19: str: identifier for base layout (reserved) + KMXPLUS_STR fontFaceName; // v19: str: font face name + KMX_DWORD_unaligned fontSizePct; // v19: font size in % of default size + KMX_DWORD_unaligned flags; // v19: flags }; -static_assert(sizeof(struct COMP_KMXPLUS_LAYR_LIST) == LDML_LENGTH_LAYR_LIST, "mismatched size of COMP_KMXPLUS_LAYR_LIST"); +static_assert(sizeof(struct COMP_KMXPLUS_LAYR_FORM_V17) == LDML_LENGTH_LAYR_FORM_V17, "mismatched size of COMP_KMXPLUS_LAYR_FORM_V17"); +static_assert(sizeof(struct COMP_KMXPLUS_LAYR_FORM_V19) == LDML_LENGTH_LAYR_FORM_V19, "mismatched size of COMP_KMXPLUS_LAYR_FORM_V19"); struct COMP_KMXPLUS_LAYR_ENTRY { KMXPLUS_STR id; @@ -434,66 +581,74 @@ static_assert(sizeof(struct COMP_KMXPLUS_LAYR_KEY) == LDML_LENGTH_LAYR_KEY, "mis struct COMP_KMXPLUS_LAYR { static const KMXPLUS_IDENT IDENT = LDML_SECTIONID_LAYR; - COMP_KMXPLUS_HEADER header; - KMX_DWORD_unaligned listCount; + KMX_DWORD_unaligned formCount; KMX_DWORD_unaligned layerCount; KMX_DWORD_unaligned rowCount; KMX_DWORD_unaligned keyCount; // entries, rows, and keys have a dynamic offset // use COMP_KMXPLUS_LAYR_Helper to access. // - // COMP_KMXPLUS_LAYR_LIST lists[]; + // COMP_KMXPLUS_LAYR_FORM forms[]; // COMP_KMXPLUS_LAYR_ENTRY entries[]; // COMP_KMXPLUS_LAYR_ROW rows[]; // COMP_KMXPLUS_LAYR_KEY keys[]; /** * @brief True if section is valid. + * @param header reference to the 'layr' section header + * @param length length of the section in bytes */ - bool valid(KMX_DWORD length) const; + bool valid(COMP_KMXPLUS_HEADER const &header, KMX_DWORD length) const; + bool valid_19(COMP_KMXPLUS_HEADER const &header, KMX_DWORD length) const; + bool valid_17(COMP_KMXPLUS_HEADER const &header, KMX_DWORD length) const; }; /** * Helper accessor for the dynamic part of a layr section. */ -class COMP_KMXPLUS_LAYR_Helper { +class COMP_KMXPLUS_LAYR_Helper : public COMP_KMXPLUS_Section_Helper { public: COMP_KMXPLUS_LAYR_Helper(); + ~COMP_KMXPLUS_LAYR_Helper() { + if(own_forms) { + delete [] forms; + forms = nullptr; + } + } /** * Initialize the helper to point at a layr section. * @return true if valid */ - bool setLayr(const COMP_KMXPLUS_LAYR *newLayr); - bool valid() const; + virtual bool set(const COMP_KMXPLUS_LAYR *newLayr); + virtual bool valid() const; /** - * @param list index from 0 to layr->listCount + * @param form index from 0 to layr->formCount */ - const COMP_KMXPLUS_LAYR_LIST *getList(KMX_DWORD list) const; + const COMP_KMXPLUS_LAYR_FORM_V19 *getForm(KMX_DWORD form) const; /** - * @param entry index value: COMP_KMXPLUS_LAYR_LIST.layer but less than COMP_KMXPLUS_LAYR_LIST.layer+COMP_KMXPLUS_LAYR_LIST.count + * @param entry index value: COMP_KMXPLUS_LAYR_FORM.layer but less than COMP_KMXPLUS_LAYR_FORM.layer+COMP_KMXPLUS_LAYR_FORM.count */ const COMP_KMXPLUS_LAYR_ENTRY *getEntry(KMX_DWORD entry) const; const COMP_KMXPLUS_LAYR_ROW *getRow(KMX_DWORD row) const; const COMP_KMXPLUS_LAYR_KEY *getKey(KMX_DWORD key) const; private: - const COMP_KMXPLUS_LAYR *layr; bool is_valid; - const COMP_KMXPLUS_LAYR_LIST *lists; + bool own_forms; + const COMP_KMXPLUS_LAYR_FORM_V19 *forms; const COMP_KMXPLUS_LAYR_ENTRY *entries; const COMP_KMXPLUS_LAYR_ROW *rows; const COMP_KMXPLUS_LAYR_KEY *keys; }; static_assert(sizeof(struct COMP_KMXPLUS_LAYR) % 0x4 == 0, "Structs prior to variable part should align to 32-bit boundary"); -static_assert(sizeof(struct COMP_KMXPLUS_LAYR) == LDML_LENGTH_LAYR, "mismatched size of section layr"); +static_assert(sizeof(struct COMP_KMXPLUS_LAYR) == LDML_LENGTH_LAYR - LDML_LENGTH_HEADER_17, "mismatched size of section layr"); /* ------------------------------------------------------------------ * key2 section ------------------------------------------------------------------ */ struct COMP_KMXPLUS_KEYS { static const KMXPLUS_IDENT IDENT = LDML_SECTIONID_KEYS; - COMP_KMXPLUS_HEADER header; KMX_DWORD_unaligned keyCount; KMX_DWORD_unaligned flicksCount; KMX_DWORD_unaligned flickCount; @@ -505,8 +660,10 @@ struct COMP_KMXPLUS_KEYS { /** * @brief True if section is valid. + * @param header reference to the 'keys' section header + * @param length length of the section in bytes */ - bool valid(KMX_DWORD length) const; + bool valid(COMP_KMXPLUS_HEADER const &header, KMX_DWORD length) const; }; struct COMP_KMXPLUS_KEYS_FLICK_ELEMENT { @@ -541,15 +698,15 @@ struct COMP_KMXPLUS_KEYS_KMAP { KMX_DWORD_unaligned key; // index into key subtable }; -class COMP_KMXPLUS_KEYS_Helper { +class COMP_KMXPLUS_KEYS_Helper : public COMP_KMXPLUS_Section_Helper { public: COMP_KMXPLUS_KEYS_Helper(); /** * Initialize the helper to point at a layr section. * @return true if valid */ - bool setKeys(const COMP_KMXPLUS_KEYS *newKeys); - inline bool valid() const { return is_valid; } + virtual bool set(const COMP_KMXPLUS_KEYS *newKeys); + virtual bool valid() const { return is_valid; } const COMP_KMXPLUS_KEYS_KEY *getKeys(KMX_DWORD key) const; const COMP_KMXPLUS_KEYS_FLICK_LIST *getFlickLists(KMX_DWORD list) const; @@ -573,7 +730,6 @@ class COMP_KMXPLUS_KEYS_Helper { const COMP_KMXPLUS_KEYS_KEY *findKeyByStringTo(const std::u16string& str, KMX_DWORD strId, KMX_DWORD &index) const; private: - const COMP_KMXPLUS_KEYS *key2; bool is_valid; const COMP_KMXPLUS_KEYS_KEY *keys; const COMP_KMXPLUS_KEYS_FLICK_LIST *flickLists; @@ -586,14 +742,13 @@ static_assert(sizeof(struct COMP_KMXPLUS_KEYS_FLICK_ELEMENT) == LDML_LENGTH_KEYS static_assert(sizeof(struct COMP_KMXPLUS_KEYS_FLICK_LIST) == LDML_LENGTH_KEYS_FLICK_LIST, "mismatched size of key2.flicks"); static_assert(sizeof(struct COMP_KMXPLUS_KEYS_KMAP) == LDML_LENGTH_KEYS_KMAP, "mismatched size of key2.kmap"); static_assert(sizeof(struct COMP_KMXPLUS_KEYS) % 0x4 == 0, "Structs prior to variable part should align to 32-bit boundary"); -static_assert(sizeof(struct COMP_KMXPLUS_KEYS) == LDML_LENGTH_KEYS, "mismatched size of section key2"); +static_assert(sizeof(struct COMP_KMXPLUS_KEYS) == LDML_LENGTH_KEYS - LDML_LENGTH_HEADER_17, "mismatched size of section key2"); /* ------------------------------------------------------------------ * list section ------------------------------------------------------------------ */ struct COMP_KMXPLUS_LIST { static const KMXPLUS_IDENT IDENT = LDML_SECTIONID_LIST; - COMP_KMXPLUS_HEADER header; KMX_DWORD_unaligned listCount; KMX_DWORD_unaligned indexCount; // see helper for: lists sub-table @@ -601,8 +756,10 @@ struct COMP_KMXPLUS_LIST { /** * @brief True if section is valid. + * @param header reference to the 'list' section header + * @param length length of the section in bytes */ - bool valid(KMX_DWORD length) const; + bool valid(COMP_KMXPLUS_HEADER const &header, KMX_DWORD length) const; }; /** @@ -619,21 +776,20 @@ struct COMP_KMXPLUS_LIST_ITEM { typedef KMX_DWORD_unaligned COMP_KMXPLUS_LIST_INDEX; -class COMP_KMXPLUS_LIST_Helper { +class COMP_KMXPLUS_LIST_Helper : public COMP_KMXPLUS_Section_Helper { public: COMP_KMXPLUS_LIST_Helper(); /** * Initialize the helper to point at a layr section. * @return true if valid */ - bool setList(const COMP_KMXPLUS_LIST *newList); - inline bool valid() const { return is_valid; } + virtual bool set(const COMP_KMXPLUS_LIST *newList); + virtual bool valid() const { return is_valid; } const COMP_KMXPLUS_LIST_ITEM *getList(KMX_DWORD list) const; const COMP_KMXPLUS_LIST_INDEX *getIndex(KMX_DWORD index) const; private: - const COMP_KMXPLUS_LIST *list; bool is_valid; const COMP_KMXPLUS_LIST_ITEM *lists; const COMP_KMXPLUS_LIST_INDEX *indices; @@ -641,7 +797,7 @@ class COMP_KMXPLUS_LIST_Helper { static_assert(sizeof(struct COMP_KMXPLUS_LIST) % 0x4 == 0, "Structs prior to variable part should align to 32-bit boundary"); -static_assert(sizeof(struct COMP_KMXPLUS_LIST) == LDML_LENGTH_LIST, "mismatched size of section list"); +static_assert(sizeof(struct COMP_KMXPLUS_LIST) == LDML_LENGTH_LIST - LDML_LENGTH_HEADER_17, "mismatched size of section list"); static_assert(sizeof(struct COMP_KMXPLUS_LIST_ITEM) == LDML_LENGTH_LIST_ITEM, "mismatched size of section list.lists subtable"); static_assert(sizeof(COMP_KMXPLUS_LIST_INDEX) == LDML_LENGTH_LIST_INDEX, "mismatched size of section list.indices subtable"); @@ -657,7 +813,6 @@ typedef KMX_DWORD KMXPLUS_USET; struct COMP_KMXPLUS_USET { static const KMX_DWORD IDENT = LDML_SECTIONID_USET; - COMP_KMXPLUS_HEADER header; KMX_DWORD_unaligned usetCount; KMX_DWORD_unaligned rangeCount; // see helper for: usets sub-table @@ -665,8 +820,10 @@ struct COMP_KMXPLUS_USET { /** * @brief True if section is valid. + * @param header reference to the 'uset' section header + * @param length length of the section in bytes */ - bool valid(KMX_DWORD length) const; + bool valid(COMP_KMXPLUS_HEADER const &header, KMX_DWORD length) const; }; /** @@ -706,28 +863,27 @@ class SimpleUSet { std::list ranges; }; -class COMP_KMXPLUS_USET_Helper { +class COMP_KMXPLUS_USET_Helper: public COMP_KMXPLUS_Section_Helper { public: COMP_KMXPLUS_USET_Helper(); /** * Initialize the helper to point at a uset section. * @return true if valid */ - bool setUset(const COMP_KMXPLUS_USET *newUset); - inline bool valid() const { return is_valid; } + virtual bool set(const COMP_KMXPLUS_USET *newUset); + virtual bool valid() const { return is_valid; } SimpleUSet getUset(KMXPLUS_USET list) const; const COMP_KMXPLUS_USET_RANGE *getRange(KMX_DWORD index) const; private: - const COMP_KMXPLUS_USET *uset; bool is_valid; const COMP_KMXPLUS_USET_USET *usets; const COMP_KMXPLUS_USET_RANGE *ranges; }; static_assert(sizeof(struct COMP_KMXPLUS_USET) % 0x4 == 0, "Structs prior to variable part should align to 32-bit boundary"); -static_assert(sizeof(struct COMP_KMXPLUS_USET) == LDML_LENGTH_USET, "mismatched size of section uset"); +static_assert(sizeof(struct COMP_KMXPLUS_USET) == LDML_LENGTH_USET - LDML_LENGTH_HEADER_17, "mismatched size of section uset"); static_assert(sizeof(struct COMP_KMXPLUS_USET_RANGE) == LDML_LENGTH_USET_RANGE, "mismatched size of section uset.ranges subtable"); static_assert(sizeof(struct COMP_KMXPLUS_USET_USET) == LDML_LENGTH_USET_USET, "mismatched size of section uset.usets subtable"); @@ -747,26 +903,21 @@ class kmx_plus { */ kmx_plus(const COMP_KEYBOARD *keyboard, size_t length); // keep the next elements sorted - const COMP_KMXPLUS_BKSP *bksp; - const COMP_KMXPLUS_DISP *disp; - const COMP_KMXPLUS_ELEM *elem; - const COMP_KMXPLUS_KEYS *key2; - const COMP_KMXPLUS_LAYR *layr; - const COMP_KMXPLUS_LIST *list; - const COMP_KMXPLUS_LOCA *loca; - const COMP_KMXPLUS_META *meta; - const COMP_KMXPLUS_SECT *sect; - const COMP_KMXPLUS_STRS *strs; - const COMP_KMXPLUS_TRAN *tran; - const COMP_KMXPLUS_USET *uset; - const COMP_KMXPLUS_VARS *vars; + // TODO: put data inside the helper only, rename + const COMP_KMXPLUS_BKSP *bksp; COMP_KMXPLUS_TRAN_Helper bkspHelper; + const COMP_KMXPLUS_DISP *disp; COMP_KMXPLUS_DISP_Helper dispHelper; + const COMP_KMXPLUS_ELEM *elem; COMP_KMXPLUS_ELEM_Helper elemHelper; + const COMP_KMXPLUS_KEYS *key2; COMP_KMXPLUS_KEYS_Helper key2Helper; + const COMP_KMXPLUS_LAYR *layr; COMP_KMXPLUS_LAYR_Helper layrHelper; + const COMP_KMXPLUS_LIST *list; COMP_KMXPLUS_LIST_Helper listHelper; + const COMP_KMXPLUS_LOCA *loca; COMP_KMXPLUS_LOCA_Helper locaHelper; + const COMP_KMXPLUS_META *meta; COMP_KMXPLUS_META_Helper metaHelper; + const COMP_KMXPLUS_SECT *sect; COMP_KMXPLUS_SECT_Helper sectHelper; + const COMP_KMXPLUS_STRS *strs; COMP_KMXPLUS_STRS_Helper strsHelper; + const COMP_KMXPLUS_TRAN *tran; COMP_KMXPLUS_TRAN_Helper tranHelper; + const COMP_KMXPLUS_USET *uset; COMP_KMXPLUS_USET_Helper usetHelper; + const COMP_KMXPLUS_VARS *vars; COMP_KMXPLUS_VARS_Helper varsHelper; inline bool is_valid() { return valid; } - COMP_KMXPLUS_BKSP_Helper bkspHelper; - COMP_KMXPLUS_KEYS_Helper key2Helper; - COMP_KMXPLUS_LAYR_Helper layrHelper; - COMP_KMXPLUS_LIST_Helper listHelper; - COMP_KMXPLUS_TRAN_Helper tranHelper; - COMP_KMXPLUS_USET_Helper usetHelper; private: bool valid; // true if valid }; diff --git a/core/src/ldml/ldml_processor.cpp b/core/src/ldml/ldml_processor.cpp index 63e7995273c..93342697cf3 100644 --- a/core/src/ldml/ldml_processor.cpp +++ b/core/src/ldml/ldml_processor.cpp @@ -77,7 +77,7 @@ ldml_processor::ldml_processor(std::u16string const& kb_name, const std::vector< assert(false); return; } - str = kplus.strs->get(keyEntry->to); + str = kplus.strsHelper.get(keyEntry->to); } else { str = keyEntry->get_to_string(); } diff --git a/core/src/ldml/ldml_transforms.cpp b/core/src/ldml/ldml_transforms.cpp index 8e37011bda4..ca729a92487 100644 --- a/core/src/ldml/ldml_transforms.cpp +++ b/core/src/ldml/ldml_transforms.cpp @@ -180,7 +180,7 @@ element_list::match_end(const std::u32string &str) const { bool element_list::load(const kmx::kmx_plus &kplus, kmx::KMXPLUS_ELEM id) { KMX_DWORD elementsLength; - auto elements = kplus.elem->getElementList(id, elementsLength); // pointer to beginning of element list + auto elements = kplus.elemHelper.getElementList(id, elementsLength); // pointer to beginning of element list assert((elementsLength == 0) || (elements != nullptr)); // it could be a 0-length list for (size_t i = 0; itype == LDML_VARS_ENTRY_TYPE_SET); assert(toVar->type == LDML_VARS_ENTRY_TYPE_SET); KMX_DWORD fromLength, toLength; - auto *fromList = kplus.elem->getElementList(fromVar->elem, fromLength); - auto *toList = kplus.elem->getElementList(toVar->elem, toLength); + auto *fromList = kplus.elemHelper.getElementList(fromVar->elem, fromLength); + auto *toList = kplus.elemHelper.getElementList(toVar->elem, toLength); assert(fromLength == toLength); assert(fromList != nullptr); assert(toList != nullptr); // populate the deques from the lists - fMapFromList = fromList->loadAsStringList(fromLength, *(kplus.strs)); - fMapToList = toList->loadAsStringList(toLength, *(kplus.strs)); + fMapFromList = fromList->loadAsStringList(fromLength, kplus.strsHelper); + fMapToList = toList->loadAsStringList(toLength, kplus.strsHelper); // did we get the expected items? assert(fMapFromList.size() == fromLength); assert(fMapToList.size() == toLength); @@ -743,8 +743,8 @@ transforms::load( for (KMX_DWORD itemNumber = 0; itemNumber < group->count; itemNumber++) { const kmx::COMP_KMXPLUS_TRAN_TRANSFORM *element = tranHelper.getTransform(group->index + itemNumber); - const std::u32string fromStr = kmx::u16string_to_u32string(kplus.strs->get(element->from)); - const std::u32string toStr = kmx::u16string_to_u32string(kplus.strs->get(element->to)); + const std::u32string fromStr = kmx::u16string_to_u32string(kplus.strsHelper.get(element->from)); + const std::u32string toStr = kmx::u16string_to_u32string(kplus.strsHelper.get(element->to)); KMX_DWORD mapFrom = element->mapFrom; // copy, because of alignment KMX_DWORD mapTo = element->mapTo; // copy, because of alignment assert(!fromStr.empty()); diff --git a/core/tests/unit/kmnkbd/actions_get_api.tests.cpp b/core/tests/unit/kmnkbd/actions_get_api.tests.cpp index f9b7c75dac5..3566add5ec3 100644 --- a/core/tests/unit/kmnkbd/actions_get_api.tests.cpp +++ b/core/tests/unit/kmnkbd/actions_get_api.tests.cpp @@ -48,7 +48,7 @@ void teardown() { void setup(const km_core_cu *app_context, const km_core_cu *cached_context, int actions_code_points_to_delete, const std::u32string actions_output) { teardown(); - km::core::path path = km::core::path::join(arg_path, "..", "ldml", "keyboards", "k_001_tiny.kmx"); + km::core::path path = km::core::path::join(arg_path, "..", "ldml", "keyboards", "17.0", "k_001_tiny.kmx"); auto blob = km::tests::load_kmx_file(path.native().c_str()); try_status(km_core_keyboard_load_from_blob(path.stem().c_str(), blob.data(), blob.size(), &test_kb)); try_status(km_core_state_create(test_kb, test_env_opts, &test_state)); diff --git a/core/tests/unit/kmnkbd/actions_normalize.tests.cpp b/core/tests/unit/kmnkbd/actions_normalize.tests.cpp index f71ef1cd3f1..cf2e7f4f022 100644 --- a/core/tests/unit/kmnkbd/actions_normalize.tests.cpp +++ b/core/tests/unit/kmnkbd/actions_normalize.tests.cpp @@ -46,7 +46,7 @@ void teardown() { void setup(const km_core_cu *app_context, const km_core_cu *cached_context_string, const km_core_context_item *cached_context_items, int actions_code_points_to_delete, const std::u32string actions_output) { teardown(); - km::core::path path = km::core::path::join(arg_path, "..", "ldml", "keyboards", "k_001_tiny.kmx"); + km::core::path path = km::core::path::join(arg_path, "..", "ldml", "keyboards", "17.0", "k_001_tiny.kmx"); auto blob = km::tests::load_kmx_file(path.native().c_str()); try_status(km_core_keyboard_load_from_blob(path.stem().c_str(), blob.data(), blob.size(), &test_kb)); try_status(km_core_state_create(test_kb, test_env_opts, &test_state)); diff --git a/core/tests/unit/ldml/context_normalization.tests.cpp b/core/tests/unit/ldml/context_normalization.tests.cpp index ea9d635ab23..b9a0020c911 100644 --- a/core/tests/unit/ldml/context_normalization.tests.cpp +++ b/core/tests/unit/ldml/context_normalization.tests.cpp @@ -42,7 +42,7 @@ void teardown() { void setup(const char *keyboard) { teardown(); - km::core::path path = km::core::path::join(arg_path, "keyboards", keyboard); + km::core::path path = km::core::path::join(arg_path, "keyboards", "17.0", keyboard); auto blob = km::tests::load_kmx_file(path.native().c_str()); try_status(km_core_keyboard_load_from_blob(path.stem().c_str(), blob.data(), blob.size(), &test_kb)); try_status(km_core_state_create(test_kb, test_env_opts, &test_state)); diff --git a/core/tests/unit/ldml/keyboards/keyboards.kpj.in b/core/tests/unit/ldml/keyboards/keyboards-17.kpj.in similarity index 89% rename from core/tests/unit/ldml/keyboards/keyboards.kpj.in rename to core/tests/unit/ldml/keyboards/keyboards-17.kpj.in index 9d704396c4a..9f85a4f73d3 100644 --- a/core/tests/unit/ldml/keyboards/keyboards.kpj.in +++ b/core/tests/unit/ldml/keyboards/keyboards-17.kpj.in @@ -1,7 +1,7 @@ - $PROJECTPATH + $PROJECTPATH/17.0 False False False diff --git a/core/tests/unit/ldml/keyboards/keyboards-19.kpj.in b/core/tests/unit/ldml/keyboards/keyboards-19.kpj.in new file mode 100644 index 00000000000..3785383e049 --- /dev/null +++ b/core/tests/unit/ldml/keyboards/keyboards-19.kpj.in @@ -0,0 +1,13 @@ + + + + $PROJECTPATH/19.0 + False + False + False + keyboard + + + @keyboards_kpj@ + + diff --git a/core/tests/unit/ldml/keyboards/meson.build b/core/tests/unit/ldml/keyboards/meson.build index 0fe3980681b..19e158f482d 100644 --- a/core/tests/unit/ldml/keyboards/meson.build +++ b/core/tests/unit/ldml/keyboards/meson.build @@ -105,14 +105,30 @@ endforeach cfg = configuration_data() cfg.set('keyboards_kpj', test_ldml_keyboards_kpj) -ldml_keyboards_kpj = configure_file( +# Build v17.0 keyboard fixtures + +ldml_keyboards_kpj_17 = configure_file( + configuration: cfg, + input: 'keyboards-17.kpj.in', + output: 'keyboards-17.kpj' +) + +configure_file( + input: ldml_keyboards_kpj_17, + output: tests_from_cldr[0]+'.kmx', + command: kmc_cmd + ['build', '--target-version', '17', '@INPUT@'] +) + +# Build v19.0 keyboard fixtures + +ldml_keyboards_kpj_19 = configure_file( configuration: cfg, - input: 'keyboards.kpj.in', - output: 'keyboards.kpj' + input: 'keyboards-19.kpj.in', + output: 'keyboards19.kpj' ) configure_file( - input: ldml_keyboards_kpj, + input: ldml_keyboards_kpj_19, output: tests_from_cldr[0]+'.kmx', - command: kmc_cmd + ['build', '@INPUT@'] + command: kmc_cmd + ['build', '--target-version', '19', '@INPUT@'] ) diff --git a/core/tests/unit/ldml/kmx_plus.tests.cpp b/core/tests/unit/ldml/kmx_plus.tests.cpp index c0643a5d600..9c705a5ba4f 100644 --- a/core/tests/unit/ldml/kmx_plus.tests.cpp +++ b/core/tests/unit/ldml/kmx_plus.tests.cpp @@ -12,6 +12,16 @@ using namespace km::core::kmx; +namespace km { + namespace core { + namespace kmx { + + // Declarations of helper functions + extern bool header_from_bytes(const uint8_t *data, KMX_DWORD length, KMX_DWORD fileVersion, uint32_t ident, COMP_KMXPLUS_HEADER &out); + } + } +} + TEST(KMXPlusTest, test_COMP_KMXPLUS_KEYS_KEY) { COMP_KMXPLUS_KEYS_KEY e[2] = { { @@ -282,18 +292,26 @@ TEST(KMXPlusTest, COMP_KMXPLUS_STRS_withGoodStrings) { 0x00000000, // null // data @ +7 - 0x0041, // 'a' + 0x0041, // 'A' 0x0000, // null }; - const COMP_KMXPLUS_HEADER *header = reinterpret_cast(mystrs); - ASSERT_TRUE(header->valid(mystrs[1])); - const COMP_KMXPLUS_STRS *strs = reinterpret_cast(mystrs); - ASSERT_TRUE(strs->valid(mystrs[1])); + + COMP_KMXPLUS_HEADER header; + ASSERT_TRUE(header_from_bytes(reinterpret_cast(&mystrs[0]), len * 4, LDML_KMXPLUS_VERSION_17, COMP_KMXPLUS_STRS::IDENT, header)); + + ASSERT_EQ(header.fileVersion(), LDML_KMXPLUS_VERSION_17); + ASSERT_EQ(header.headerSize(), LDML_LENGTH_HEADER_17); + ASSERT_EQ(header.ident, LDML_SECTIONID_STRS); + ASSERT_EQ(header.size, len * 4); + ASSERT_EQ(header.version, LDML_KMXPLUS_VERSION_17); + + const COMP_KMXPLUS_STRS *strs = reinterpret_cast(&mystrs[2]); + ASSERT_TRUE(strs->valid(header, mystrs[1])); KMX_DWORD_unaligned mycount = strs->count; ASSERT_EQ(mycount, 2); - ASSERT_EQ(strs->get(0), u""); - ASSERT_EQ(strs->get(1), u"A"); + ASSERT_EQ(strs->get(header, 0), u""); + ASSERT_EQ(strs->get(header, 1), u"A"); } @@ -320,14 +338,24 @@ TEST(KMXPlusTest, COMP_KMXPLUS_STRS_withBadStrings) { 0x0000, // null }; - const COMP_KMXPLUS_HEADER *header = reinterpret_cast(mystrs); - ASSERT_TRUE(header->valid(mystrs[1])); - const COMP_KMXPLUS_STRS *strs = reinterpret_cast(mystrs); - ASSERT_FALSE(strs->valid(mystrs[1])); + COMP_KMXPLUS_HEADER header; + ASSERT_TRUE(header_from_bytes(reinterpret_cast(mystrs), len * 4, LDML_KMXPLUS_VERSION_17, COMP_KMXPLUS_STRS::IDENT, header)); + + ASSERT_EQ(header.fileVersion(), LDML_KMXPLUS_VERSION_17); + ASSERT_EQ(header.headerSize(), LDML_LENGTH_HEADER_17); + ASSERT_EQ(header.ident, LDML_SECTIONID_STRS); + ASSERT_EQ(header.size, len * 4); + ASSERT_EQ(header.version, LDML_KMXPLUS_VERSION_17); + + const COMP_KMXPLUS_STRS *strs = reinterpret_cast(&mystrs[2]); + ASSERT_FALSE(strs->valid(header, mystrs[1])); } +extern KMX_BOOL km::core::kmx::g_debug_KeymanLog; + GTEST_API_ int main(int argc, char **argv) { testing::InitGoogleTest(&argc, argv); + km::core::kmx::g_debug_KeymanLog = FALSE; return RUN_ALL_TESTS(); } diff --git a/core/tests/unit/ldml/ldml.cpp b/core/tests/unit/ldml/ldml.cpp index f0b1eac4fed..bbc6661043c 100644 --- a/core/tests/unit/ldml/ldml.cpp +++ b/core/tests/unit/ldml/ldml.cpp @@ -516,7 +516,7 @@ int run_all_tests(const km::core::path &source, const km::core::path &compiled, km::tests::LdmlJsonTestSourceFactory json_factory; // adjust path - const auto json_path = km::tests::LdmlJsonTestSourceFactory::kmx_to_test_json(compiled); + const auto json_path = km::tests::LdmlJsonTestSourceFactory::source_to_test_json(source); int json_result = json_factory.load(compiled, json_path); if (json_result != -1) { const km::tests::JsonTestMap& json_tests = json_factory.get_tests(); diff --git a/core/tests/unit/ldml/ldml_test_source.cpp b/core/tests/unit/ldml/ldml_test_source.cpp index 76b7a34a4a4..c20d6dbf2d0 100644 --- a/core/tests/unit/ldml/ldml_test_source.cpp +++ b/core/tests/unit/ldml/ldml_test_source.cpp @@ -554,7 +554,7 @@ bool LdmlJsonTestSource::set_key_from_id(key_event& k, const std::u16string& id) test_assert(kmxplus->key2Helper.valid()); // First, find the string - KMX_DWORD strId = kmxplus->strs->find(id); + KMX_DWORD strId = kmxplus->strsHelper.find(id); if (strId == 0) { return false; } @@ -744,7 +744,7 @@ LdmlJsonRepertoireTestSource::next_action(ldml_action &fillin) { // First, find the string as an id // TODO-LDML: will not work for multi string cases - KMX_DWORD strId = kmxplus->strs->find(chstr); // not an error if chstr is 0, may be single ch + KMX_DWORD strId = kmxplus->strsHelper.find(chstr); // not an error if chstr is 0, may be single ch // OK. Now we can search the keybag KMX_DWORD keyIndex = 0; @@ -813,8 +813,8 @@ LdmlJsonTestSourceFactory::LdmlJsonTestSourceFactory() : test_map() { } km::core::path -LdmlJsonTestSourceFactory::kmx_to_test_json(const km::core::path &kmx) { - km::core::path p = kmx; +LdmlJsonTestSourceFactory::source_to_test_json(const km::core::path &source) { + km::core::path p = source; p.replace_extension(TEST_JSON_SUFFIX); return p; } diff --git a/core/tests/unit/ldml/ldml_test_source.hpp b/core/tests/unit/ldml/ldml_test_source.hpp index 4e511d43bf2..b93f00d7b69 100644 --- a/core/tests/unit/ldml/ldml_test_source.hpp +++ b/core/tests/unit/ldml/ldml_test_source.hpp @@ -164,7 +164,7 @@ class LdmlJsonTestSourceFactory { */ int load(const km::core::path &compiled, const km::core::path &path); - static km::core::path kmx_to_test_json(const km::core::path& kmx); + static km::core::path source_to_test_json(const km::core::path& kmx); const JsonTestMap& get_tests() const; private: diff --git a/core/tests/unit/ldml/meson.build b/core/tests/unit/ldml/meson.build index 21ae49590d6..6a67a2c0314 100644 --- a/core/tests/unit/ldml/meson.build +++ b/core/tests/unit/ldml/meson.build @@ -147,8 +147,14 @@ test('unicode_tests', test_unicode, suite: 'ldml', foreach kbd : tests kbd_src = test_path / kbd + '.xml' - kbd_obj = test_path / kbd + '.kmx' - test(kbd, ldml, args: [kbd_src, kbd_obj], suite: 'ldml-keyboards') + kbd_obj = test_path / '17.0' / kbd + '.kmx' + test(kbd, ldml, args: [kbd_src, kbd_obj], suite: 'ldml-keyboards-17') +endforeach + +foreach kbd : tests + kbd_src = test_path / kbd + '.xml' + kbd_obj = test_path / '19.0' / kbd + '.kmx' + test(kbd, ldml, args: [kbd_src, kbd_obj], suite: 'ldml-keyboards-19') endforeach # Run tests on all invalid keyboards (`invalid_tests` defined in invalid-keyboards/meson.build) diff --git a/developer/docs/help/guides/develop/creating-a-touch-keyboard-layout-for-amharic-the-nitty-gritty.md b/developer/docs/help/guides/develop/creating-a-touch-keyboard-layout-for-amharic-the-nitty-gritty.md index f33ea20c343..9d5c20e5263 100644 --- a/developer/docs/help/guides/develop/creating-a-touch-keyboard-layout-for-amharic-the-nitty-gritty.md +++ b/developer/docs/help/guides/develop/creating-a-touch-keyboard-layout-for-amharic-the-nitty-gritty.md @@ -131,7 +131,7 @@ Character Keys"). Typically, you would use only the "common" virtual key - `U_####[_####]` is used as a shortcut for a key that will output those Unicode values, if no rule matches it. This is similar to the overloaded - behaviour for `K_` ids. Thus `####` must be valid Unicode characters. + behaviour for `K_` ids. Thus `####` must be valid Unicode characters. The square bracket characters `[` and `]` indicate an optional portion of the sequence and are not to be included in the shortcut. E.g. `U_0259` would generate a schwa if no rule matches. It is still valid to @@ -226,338 +226,9 @@ A number of special text labels are recognized as identifying special purpose keys, such as Shift, Backspace, Enter, etc., for which icons are more appropriately used than a text label. A special font including these icons is included with Keyman and automatically embedded and used in any web page using -Keyman. The list of icons in the font will probably be extended in future, but -for now the following special labels are recognized: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Text StringKey CapKey Purpose
`*Shift*`Select Shift layer (inactive). Use on the Shift key to indicate that it switches to the shift layer.
`*Shifted*`Select Shift layer (active). Use on the Shift key on the shift layer to switch back to the default layer.
`*ShiftLock*`Switch to Caps layer (inactive). Not commonly used; generally double-tap on Shift key is used to access the - caps layer.
`*ShiftedLock*`Switch to Caps layer (active). Use on the Shift key on the caps layer to switch back to the default layer. -
`*Enter*` or Return or Enter key (shape determined by writing system direction)
`*LTREnter*`Return or Enter key (left-to-right script shape)
`*RTLEnter*`Return or Enter key (right-to-left script shape)
`*BkSp*` or Backspace key (shape determined by writing system direction)
`*LTRBkSp*`Backspace key (left-to-right script shape)
`*RTLBkSp*`Backspace key (right-to-left script shape)
`*Menu*`Globe key; display the language menu. Use on the `K_LOPT` key.
`*Hide*`Hide the on screen keyboard. Use on the `K_ROPT` key.
`*ABC*`Select alphabetic layer (Uppercase)
`*abc*`Select alphabetic layer (Lowercase)
`*123*`Select the numeric layer
`*Symbol*`Select the symbol layer
`*Currency*`Select the currency symbol layer
`*ZWNJ*` (iOS) or  (Android)Zero Width Non Joiner (shape determined by current platform)
`*ZWNJiOS*`Zero Width Non Joiner (iOS style shape)
`*ZWNJAndroid*`Zero Width Non Joiner (Android style shape)
`*ZWNJGeneric*`Zero Width Non Joiner (not platform-specific)
`*Sp*`Regular space
`*NBSp*`No-Break Space
`*NarNBSp*`Narrow No-Break Space
`*EnQ*`En Quad
`*EmQ*`Em Quad
`*EnSp*`En Space
`*EmSp*`Em Space
`*PunctSp*`Punctuation Space
`*ThSp*`Thin Space
`*HSp*`Hair Space
`*ZWSp*`Zero Width Space
`*ZWJ*`Zero Width Joiner
`*WJ*`Word Joiner
`*CGJ*`Combining Grapheme Joiner
`*LTRM*`Left-to-right Mark
`*RTLM*`Right-to-left Mark
`*SH*`Soft Hyphen
`*HTab*`Horizontal Tabulation
- -The following additional symbols are also available, but intended for working -with legacy desktop layouts, and not recommended for general use: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Text StringKey CapKey Purpose
`*Tab*`Move to next input element in tab order
`*TabLeft*`Move to previous input element in tab order
`*Caps*`Select caps layer (legacy)
`*AltGr*`Select AltGr (Right-Alt) layer (desktop layout compatibility)
`*Alt*`Select Alt layer (desktop layout compatibility)
`*Ctrl*`Select Ctrl layer (desktop layout compatibility)
`*LAlt*`Select Left-Alt layer (desktop layout compatibility)
`*RAlt*`Select Right-Alt layer (desktop layout compatibility)
`*LCtrl*`Select Left-Ctrl layer (desktop layout compatibility)
`*RCtrl*`Select Right-Ctrl layer (desktop layout compatibility)
`*LAltCtrl*`Select Left-Alt-Ctrl layer (desktop layout compatibility)
`*RAltCtrl*`Select Right-Alt-Ctrl layer (desktop layout compatibility)
`*LAltCtrlShift*`Select Left-Alt-Ctrl-Shift layer (desktop layout compatibility)
`*RAltCtrlShift*`Select Right-Alt-Ctrl-Shift layer (desktop layout compatibility)
`*AltShift*`Select Alt-Shift layer (desktop layout compatibility)
`*CtrlShift*`Select Ctrl-Shift layer (desktop layout compatibility)
`*AltCtrlShift*`Select Alt-Ctrl-Shift layer (desktop layout compatibility)
`*LAltShift*`Select Left-Alt-Shift layer (desktop layout compatibility)
`*RAltShift*`Select Right-Alt-Shift layer (desktop layout compatibility)
`*LCtrlShift*`Select Left-Ctrl-Shift layer (desktop layout compatibility)
`*RCtrlShift*`Select Right-Ctrl-Shift layer (desktop layout compatibility)
+Keyman. The list of icons in the font may be extended in future. See +the list of special characters in the +[.keyman-touch-layout reference](../../reference/file-types/keyman-touch-layout). ### Key type diff --git a/developer/docs/help/reference/file-types/keyman-touch-layout.md b/developer/docs/help/reference/file-types/keyman-touch-layout.md index 9f9bbd6fc3f..e689c0aa41f 100644 --- a/developer/docs/help/reference/file-types/keyman-touch-layout.md +++ b/developer/docs/help/reference/file-types/keyman-touch-layout.md @@ -97,7 +97,7 @@ Character Keys"). Typically, you would use only the "common" virtual key - `U_####[_####]` is used as a shortcut for a key that will output those Unicode values, if no rule matches it. This is similar to the overloaded - behaviour for `K_` ids. Thus `####` must be valid Unicode characters. + behaviour for `K_` ids. Thus `####` must be valid Unicode characters. The square bracket characters `[` and `]` indicate an optional portion of the sequence and are not to be included in the shortcut. E.g. `U_0259` would generate a schwa if no rule matches. It is still valid to @@ -192,9 +192,10 @@ A number of special text labels are recognized as identifying special purpose keys, such as Shift, Backspace, Enter, etc., for which icons are more appropriately used than a text label. A special font including these icons is included with Keyman and automatically embedded and used in any web page using -Keyman. The list of icons in the font will probably be extended in future, but -for now the following special labels are recognized: +Keyman. The list of icons in the font may be extended in future. +The special labels are listed below: + @@ -293,8 +294,9 @@ for now the following special labels are recognized: - - + + @@ -524,6 +526,7 @@ with legacy desktop layouts, and not recommended for general use:
`*ZWNJ*` (iOS) or  (Android)Zero Width Non Joiner (shape determined by current platform)Zero Width Non Joiner (shape determined by current platform, + will be on Android)
`*ZWNJiOS*`
+ ### Key type diff --git a/developer/docs/internal/keyman-touch-layout-to-kmx-plus.md b/developer/docs/internal/keyman-touch-layout-to-kmx-plus.md new file mode 100644 index 00000000000..09f24e5bece --- /dev/null +++ b/developer/docs/internal/keyman-touch-layout-to-kmx-plus.md @@ -0,0 +1,174 @@ +--- +title: Mapping from .keyman-touch-layout to LDML +--- + +.keyman-touch-layout is referred to as .ktl for short + +Note that counts and headers will need to merge both .kvks and .keyman-touch-layout. + +# .keyman-touch-layout notes + +``` +touch-layout + tablet|phone|desktop -> determines minDeviceWidth and hardware + font: meta.fontFaceName + fontsize: meta.fontSize + displayUnderlying: lay2.forms.flags.showBaseLayout + defaultHint: resolved at compile time to generate hints for each key + layer[] + id: layr.layers.id + row[] + id (numeric) + key[] + id: key2.keys.id (along with `layer`) + text: key2.keys.text / dis2.display.display + layer: key2.keys.id + nextlayer: key2.switch + font: not supported + fontsize: not supported + sp: map to various 'special' keys by id; see below + pad: adds a "gap" key2.keys + width: map to key2.keys.width / 10 + sk[] + id: key2.keys.id + text: key2.keys.to + layer: key2.keys.id + nextlayer: key2.switch + font: not supported + fontsize: not supported + sp: map to various 'special' keys by id; see below + pad: adds a "gap" key2.keys + width: map to key2.keys.width / 10 + flick{} - see sk + multitap[] - see sk + hint: add to dis2 for the given key +``` + +## `tablet` | `phone` | `desktop` element + +The name of the element drives the selection of the corresponding form data: + + * `tablet` -> sets `minDeviceWidth` to `100` (mm), `hardware` = `touch` + * `phone` -> sets `minDeviceWidth` to `0` (mm), `hardware` = `touch` + * `desktop` -> ignore, as this is not really supported in Keyman at present; + alternative: `hardware` = `us` (4 keyboards in repo that currently use this: + karambolpoular, orma, sxava, sxava_eo) + +## `id` and `layer` properties + +These two properties together will form the identifier of the key. This +identifier will have semantic meaning, and will be used in event generation. + +## `nextlayer` property + +This can be mapped to `key2.switch`. + +## `sp` property + +The value of the `sp` property will drive slightly different behaviors: + + * `0` Default -> Default key type, no additional metadata + * `1` Special -> Sets `dis2.display.flags.isFrameKey` flag + * `2` Special (active) -> Sets `dis2.display.flags.isHighlighted` and + `dis2.display.flags.isFrameKey` flags + * `8`: Deadkey -> Sets `dis2.display.flags.isDeadKey` flag (styling only) + * `9`: Blank -> Sets `dis2.display.flags.isBlankKey` flag (styling and + interactivity) + * `10`: Spacer -> Adds a `gap` key with the required width + +# LDML table notes + +## `key2` + +### C7043.2.15 `key2`—Extended keybag + +| ∆ | Bits | Name | Description | Content +|---|------|-------------|------------------------------------------|----------------------- +| 0 | 32 | ident | `key2` | `key2` +| 4 | 32 | size | int: Length of section | size in bytes +| 8 | 32 | keyCount | int: Number of keys | # keys from .ktl +|12 | 32 | flicksCount | int: Number of flick lists | +|16 | 32 | flickCount | int: Number of flick elements | +|20 | 32 | kmapCount | int: Number of kmap elements | `0` +|24 | var | keys | keys sub-table | +| - | var | flicks | flick lists sub-table | +| - | var | flick | flick elements sub-table | +| - | var | kmap | key map sub-table | + +#### `key2.keys` subtable + +For each `key` element in .ktl: + +| ∆ | Bits | Name | Description | Content +|---|------|---------------- |----------------------------------------------------------|----------------------- +| 0+| 32 | to | str: output string OR UTF-32LE codepoint | `key.text`* +| 4+| 32 | flags | int: per-key flags | bit 1 set if gap +| 8+| 32 | id | str: key id | `key.layer` + ':' + `key.id`? +|12+| 32 | switch | str: layer id to switch to | `key.nextlayer` +|16+| 32 | width | int: key width*10 (supports 0.1 as min width) | `key.width / 10` +|20+| 32 | longPress | list: index into `list` with longPress key id list or 0 | +|24+| 32 | longPressDefault | str: default longpress key id or 0 | +|28+| 32 | multiTap | list: index into `list` sect multiTap key id list or 0 | +|32+| 32 | flicks | int: index into `key2.flicks` subtable | + +* specials will be mapped to the corresponding `dis2`.flags.specialKeyCap value + + +## `layr` + +| ∆ | Bits | Name | Description | Content +|---|------|------------|------------------------------------------|----------------------- +| 0 | 32 | ident | `layr` | `layr` +| 4 | 32 | size | int: Length of section | size in bytes +| 8 | 32 | listCount | int: Number of layer lists | # layer lists +|12 | 32 | layerCount | int: number of layer entries | # layer entries +|16 | 32 | rowCount | int: number of row entries | # row entries +|20 | 32 | keyCount | int: number of key entries | # key entries +|24 | var | lists | layer list sub-table | +| - | var | layers | layers sub-table | +| - | var | rows | rows sub-table | +| - | var | keys | keys sub-table | + +### `layr.lists` + +| ∆ | Bits | Name | Description | Content +|---|------|------------------|--------------------------------------------|----------------------- +| 0+| 32 | hardware | str: name of hardware layout. | `us` (default) or `iso` <- kvkh102 +| 4+| 32 | layer | int: index to first layer element | `0` +| 8+| 32 | count | int: number of layer elements in this list | # layer elements <- modifiers +|12+| 32 | minDeviceWidth | int: min device width in millimeters, or 0 | `0` + +### `layr.layers` + +Each layer corresponds to a .kvks `visualkeyboard/encoding[@name=unicode]/layer` element. + +The `layer[@shift]` mapping is from [`VisualKeyboardLegalShiftStates\[\].name`](https://github.com/keymanapp/keyman/blob/7ac6bfc189333c5758fb14ef1cc0c810e1460b59/common/web/types/src/kvk/visual-keyboard.ts#L63) to keys.key.mod +and `K_MODIFIERFLAG` bitmask (this is not the same as the KVK bitmasks, which shall be deprecated) + +The compiler starts by filling out a blank representation of each modifier layer, then +iterates through the .kvk `visualkeyboard/encoding/layer/key`, keying off the attribute `vkey`. + +| ∆ | Bits | Name | Description | Content +|---|------|------------|------------------------------------------------|----------------------- +| 0+| 32 | id | str: layer id such as `base` or `shift` | <-- modifier name +| 4+| 32 | mod | int: modifier key flags | <-- `K_MODIFIERFLAG` bitmask +| 8+| 32 | row | int: index into rows area (next section) | index to rows +|12+| 32 | count | int: number of `rows` elements for this layer | # rows + +### `layr.rows` + +| ∆ | Bits | Name | Description | Content +|---|------|------------|----------------------------------------|----------------------- +| 0+| 32 | key | int: index into key element | index to keys +| 4+| 32 | count | int: count of key elements in this row | # keys + +Each row should have the full set of 'white' keys, even if they are not all used. Note that `us` +and `iso` keyboards have different row counts for rows 2-4. + +### `layr.keys` + +| ∆ | Bits | Name | Description | Content +|---|------|---------|------------------------------------------|----------------------- +| 0+| 32 | key | str: key id | key id + +? this implies that key.id is required, though key2.keys subtable says it is optional. diff --git a/developer/docs/internal/kvk-to-kmx-plus.md b/developer/docs/internal/kvk-to-kmx-plus.md new file mode 100644 index 00000000000..6f440895aeb --- /dev/null +++ b/developer/docs/internal/kvk-to-kmx-plus.md @@ -0,0 +1,100 @@ +--- +title: Mapping from .kvk to LDML +--- + +Note that counts and headers will need to merge both .kvks and .keyman-touch-layout. + +# .kvks notes + +``` +visualkeyboard + header + version: ignore + kbdname: ignore + flags + key102: determines lay2.forms.hardware `us` or `iso`, and the number of keys in row 4 of hardware + displayunderlying: lay2.forms.flags.showBaseLayout + usealtgr: lay2.forms.flags.chiralSeparate + useunderlying: ignore (unused) + layout: ignore + encoding + @name: only recognize 'unicode' encoding, ignore 'ansi' + fontname, fontsize: meta.fontFaceName, meta.fontFaceSize + layer + @shift: see layr.layers discussion + key + @vkey: map to string +``` + +# LDML table notes + +## `key2` + +### C7043.2.15 `key2`—Extended keybag + +| ∆ | Bits | Name | Description | Content +|---|------|-------------|------------------------------------------|----------------------- +| 0 | 32 | ident | `key2` | `key2` +| 4 | 32 | size | int: Length of section | size in bytes +| 8 | 32 | keyCount | int: Number of keys | # keys from .kvks +|12 | 32 | flicksCount | int: Number of flick lists | `0` +|16 | 32 | flickCount | int: Number of flick elements | `0` +|20 | 32 | kmapCount | int: Number of kmap elements | `0`? +|24 | var | keys | keys sub-table | +| - | var | flicks | flick lists sub-table | +| - | var | flick | flick elements sub-table | +| - | var | kmap | key map sub-table | + +#### `key2.keys` subtable + +For each `key` element in .kvks: + +| ∆ | Bits | Name | Description | Content +|---|------|---------------- |----------------------------------------------------------|----------------------- +| 0+| 32 | to | str: output string OR UTF-32LE codepoint | content of the `key` element +| 4+| 32 | flags | int: per-key flags | (optimization only) +| 8+| 32 | id | str: key id | key vk name + modifier +|12+| 32 | switch | str: layer id to switch to | `0` +|16+| 32 | width | int: key width*10 (supports 0.1 as min width) | `1` +|20+| 32 | longPress | list: index into `list` with longPress key id list or 0 | `0` +|24+| 32 | longPressDefault | str: default longpress key id or 0 | `0` +|28+| 32 | multiTap | list: index into `list` sect multiTap key id list or 0 | `0` +|32+| 32 | flicks | int: index into `key2.flicks` subtable | `0` + +Graphical keys should have a corresponding element added to `dis2`. + +### `layr.layers` + +Each layer corresponds to a .kvks `visualkeyboard/encoding[@name=unicode]/layer` element. + +The `layer[@shift]` mapping is from [`VisualKeyboardLegalShiftStates[].name`](https://github.com/keymanapp/keyman/blob/7ac6bfc189333c5758fb14ef1cc0c810e1460b59/common/web/types/src/kvk/visual-keyboard.ts#L63) +to keys.key.mod and `K_MODIFIERFLAG` bitmask (this is not the same as the KVK bitmasks, +which shall be deprecated) + +The compiler starts by filling out a blank representation of each modifier layer, then +iterates through the .kvk `visualkeyboard/encoding/layer/key`, keying off the attribute `vkey`. + +| ∆ | Bits | Name | Description | Content +|---|------|------------|------------------------------------------------|----------------------- +| 0+| 32 | id | str: layer id such as `base` or `shift` | <-- modifier name +| 4+| 32 | mod | int: modifier key flags | <-- `K_MODIFIERFLAG` bitmask +| 8+| 32 | row | int: index into rows area (next section) | index to rows +|12+| 32 | count | int: number of `rows` elements for this layer | # rows + +### `layr.rows` + +| ∆ | Bits | Name | Description | Content +|---|------|------------|----------------------------------------|----------------------- +| 0+| 32 | key | int: index into key element | index to keys +| 4+| 32 | count | int: count of key elements in this row | # keys + +Each row should have the full set of 'white' keys, even if they are not all used. Note that `us` +and `iso` keyboards have different row counts for rows 2-4. + +### `layr.keys` + +| ∆ | Bits | Name | Description | Content +|---|------|---------|------------------------------------------|----------------------- +| 0+| 32 | key | str: key id | key id + +? this implies that key.id is required, though key2.keys subtable says it is optional. diff --git a/developer/src/common/web/utils/package.json b/developer/src/common/web/utils/package.json index 3bf769dbad5..cc79ccbb9dc 100644 --- a/developer/src/common/web/utils/package.json +++ b/developer/src/common/web/utils/package.json @@ -10,6 +10,7 @@ ], "dependencies": { "@keymanapp/common-types": "*", + "@keymanapp/ldml-keyboard-constants": "*", "eventemitter3": "^5.0.0", "fast-xml-parser": "^5.2.2", "path-browserify": "^1.0.1", diff --git a/developer/src/common/web/utils/src/compiler-interfaces.ts b/developer/src/common/web/utils/src/compiler-interfaces.ts index 74231aaf2b8..861ba1774b4 100644 --- a/developer/src/common/web/utils/src/compiler-interfaces.ts +++ b/developer/src/common/web/utils/src/compiler-interfaces.ts @@ -1,5 +1,5 @@ import { CompilerCallbacks } from "./compiler-callbacks.js"; -import { ObjectWithCompileContext } from '@keymanapp/common-types'; +import { KMX, ObjectWithCompileContext } from '@keymanapp/common-types'; import { KeymanXMLReader, XML_FILENAME_SYMBOL } from "./xml-utils.js"; /** @@ -406,6 +406,11 @@ export interface CompilerOptions extends CompilerBaseOptions { * Check filename conventions in packages */ checkFilenameConventions?: boolean; + /** + * Target version of Keyman for compiled objects; default is minimum Keyman + * version that supports all features in the object + */ + targetVersion?: KMX.KMX_Version; }; export const defaultCompilerOptions: CompilerOptions = { @@ -417,6 +422,7 @@ export const defaultCompilerOptions: CompilerOptions = { compilerWarningsAsErrors: false, warnDeprecatedCode: true, checkFilenameConventions: false, + targetVersion: undefined, } /** diff --git a/developer/src/common/web/utils/src/index.ts b/developer/src/common/web/utils/src/index.ts index a4141395844..8d95a9de948 100644 --- a/developer/src/common/web/utils/src/index.ts +++ b/developer/src/common/web/utils/src/index.ts @@ -24,7 +24,7 @@ export { TouchLayoutFileReader } from './types/keyman-touch-layout/keyman-touch- export { TouchLayoutFileWriter, TouchLayoutFileWriterOptions } from './types/keyman-touch-layout/keyman-touch-layout-file-writer.js'; export { default as KMXBuilder } from './types/kmx/kmx-builder.js'; -export { default as KMXPlusBuilder} from './types/kmx/kmx-plus-builder/kmx-plus-builder.js'; +export { default as KMXPlusBuilder, SectionBuilders } from './types/kmx/kmx-plus-builder/kmx-plus-builder.js'; export * as LDMLKeyboard from './types/ldml-keyboard/ldml-keyboard-xml.js'; export { LDMLKeyboardTestDataXMLSourceFile } from './types/ldml-keyboard/ldml-keyboard-testdata-xml.js'; @@ -78,3 +78,5 @@ export { getFontFamily, getFontFamilySync } from './font-family.js'; export * as ValidIds from './valid-ids.js'; export * as ProjectLoader from './project-loader.js'; + +export { oskFontMagicToken } from './kmx-plus-osk-token.js'; \ No newline at end of file diff --git a/developer/src/common/web/utils/src/kmx-plus-osk-token.ts b/developer/src/common/web/utils/src/kmx-plus-osk-token.ts new file mode 100644 index 00000000000..a900372e904 --- /dev/null +++ b/developer/src/common/web/utils/src/kmx-plus-osk-token.ts @@ -0,0 +1,17 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +/** + * In a KMX+ file, when we first build it, we reserve space for the font + * facename to be rewritten, with a magic token that the package compiler will + * search for; see kmp-compiler.ts and embed-osk.ts. This is the token that + * marks the rewritten space; it is 32 characters long, to match the maximum + * font facename length (in Windows). + * + * Background: this comes from a legacy design decision, where responsibility for + * font selection is made in the package, referencing the relevant font .ttf + * files. This forces a rewrite of font metadata in .kvk and .kmx files when they + * are compiled into a .kmp file. + */ +export const oskFontMagicToken = "*OSK-FONT-MAGIC-TOKEN-OSK-FONT*"; diff --git a/developer/src/common/web/utils/src/types/kmx/kmx-builder.ts b/developer/src/common/web/utils/src/types/kmx/kmx-builder.ts index a8ce169d303..f9118b4ba59 100644 --- a/developer/src/common/web/utils/src/types/kmx/kmx-builder.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-builder.ts @@ -1,5 +1,10 @@ -import { KMXPlus, KMX } from '@keymanapp/common-types'; +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Build a full KMX+ file + */ import * as r from 'restructure'; +import { KMXPlus, KMX } from '@keymanapp/common-types'; import KMXPlusBuilder from './kmx-plus-builder/kmx-plus-builder.js'; import KMXPlusFile = KMXPlus.KMXPlusFile; @@ -14,22 +19,18 @@ import BUILDER_COMP_GROUP = KMX.BUILDER_COMP_GROUP; import BUILDER_COMP_KEY = KMX.BUILDER_COMP_KEY; export default class KMXBuilder { - file: KMXFile; - base_keyboard: number = 0; - base_kmxplus: number = 0; - comp_header: BUILDER_COMP_KEYBOARD; - comp_kmxplus: BUILDER_COMP_KEYBOARD_KMXPLUSINFO; - comp_stores: {base: number, store: STORE, obj: BUILDER_COMP_STORE}[] = []; - comp_groups: {base: number, group: GROUP, obj: BUILDER_COMP_GROUP, keys: {base: number, key: KEY, obj: BUILDER_COMP_KEY}[]}[] = []; - comp_kmxplus_data: Uint8Array; - writeDebug: boolean = false; - - constructor(file: KMXFile, writeDebug: boolean) { - this.file = file; - this.writeDebug = writeDebug; + private base_keyboard: number = 0; + private base_kmxplus: number = 0; + private comp_header: BUILDER_COMP_KEYBOARD; + private comp_kmxplus: BUILDER_COMP_KEYBOARD_KMXPLUSINFO; + private comp_stores: {base: number, store: STORE, obj: BUILDER_COMP_STORE}[] = []; + private comp_groups: {base: number, group: GROUP, obj: BUILDER_COMP_GROUP, keys: {base: number, key: KEY, obj: BUILDER_COMP_KEY}[]}[] = []; + private comp_kmxplus_data: Uint8Array; + + constructor(private file: KMXFile, private writeDebug: boolean) { } - calculateStringOffsetAndSize(string: string, base: number, requireString: boolean = false) { + private calculateStringOffsetAndSize(string: string, base: number, requireString: boolean = false) { if(string.length == 0 && !requireString) { // Zero length strings take up no space in the file, and // are treated as a 'null string' @@ -38,7 +39,7 @@ export default class KMXBuilder { return [base, base + string.length * 2 + 2]; // include trailing zero } - private build() { + private build(version: number) { this.base_keyboard = 0; this.base_kmxplus = 0; @@ -46,7 +47,7 @@ export default class KMXBuilder { this.comp_header = { dwIdentifier: KMXFile.FILEID_COMPILED, - dwFileVersion: KMXFile.VERSION_170, + dwFileVersion: version, dwCheckSum: 0, // Deprecated in Keyman 16.0 KeyboardID: 0, IsRegistered: 1, @@ -150,17 +151,17 @@ export default class KMXBuilder { return size; } - buildBitmap() { + private buildBitmap() { // TODO return 0; } - buildKMXPlus(base: number) { + private buildKMXPlus(base: number) { if(!(this.file instanceof KMXPlusFile)) { return 0; } - const plusbuilder: KMXPlusBuilder = new KMXPlusBuilder(this.file, this.writeDebug); + const plusbuilder: KMXPlusBuilder = new KMXPlusBuilder(this.file); this.comp_kmxplus_data = plusbuilder.compile(); this.comp_kmxplus = { dpKMXPlus: base, @@ -170,7 +171,7 @@ export default class KMXBuilder { return this.comp_kmxplus.dwKMXPlusSize; } - setString(file: Uint8Array, pos: number, str: string, requireString: boolean = false): void { + private setString(file: Uint8Array, pos: number, str: string, requireString: boolean = false): void { if(requireString && !str.length) { // Just write zero terminator, as r.String for a zero-length string // seems to fail. @@ -183,8 +184,8 @@ export default class KMXBuilder { } } - compile(): Uint8Array { - const fileSize = this.build(); + compile(version: number = KMXFile.VERSION_170): Uint8Array { + const fileSize = this.build(version); const file: Uint8Array = new Uint8Array(fileSize); diff --git a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-disp.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-disp.ts index 6984aa2cfa1..3e7dca1efec 100644 --- a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-disp.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-disp.ts @@ -1,4 +1,4 @@ -import { constants } from '@keymanapp/ldml-keyboard-constants'; +import { constants, KMXPlusVersion } from '@keymanapp/ldml-keyboard-constants'; import { KMXPlus } from "@keymanapp/common-types"; import { build_strs_index, BUILDER_STR_REF, BUILDER_STRS } from "./build-strs.js"; import { BUILDER_SECTION } from './builder-section.js'; @@ -13,9 +13,14 @@ import KMXPlusData = KMXPlus.KMXPlusData; * Builder for the 'disp' section */ interface BUILDER_DISP_ITEM { - to: BUILDER_STR_REF; - id: BUILDER_STR_REF; + // v17 + to: BUILDER_STR_REF; // not used in v19 + id: BUILDER_STR_REF; // not used in v19 display: BUILDER_STR_REF; + + // v19+ + toId: BUILDER_STR_REF; + flags: KMXPlus.DispItemFlags; }; export interface BUILDER_DISP extends BUILDER_SECTION { @@ -24,14 +29,17 @@ export interface BUILDER_DISP extends BUILDER_SECTION { items: BUILDER_DISP_ITEM[]; }; -export function build_disp(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS): BUILDER_DISP { +export function build_disp(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS, version: KMXPlusVersion): BUILDER_DISP { if(!kmxplus.disp.disps.length && !kmxplus.disp.baseCharacter.value) { return null; } const disp: BUILDER_DISP = { - ident: constants.hex_section_id(constants.section.disp), - size: constants.length_disp + constants.length_disp_item * kmxplus.disp.disps.length, + header: { + ident: constants.hex_section_id(constants.section.disp), + size: constants.length_disp + constants.length_disp_item * kmxplus.disp.disps.length, // note size is same in v17 and v19 but content differs + version: version == KMXPlusVersion.Version17 ? KMXPlusVersion.Version17 : KMXPlusVersion.Version19, + }, _offset: 0, count: kmxplus.disp.disps.length, baseCharacter: build_strs_index(sect_strs, kmxplus.disp.baseCharacter), @@ -39,10 +47,17 @@ export function build_disp(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS): BUILD }; for(const item of kmxplus.disp.disps) { + // Only allocate strings that we are actually going to use + const to = version == KMXPlusVersion.Version17 ? build_strs_index(sect_strs, item.to) : 0; + const id = version == KMXPlusVersion.Version17 ? build_strs_index(sect_strs, item.id) : 0; + const toId = version == KMXPlusVersion.Version19 ? build_strs_index(sect_strs, item.toId) : 0; disp.items.push({ - to: build_strs_index(sect_strs, item.to), - id: build_strs_index(sect_strs, item.id), + to, + id, display: build_strs_index(sect_strs, item.display), + // v19 + toId, + flags: item.flags, }); } diff --git a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-elem.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-elem.ts index 8e352d191f5..0844db0f275 100644 --- a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-elem.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-elem.ts @@ -1,4 +1,4 @@ -import { constants } from "@keymanapp/ldml-keyboard-constants"; +import { constants, KMXPlusVersion } from "@keymanapp/ldml-keyboard-constants"; import { KMXPlus, LdmlKeyboardTypes } from "@keymanapp/common-types"; import { build_strs_index, BUILDER_STR_REF, BUILDER_STRS } from "./build-strs.js"; import { BUILDER_SECTION, BUILDER_U32CHAR } from "./builder-section.js"; @@ -46,10 +46,12 @@ function binaryElemCompare(a: BUILDER_ELEM_STRING, b: BUILDER_ELEM_STRING): numb return 0; } -export function build_elem(source_elem: Elem, sect_strs: BUILDER_STRS, sect_uset: BUILDER_USET): BUILDER_ELEM { +export function build_elem(source_elem: Elem, sect_strs: BUILDER_STRS, sect_uset: BUILDER_USET, version: KMXPlusVersion): BUILDER_ELEM { const result: BUILDER_ELEM = { - ident: constants.hex_section_id(constants.section.elem), - size: 0, // finalized below + header: { + ident: constants.hex_section_id(constants.section.elem), + size: 0, // finalized below + }, _offset: 0, count: source_elem.strings.length, strings: [], // finalized below @@ -93,12 +95,12 @@ export function build_elem(source_elem: Elem, sect_strs: BUILDER_STRS, sect_uset // no length gets a zero offset item.offset = 0; } else { - item.offset = offset; + item.offset = offset + constants.headerSizeDelta(version); offset += item.length * constants.length_elem_item_element; } } - result.size = offset; + result.header.size = offset; return result; } diff --git a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-keys.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-keys.ts index fd48ec49ec9..620a1397ece 100644 --- a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-keys.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-keys.ts @@ -76,8 +76,10 @@ export function build_keys(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS, sect_l } const keys: BUILDER_KEYS = { - ident: constants.hex_section_id(constants.section.keys), - size: 0, + header: { + ident: constants.hex_section_id(constants.section.keys), + size: 0, + }, keyCount: kmxplus.keys.keys.length, flicksCount: kmxplus.keys.flicks.length, flickCount: 0, @@ -169,7 +171,7 @@ export function build_keys(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS, sect_l (constants.length_keys_flick_element * keys.flickCount) + (constants.length_keys_flick_list * keys.flicksCount) + (constants.length_keys_kmap * keys.kmapCount); - keys.size = offset; + keys.header.size = offset; return keys; } diff --git a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-layr.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-layr.ts index 07249368f4e..ad95b4efbe0 100644 --- a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-layr.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-layr.ts @@ -1,8 +1,7 @@ -import { constants } from "@keymanapp/ldml-keyboard-constants"; +import { constants, KMXPlusVersion } from "@keymanapp/ldml-keyboard-constants"; import { KMXPlus } from "@keymanapp/common-types"; import { build_strs_index, BUILDER_STR_REF, BUILDER_STRS } from "./build-strs.js"; -import { BUILDER_LIST } from "./build-list.js"; import { BUILDER_SECTION } from "./builder-section.js"; import KMXPlusData = KMXPlus.KMXPlusData; @@ -15,14 +14,18 @@ import StrsItem = KMXPlus.StrsItem; ------------------------------------------------------------------ */ /** - * List of layers, the element + * A form that contains a set of layers, the element */ -interface BUILDER_LAYR_LIST { +interface BUILDER_LAYR_FORM { hardware: BUILDER_STR_REF; // hardware or 'touch' - layer: number; // index of first layer in the list, in the - count: number; // number of layer entries in the list + layer: number; // index of first layer in the form, in the layers subtable + count: number; // number of layer entries in the form minDeviceWidth: number; // width in millimeters _layers: LayrEntry[]; // original layer entry, for in-memory only + baseLayout: BUILDER_STR_REF; // v19: base layout (e.g. 'us') + fontFaceName: BUILDER_STR_REF; // v19: face name of font for key caps + fontSizePct: number; // v19: font size in % of default size for implementation, typically 100 + flags: KMXPlus.LayrFormFlags; // v19: flags }; /** @@ -56,47 +59,54 @@ interface BUILDER_LAYR_KEY { * Builder for the 'keys' section */ export interface BUILDER_LAYR extends BUILDER_SECTION { - listCount: number, // number of entries in lists subtable + formCount: number, // number of entries in forms subtable (renamed in v19, formerly, listCount) layerCount: number, // number of entries in layers subtable rowCount: number, // number of entries in rows subtable keyCount: number, // number of entries in keys subtable - lists: BUILDER_LAYR_LIST[], // subtable of elements + forms: BUILDER_LAYR_FORM[], // subtable of elements (renamed in v19, formerly, lists) layers: BUILDER_LAYR_LAYER[], // subtable of elements rows: BUILDER_LAYR_ROW[], // subtable of elements keys: BUILDER_LAYR_KEY[], // subtable of key entries }; -export function build_layr(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS, sect_list: BUILDER_LIST): BUILDER_LAYR { - if (!kmxplus.layr?.lists) { +export function build_layr(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS, version: KMXPlusVersion): BUILDER_LAYR { + if (!kmxplus.layr?.forms) { return null; // if there aren't any layers at all (which should be an invalid keyboard) } const layr: BUILDER_LAYR = { - ident: constants.hex_section_id(constants.section.layr), - size: constants.length_layr, + header: { + ident: constants.hex_section_id(constants.section.layr), + size: 0, // calculated below + version: version == KMXPlusVersion.Version17 ? KMXPlusVersion.Version17 : KMXPlusVersion.Version19, + }, _offset: 0, - listCount: kmxplus.layr.lists.length, + formCount: kmxplus.layr.forms.length, layerCount: 0, // calculated below rowCount: 0, // calculated below keyCount: 0, // calculated below - lists: [], + forms: [], layers: [], rows: [], - keys: [] + keys: [], }; - layr.lists = kmxplus.layr.lists.map((list) => { - const blist: BUILDER_LAYR_LIST = { - hardware: build_strs_index(sect_strs, list.hardware), + layr.forms = kmxplus.layr.forms.map((form) => { + return { + hardware: build_strs_index(sect_strs, form.hardware), layer: null, // to be set below - _layers: list.layers, - count: list.layers.length, - minDeviceWidth: list.minDeviceWidth, + _layers: form.layers, + count: form.layers.length, + minDeviceWidth: form.minDeviceWidth, + baseLayout: build_strs_index(sect_strs, form.baseLayout), + fontFaceName: build_strs_index(sect_strs, form.fontFaceName), + fontSizePct: form.fontSizePct, + flags: form.flags, }; - return blist; }); - // now sort the lists - layr.lists.sort((a, b) => { + + // now sort the forms + layr.forms.sort((a, b) => { // sort by string # if (a.hardware < b.hardware) { return -1; @@ -112,9 +122,9 @@ export function build_layr(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS, sect_l } }); // Now allocate the layers, rows, and keys - layr.lists.forEach((list) => { - list.layer = layr.layers.length; // index to first layer in list - const blayers = list._layers.map((layer) => { + layr.forms.forEach((form) => { + form.layer = layr.layers.length; // index to first layer in form + const blayers = form._layers.map((layer) => { const blayer: BUILDER_LAYR_LAYER = { _id: layer.id.value, // original id id: build_strs_index(sect_strs, layer.id), @@ -128,7 +138,7 @@ export function build_layr(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS, sect_l // sort the new layers blayers.sort((a, b) => StrsItem.binaryStringCompare(a._id, b._id)); blayers.forEach((layer) => { - layer.row = layr.rows.length; // index to first row in list + layer.row = layr.rows.length; // index to first row in form layer._rows.forEach((row) => { const brow: BUILDER_LAYR_ROW = { key: layr.keys.length, @@ -150,11 +160,16 @@ export function build_layr(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS, sect_l layr.rowCount = layr.rows.length; layr.keyCount = layr.keys.length; - const offset = constants.length_layr + - (constants.length_layr_list * layr.listCount) + - (constants.length_layr_entry * layr.layerCount) + - (constants.length_layr_row * layr.rowCount) + - (constants.length_layr_key * layr.keyCount); - layr.size = offset; + layr.header.size = version == KMXPlusVersion.Version17 + ? constants.length_layr + + (constants.length_layr_form_v17 * layr.formCount) + + (constants.length_layr_entry * layr.layerCount) + + (constants.length_layr_row * layr.rowCount) + + (constants.length_layr_key * layr.keyCount) + : constants.length_layr + + (constants.length_layr_form_v19 * layr.formCount) + + (constants.length_layr_entry * layr.layerCount) + + (constants.length_layr_row * layr.rowCount) + + (constants.length_layr_key * layr.keyCount); return layr; } diff --git a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-list.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-list.ts index 759f6498f4f..787b246b719 100644 --- a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-list.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-list.ts @@ -44,8 +44,10 @@ export function build_list(source_list: List, sect_strs: BUILDER_STRS): BUILDER_ } const result: BUILDER_LIST = { - ident: constants.hex_section_id(constants.section.list), - size: 0, + header: { + ident: constants.hex_section_id(constants.section.list), + size: 0, + }, _offset: 0, listCount: source_list.lists.length, indexCount: 0, @@ -77,7 +79,7 @@ export function build_list(source_list: List, sect_strs: BUILDER_STRS): BUILDER_ const offset = constants.length_list + (constants.length_list_item * result.listCount) + (constants.length_list_index * result.indexCount); - result.size = offset; + result.header.size = offset; return result; } diff --git a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-loca.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-loca.ts index 4a81fcd9abd..7ddd944590c 100644 --- a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-loca.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-loca.ts @@ -20,8 +20,10 @@ export interface BUILDER_LOCA extends BUILDER_SECTION { export function build_loca(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS): BUILDER_LOCA { const loca: BUILDER_LOCA = { - ident: constants.hex_section_id(constants.section.loca), - size: constants.length_loca + constants.length_loca_item * kmxplus.loca.locales.length, + header: { + ident: constants.hex_section_id(constants.section.loca), + size: constants.length_loca + constants.length_loca_item * kmxplus.loca.locales.length, + }, _offset: 0, count: kmxplus.loca.locales.length, items: [] diff --git a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-meta.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-meta.ts index 208051356eb..3f232fc23cc 100644 --- a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-meta.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-meta.ts @@ -25,8 +25,10 @@ export interface BUILDER_META extends BUILDER_SECTION { export function build_meta(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS): BUILDER_META { return { - ident: constants.hex_section_id(constants.section.meta), - size: constants.length_meta, + header: { + ident: constants.hex_section_id(constants.section.meta), + size: constants.length_meta, + }, _offset: 0, author: build_strs_index(sect_strs, kmxplus.meta.author), conform: build_strs_index(sect_strs, kmxplus.meta.conform), diff --git a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-sect.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-sect.ts index b97555d6de0..d18ef6e28c7 100644 --- a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-sect.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-sect.ts @@ -1,4 +1,4 @@ -import { constants } from "@keymanapp/ldml-keyboard-constants"; +import { constants, KMXPlusVersion } from "@keymanapp/ldml-keyboard-constants"; import { BUILDER_SECTION } from "./builder-section.js"; /* ------------------------------------------------------------------ @@ -19,10 +19,14 @@ export interface BUILDER_SECT extends BUILDER_SECTION { items: BUILDER_SECT_ITEM[]; }; -export function build_sect(): BUILDER_SECT { +export function build_sect(version: KMXPlusVersion): BUILDER_SECT { return { - ident: constants.hex_section_id(constants.section.sect), - size: 0, // finalized later + header: { + // v19+ uses ident "sec2" to indicate that we have a `version` header field + ident: constants.hex_section_id(version == KMXPlusVersion.Version17 ? constants.section.sect : constants.sectionname_sec2), + size: 0, // finalized later + version, + }, _offset: 0, total: 0, // finalized later count: 0, // finalized later diff --git a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-strs.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-strs.ts index 89d733f22d3..822f22f168a 100644 --- a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-strs.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-strs.ts @@ -1,4 +1,4 @@ -import { constants } from "@keymanapp/ldml-keyboard-constants"; +import { constants, KMXPlusVersion } from "@keymanapp/ldml-keyboard-constants"; import { KMXPlus } from "@keymanapp/common-types"; import { BUILDER_SECTION } from "./builder-section.js"; @@ -28,10 +28,12 @@ export interface BUILDER_STRS extends BUILDER_SECTION { items: BUILDER_STRS_ITEM[]; }; -export function build_strs(source_strs: Strs): BUILDER_STRS { +export function build_strs(source_strs: Strs, version: KMXPlusVersion): BUILDER_STRS { const result: BUILDER_STRS = { - ident: constants.hex_section_id(constants.section.strs), - size: 0, // finalized later + header: { + ident: constants.hex_section_id(constants.section.strs), + size: 0, // finalized later + }, _offset: 0, count: source_strs.strings.length, items: [], // filled below @@ -43,10 +45,10 @@ export function build_strs(source_strs: Strs): BUILDER_STRS { let offset = constants.length_strs + constants.length_strs_item * result.count; // TODO: consider padding for(const item of result.items) { - item.offset = offset; + item.offset = offset + constants.headerSizeDelta(version); offset += item.length * 2 + 2; /* UTF-16 code units + sizeof null terminator */ } - result.size = offset; + result.header.size = offset; return result; } diff --git a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-tran.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-tran.ts index 2473651b889..aa8bea55ae8 100644 --- a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-tran.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-tran.ts @@ -48,8 +48,10 @@ export function build_tran(source_tran: Tran | Bksp, sect_strs: BUILDER_STRS, se } const tran: BUILDER_TRAN = { - ident: constants.hex_section_id(source_tran.id), - size: 0, // need to compute total transforms + reorders + header: { + ident: constants.hex_section_id(source_tran.id), + size: 0, // need to compute total transforms + reorders + }, _offset: 0, groupCount: source_tran.groups.length, transformCount: 0, @@ -94,7 +96,7 @@ export function build_tran(source_tran: Tran | Bksp, sect_strs: BUILDER_STRS, se tran.transformCount = tran.transforms.length; tran.reorderCount = tran.reorders.length; - tran.size = constants.length_tran + + tran.header.size = constants.length_tran + (constants.length_tran_group * source_tran.groups.length) + (constants.length_tran_transform * tran.transforms.length) + (constants.length_tran_reorder * tran.reorders.length); diff --git a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-uset.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-uset.ts index 1edd4d339fc..2b2d17a904a 100644 --- a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-uset.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-uset.ts @@ -57,10 +57,12 @@ export function build_uset(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS ) : BUI }); const uset: BUILDER_USET = { - ident: constants.hex_section_id(constants.section.uset), - size: constants.length_uset + - (constants.length_uset_uset * usets.length) + - (constants.length_uset_range * ranges.length), + header: { + ident: constants.hex_section_id(constants.section.uset), + size: constants.length_uset + + (constants.length_uset_uset * usets.length) + + (constants.length_uset_range * ranges.length), + }, usetCount: usets.length, rangeCount: ranges.length, usets, diff --git a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-vars.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-vars.ts index 92acbba9be1..8ccc38694df 100644 --- a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-vars.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/build-vars.ts @@ -46,9 +46,11 @@ export function build_vars(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS, sect_e }); const vars: BUILDER_VARS = { - ident: constants.hex_section_id(constants.section.vars), - size: constants.length_vars + - (constants.length_vars_item * kmxplus.vars.totalCount()), + header: { + ident: constants.hex_section_id(constants.section.vars), + size: constants.length_vars + + (constants.length_vars_item * kmxplus.vars.totalCount()), + }, _offset: 0, markers: build_list_index(sect_list, kmxplus.vars.markers), varCount: kmxplus.vars.totalCount(), diff --git a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/builder-section.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/builder-section.ts index a6d81fd25e8..272e17ecb3b 100644 --- a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/builder-section.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/builder-section.ts @@ -1,11 +1,16 @@ +import { KMXPlusVersion } from "@keymanapp/ldml-keyboard-constants"; + /** for a 4-byte section identity */ export type BUILDER_IDENT = number; /** for a single UTF-32 character (Unicode codepoint) */ export type BUILDER_U32CHAR = number; export interface BUILDER_SECTION { - ident: BUILDER_IDENT; - size: number; + header: { + ident: BUILDER_IDENT; + size: number; + version?: KMXPlusVersion; // v19+ + }; _offset: number; // used only for building the output } ; diff --git a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/kmx-plus-builder.ts b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/kmx-plus-builder.ts index acc732c932a..c5cb7330ab3 100644 --- a/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/kmx-plus-builder.ts +++ b/developer/src/common/web/utils/src/types/kmx/kmx-plus-builder/kmx-plus-builder.ts @@ -1,6 +1,6 @@ import * as r from 'restructure'; import { KMXPlus } from "@keymanapp/common-types"; -import { constants, SectionIdent } from '@keymanapp/ldml-keyboard-constants'; +import { constants, KMXPlusVersion, SectionIdent } from '@keymanapp/ldml-keyboard-constants'; import { BUILDER_SECTION } from './builder-section.js'; import { BUILDER_SECT, build_sect } from './build-sect.js'; import { BUILDER_DISP, build_disp } from './build-disp.js'; @@ -20,7 +20,7 @@ import KMXPlusFile = KMXPlus.KMXPlusFile; type BUILDER_BKSP = BUILDER_TRAN; // type BUILDER_FINL = BUILDER_TRAN; -type SectionBuilders = { +export type SectionBuilders = { // [id in SectionIdent]: BUILDER_SECTION; sect?: BUILDER_SECT; bksp?: BUILDER_BKSP; @@ -38,16 +38,12 @@ type SectionBuilders = { }; export default class KMXPlusBuilder { - private file: KMXPlusFile; - //private writeDebug: boolean; - sect : SectionBuilders = { }; - constructor(file: KMXPlusFile, _writeDebug: boolean) { + constructor(private file: KMXPlusFile) { this.file = file; - //this.writeDebug = _writeDebug; } public compile(): Uint8Array { @@ -57,11 +53,18 @@ export default class KMXPlusBuilder { this.emitSection(file, this.file.COMP_PLUS_SECT, this.sect.sect); // Keep the rest of these in order. this.emitSection(file, this.file.COMP_PLUS_BKSP, this.sect.bksp); - this.emitSection(file, this.file.COMP_PLUS_DISP, this.sect.disp); + this.emitSection( + file, + this.file.version == KMXPlusVersion.Version17 ? this.file.COMP_PLUS_DISP_v17 : this.file.COMP_PLUS_DISP_v19, + this.sect.disp + ); this.emitSection(file, this.file.COMP_PLUS_ELEM, this.sect.elem); this.emitElements(file); this.emitSection(file, this.file.COMP_PLUS_KEYS, this.sect.keys); - this.emitSection(file, this.file.COMP_PLUS_LAYR, this.sect.layr); + this.emitSection(file, + this.file.version == KMXPlusVersion.Version17 ? this.file.COMP_PLUS_LAYR_v17 : this.file.COMP_PLUS_LAYR_v19, + this.sect.layr + ); this.emitSection(file, this.file.COMP_PLUS_LIST, this.sect.list); this.emitSection(file, this.file.COMP_PLUS_LOCA, this.sect.loca); this.emitSection(file, this.file.COMP_PLUS_META, this.sect.meta); @@ -79,17 +82,17 @@ export default class KMXPlusBuilder { // We must prepare the strs, list, and elem sections early so that other sections can // reference them. However, they will be emitted in alpha order. - this.sect.strs = build_strs(this.file.kmxplus.strs); + this.sect.strs = build_strs(this.file.kmxplus.strs, this.file.version); this.sect.list = build_list(this.file.kmxplus.list, this.sect.strs); this.sect.uset = build_uset(this.file.kmxplus, this.sect.strs); - this.sect.elem = build_elem(this.file.kmxplus.elem, this.sect.strs, this.sect.uset); + this.sect.elem = build_elem(this.file.kmxplus.elem, this.sect.strs, this.sect.uset, this.file.version); const build_bksp = build_tran; this.sect.bksp = build_bksp(this.file.kmxplus.bksp, this.sect.strs, this.sect.elem); - this.sect.disp = build_disp(this.file.kmxplus, this.sect.strs); + this.sect.disp = build_disp(this.file.kmxplus, this.sect.strs, this.file.version); this.sect.keys = build_keys(this.file.kmxplus, this.sect.strs, this.sect.list); - this.sect.layr = build_layr(this.file.kmxplus, this.sect.strs, this.sect.list); + this.sect.layr = build_layr(this.file.kmxplus, this.sect.strs, this.file.version); this.sect.loca = build_loca(this.file.kmxplus, this.sect.strs); this.sect.meta = build_meta(this.file.kmxplus, this.sect.strs); this.sect.tran = build_tran(this.file.kmxplus.tran, this.sect.strs, this.sect.elem); @@ -98,7 +101,7 @@ export default class KMXPlusBuilder { // Finalize the sect (index) section - this.sect.sect = build_sect(); + this.sect.sect = build_sect(this.file.version); this.finalize_sect(); // must be done last return this.sect.sect.total; } @@ -115,9 +118,9 @@ export default class KMXPlusBuilder { } }); - this.sect.sect.size = constants.length_sect + constants.length_sect_item * this.sect.sect.count; + this.sect.sect.header.size = constants.length_sect + constants.length_sect_item * this.sect.sect.count + constants.headerSizeDelta(this.file.version); - let offset = this.sect.sect.size; + let offset = this.sect.sect.header.size; // Note: in order! Everyone's here except 'sect' which is at offset 0 offset = this.finalize_sect_item(this.sect.bksp, offset); offset = this.finalize_sect_item(this.sect.disp, offset); @@ -140,17 +143,24 @@ export default class KMXPlusBuilder { // Don't include null sections return offset; } + // in order to avoid changing size calculation in every section builder, + // we adjust the size here before writing. We'd need to pass version info + // into each builder, otherwise, and make this call in each header, which + // we could choose to do in the future if it makes other things easier. + sect.header.size += constants.headerSizeDelta(this.file.version); sect._offset = offset; - this.sect.sect.items.push({sect: sect.ident, offset: offset}); - return offset + sect.size; + this.sect.sect.items.push({sect: sect.header.ident, offset: offset}); + return offset + sect.header.size; } private emitSection(file: Uint8Array, comp: any, sect: BUILDER_SECTION) { if(sect) { + sect.header.version = sect.header.version ?? KMXPlusVersion.Version17; + const buf = comp.toBuffer(sect); - if (buf.length > sect.size) { + if (buf.length > sect.header.size) { // buf.length may be < sect.size if there is a variable part (i.e. elem) - throw new RangeError(`Internal Error: Section ${constants.str_section_id(sect.ident)} claimed size ${sect.size} but produced buffer of size ${buf.length}.`); + throw new RangeError(`Internal Error: Section ${constants.str_section_id(sect.header.ident)} claimed size ${sect.header.size} but produced buffer of size ${buf.length}.`); } file.set(buf, sect._offset); } diff --git a/developer/src/common/web/utils/tsconfig.json b/developer/src/common/web/utils/tsconfig.json index 362ec3a9720..8eceb5d8f7e 100644 --- a/developer/src/common/web/utils/tsconfig.json +++ b/developer/src/common/web/utils/tsconfig.json @@ -13,4 +13,8 @@ "src/**/*.ts", "src/ttfmeta/lib/*.js", ], + "references": [ + { "path": "../../../../../common/web/types/" }, + { "path": "../../../../../core/include/ldml/tsconfig.json"}, + ] } diff --git a/developer/src/kmanalyze/pch.h b/developer/src/kmanalyze/pch.h index 8a81c4235c2..a330cbeb9e9 100644 --- a/developer/src/kmanalyze/pch.h +++ b/developer/src/kmanalyze/pch.h @@ -16,7 +16,7 @@ #include #include #include "../../../common/windows/cpp/include/keymansentry.h" -#include "../../../common/windows/cpp/include/legacy_kmx_file.h" +#include "../../../common/include/kmx_file.h" #include "../../../common/windows/cpp/include/legacy_kmx_memory.h" #include "../../../common/windows/cpp/include/unicode.h" #include "../../../common/windows/cpp/include/xstring.h" diff --git a/developer/src/kmc-kmn/.gitignore b/developer/src/kmc-kmn/.gitignore index 6a608d487b8..76fb645213e 100644 --- a/developer/src/kmc-kmn/.gitignore +++ b/developer/src/kmc-kmn/.gitignore @@ -1,3 +1,6 @@ # WASM interfaces from kmcmplib are copied here so we can avoid having # to make tsc aware of debug vs release paths src/import/ + +# We copy some files from other modules for comparison purposes here +test/kmw/_imported_* \ No newline at end of file diff --git a/developer/src/kmc-kmn/build.sh b/developer/src/kmc-kmn/build.sh index 27fb9584867..cca7982abd7 100755 --- a/developer/src/kmc-kmn/build.sh +++ b/developer/src/kmc-kmn/build.sh @@ -61,6 +61,17 @@ function do_build() { function do_test() { copy_deps + + # We want to compare the key cap values from both KMW and Developer and ensure + # that all three are in sync. We'll copy the relevant source files and patch + # them in directly. The touch layout builder's constants.js is not an ES6 + # module, so we hackily patch that here. + echo 'export const builder = {specialCharacters:{}}' > ./test/kmw/_imported_layoutbuilder_constants.js + # shellcheck disable=SC2016 + echo 'function $(v) {v()}' >> ./test/kmw/_imported_layoutbuilder_constants.js + cat "${KEYMAN_ROOT}/developer/src/tike/xml/layoutbuilder/constants.js" >> ./test/kmw/_imported_layoutbuilder_constants.js + cp "${KEYMAN_ROOT}/web/src/engine/osk/src/specialCharacters.ts" ./test/kmw/_imported_web_osk_specialCharacters.ts + typescript_run_eslint_mocha_tests 80 } diff --git a/developer/src/kmc-kmn/src/compiler/compiler.ts b/developer/src/kmc-kmn/src/compiler/compiler.ts index 403f721f907..635c4470487 100644 --- a/developer/src/kmc-kmn/src/compiler/compiler.ts +++ b/developer/src/kmc-kmn/src/compiler/compiler.ts @@ -6,16 +6,18 @@ TODO: implement additional interfaces: */ // TODO: rename wasm-host? -import { VisualKeyboard, KvkFileReader, LdmlKeyboardTypes, KeymanFileTypes, KvkFileWriter, ObjectWithCompileContext } from '@keymanapp/common-types'; +import { LdmlKeyboardTypes, KeymanFileTypes, KvkFileWriter, ObjectWithCompileContext, KMX } from '@keymanapp/common-types'; import { CompilerCallbacks, CompilerEvent, CompilerOptions, KeymanCompiler, KeymanCompilerArtifacts, - KeymanCompilerArtifactOptional, KeymanCompilerResult, KeymanCompilerArtifact, KvksFileReader, + KeymanCompilerArtifactOptional, KeymanCompilerResult, KeymanCompilerArtifact, CompilerError } from '@keymanapp/developer-utils'; import * as Osk from './osk.js'; import loadWasmHost from '../import/kmcmplib/wasm-host.js'; +import { loadKvkFile } from './osk.js'; import { KmnCompilerMessages } from './kmn-compiler-messages.js'; import { WriteCompiledKeyboard } from '../kmw-compiler/kmw-compiler.js'; +import { EmbedOskInKmx } from './embed-osk/embed-osk.js'; // // Matches kmcmplibapi.h definitions @@ -56,9 +58,11 @@ export interface KmnCompilerResultExtra { */ targets: number; kvksFilename?: string; + touchLayoutFilename?: string; displayMapFilename?: string; stores: CompilerResultExtraStore[]; groups: CompilerResultExtraGroup[]; + targetVersion: KMX.KMX_Version; }; /** @internal */ @@ -224,9 +228,11 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet extra: { targets: wasm_result.extra.targets, displayMapFilename: wasm_result.extra.displayMapFilename, + touchLayoutFilename: wasm_result.extra.touchLayoutFilename, kvksFilename: wasm_result.extra.kvksFilename, stores: [], groups: [], + targetVersion: wasm_result.extra.targetVersion, }, displayMap: null }; @@ -322,6 +328,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet wasm_options.compilerWarningsAsErrors = options.compilerWarningsAsErrors; wasm_options.warnDeprecatedCode = options.warnDeprecatedCode; wasm_options.shouldAddCompilerVersion = options.shouldAddCompilerVersion; + wasm_options.targetVersion = options.targetVersion ?? 0; wasm_options.target = 0; // CKF_KEYMAN; TODO use COMPILETARGETS_KMX wasm_result = Module.kmcmp_compile(infile, wasm_options, wasm_callbacks); @@ -350,7 +357,14 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet } } + if(result.extra.targetVersion >= KMX.KMXFile.VERSION_190) { + if(!this.embedOskInKmx(infile, result)) { + return null; + } + } + if(result.extra.kvksFilename) { + // TODO-EMBED-OSK-IN-KMX: skip this once we support embedded OSK in all desktop targets result.artifacts.kvk = this.runKvkCompiler(result.extra.kvksFilename, infile, outfile, result.displayMap); if(!result.artifacts.kvk) { return null; @@ -361,7 +375,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet // KeymanWeb compiler // - if(wasm_result.extra.targets & COMPILETARGETS_JS) { + if(result.extra.targets & COMPILETARGETS_JS) { wasm_options.target = 1; // CKF_KEYMANWEB TODO use COMPILETARGETS_JS // We always want debug data in the intermediate .kmx, so that error @@ -421,47 +435,30 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet return this.callbacks.path.basename(kmnFilename, KeymanFileTypes.Source.KeymanKeyboard); } + private embedOskInKmx(kmnFilename: string, result: KmnCompilerResult): boolean { + const embedder = new EmbedOskInKmx(this.callbacks, this.options); + const newData = embedder.embed( + result.artifacts.kmx.data, + result.extra.kvksFilename ? this.callbacks.resolveFilename(kmnFilename, result.extra.kvksFilename) : null, + result.extra.touchLayoutFilename ? this.callbacks.resolveFilename(kmnFilename, result.extra.touchLayoutFilename) : null, + result.displayMap + ); + if(!newData) { + // messages will have been raised in .embed + return false; + } + + result.artifacts.kmx.data = newData; + return true; + } + private runKvkCompiler(kvksFilename: string, kmnFilename: string, kmxFilename: string, displayMap?: Osk.PuaMap) { // The compiler detected a .kvks file, which needs to be captured - kvksFilename = this.callbacks.resolveFilename(kmnFilename, kvksFilename); - const data = this.callbacks.loadFile(kvksFilename); - if(!data) { - this.callbacks.reportMessage(KmnCompilerMessages.Error_FileNotFound({filename: kvksFilename})); + const vk = loadKvkFile(this.callbacks.resolveFilename(kmnFilename, kvksFilename), this.callbacks); + if(!vk) { return null; } - const filename = this.callbacks.path.basename(kvksFilename); - let basename = null; - let vk: VisualKeyboard.VisualKeyboard = null; - if(filename.endsWith('.kvk')) { - /* Legacy keyboards may reference a binary .kvk. That's not an error */ - // TODO: (lowpri) add hint to convert to .kvks? - basename = this.callbacks.path.basename(kvksFilename, KeymanFileTypes.Binary.VisualKeyboard); - const reader = new KvkFileReader(); - try { - vk = reader.read(data); - } catch(e) { - this.callbacks.reportMessage(KmnCompilerMessages.Error_InvalidKvkFile({filename, e})); - return null; - } - } else { - basename = this.callbacks.path.basename(kvksFilename, KeymanFileTypes.Source.VisualKeyboard); - const reader = new KvksFileReader(); - let kvks = null; - try { - kvks = reader.read(data); - reader.validate(kvks); - } catch(e) { - this.callbacks.reportMessage(KmnCompilerMessages.Error_InvalidKvksFile({filename, e})); - return null; - } - const invalidVkeys: string[] = []; - vk = reader.transform(kvks, invalidVkeys); - for(const invalidVkey of invalidVkeys) { - this.callbacks.reportMessage(KmnCompilerMessages.Warn_InvalidVkeyInKvksFile({filename, invalidVkey})); - } - } - // Make sure that we maintain the correspondence between source keyboard and // .kvk. Appears to be used currently only by Windows package installer. vk.header.associatedKeyboard = this.keyboardIdFromKmnFilename(kmnFilename); @@ -471,6 +468,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet Osk.remapVisualKeyboard(vk, displayMap); } + const basename = this.callbacks.path.basename(kvksFilename, this.callbacks.path.extname(kvksFilename)); const writer = new KvkFileWriter(); return { filename: this.callbacks.path.join(this.callbacks.path.dirname(kmxFilename), 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 00000000000..434dbdca8e7 --- /dev/null +++ b/developer/src/kmc-kmn/src/compiler/embed-osk/embed-osk-kvk.ts @@ -0,0 +1,161 @@ +/* + * 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 visual keyboard from a .kvk file to the KMX+ format by setting the appropriate fields in the KMX+ data structure + * + * @param vk content of a .kvk file + * @param kmx + */ + 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): KMXPlus.LayrForm { + 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 new file mode 100644 index 00000000000..1d00e8b1fdb --- /dev/null +++ b/developer/src/kmc-kmn/src/compiler/embed-osk/embed-osk.ts @@ -0,0 +1,144 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by mcdurdin on 2025-11-24 + * + * Convert Keyman .kvks and .keyman-touch-layout files to KMX+ format and embed + * in .kmx. + */ +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 { EmbedOskKvkInKmx } from "./embed-osk-kvk.js"; + +// import { EmbedOskTouchLayoutInKmx } from "./embed-osk-touch-layout.js"; + +export class EmbedOskInKmx { + constructor( + public callbacks: CompilerCallbacks, //TODO-EMBED-OSK-IN-KMX: private + public options: KmnCompilerOptions //TODO-EMBED-OSK-IN-KMX: private + ) { + } + + 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 + * @param kmx .kmx file, v19 or later + * @param kmnFilename source filename for .kmn, only used for path resolution + * @param kvksFilename + * @param touchLayoutFilename + * @param displayMap + * @returns + */ + public embed(kmx: Uint8Array, kvksFilename: string, touchLayoutFilename: string, displayMap: PuaMap): Uint8Array { + const kmxPlus = this.createEmptyKmxPlusFile(); + + 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 + + const builder = new KMXPlusBuilder(kmxPlus); + const data = builder.compile(); + + return this.injectKmxPlusIntoKmxFile(kmx, data); + } + + /** + * 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+ + * kmxPlusBinary data blob, and updates the header flag to indicate that the + * file now includes OSK data. + * @param inputFile existing KMX file + * @param kmxPlusBinary KMX+ data blob + * @returns new KMX file with embedded KMX+ data + */ + private injectKmxPlusIntoKmxFile(inputFile: Uint8Array, kmxPlusBinary: Uint8Array): Uint8Array { + const kmx = new KMX.KMXFile(); + + const HEADER_SIZE = KMX.KMXFile.COMP_KEYBOARD_SIZE + KMX.KMXFile.COMP_KEYBOARD_KMXPLUSINFO_SIZE; + if(inputFile.byteLength < HEADER_SIZE) { + throw new Error(`Expected inputFile to have space for COMP_KEYBOARD_KMXPLUSINFO, but was only ${inputFile.byteLength} bytes`); + } + + const keyboardHeader = kmx.COMP_KEYBOARD.fromBuffer(inputFile); + if(keyboardHeader.dwFileVersion < KMX.KMX_Version.VERSION_190) { + throw new Error(`Expected inputFile to be at least VERSION_190, but it was ${keyboardHeader.dwFileVersion}`); + } + + // Verify that the input file has space reserved by looking at the two + // fields with offsets in the header. kmcmplib should have reserved space + // for the header for v19.0+ keyboards + + if(keyboardHeader.dpStoreArray < HEADER_SIZE || keyboardHeader.dpGroupArray < HEADER_SIZE) { + throw new Error( + `Expected reservation for COMP_KEYBOARD_KMXPLUSINFO, but store (${keyboardHeader.dpStoreArray})`+ + ` or group (${keyboardHeader.dpGroupArray}) offsets are too low` + ); + } + + const comp_kmxplus: KMX.BUILDER_COMP_KEYBOARD_KMXPLUSINFO = { + dpKMXPlus: inputFile.byteLength, + dwKMXPlusSize: kmxPlusBinary.byteLength + }; + + const outputFile = new Uint8Array(comp_kmxplus.dpKMXPlus + comp_kmxplus.dwKMXPlusSize); + + // Copy input KMX data + outputFile.set(inputFile, 0); + + // KMX+ header COMP_KEYBOARD_KMXPLUSINFO is written immediately after COMP_KEYBOARD + const kmxPlusHeaderBinary: Uint8Array = kmx.COMP_KEYBOARD_KMXPLUSINFO.toBuffer(comp_kmxplus); + outputFile.set(kmxPlusHeaderBinary, KMX.KMXFile.COMP_KEYBOARD_SIZE); + + // Poke the KF_KMXPLUSOSK flag into COMP_KEYBOARD.dwFlags + keyboardHeader.dwFlags = keyboardHeader.dwFlags | KMX.KMXFile.KF_KMXPLUSOSK; + const keyboardHeaderBinary = kmx.COMP_KEYBOARD.toBuffer(keyboardHeader); + outputFile.set(keyboardHeaderBinary, 0); + + // Append KMX+ data at the end of the existing file + outputFile.set(kmxPlusBinary, inputFile.byteLength); + + return outputFile; + } + + public readonly unitTestEndpoints = { + injectKmxPlusIntoKmxFile: this.injectKmxPlusIntoKmxFile.bind(this), + createEmptyKmxPlusFile: this.createEmptyKmxPlusFile.bind(this), + }; + +}; diff --git a/developer/src/kmc-kmn/src/compiler/embed-osk/osk-layout.ts b/developer/src/kmc-kmn/src/compiler/embed-osk/osk-layout.ts new file mode 100644 index 00000000000..d2e31318b5f --- /dev/null +++ b/developer/src/kmc-kmn/src/compiler/embed-osk/osk-layout.ts @@ -0,0 +1,32 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by mcdurdin on 2025-11-24 + */ +import { USVirtualKeyCodes as V } from "@keymanapp/common-types"; + +/** + * On-Screen Keyboard virtual key codes and positions for the Windows "kbdus" + * hardware layout which Keyman for Windows uses as its basic positional layout, + * mapped against the LDML supported base layouts. Only the character-producing + * keys are included; frame keys are excluded. + */ +export const oskLayouts = { + us: [ + [V.K_BKQUOTE, V.K_1, V.K_2, V.K_3, V.K_4, V.K_5, V.K_6, V.K_7, V.K_8, V.K_9, V.K_0, V.K_HYPHEN, V.K_EQUAL], + [V.K_Q, V.K_W, V.K_E, V.K_R, V.K_T, V.K_Y, V.K_U, V.K_I, V.K_O, V.K_P, V.K_LBRKT, V.K_RBRKT, V.K_BKSLASH], + [V.K_A, V.K_S, V.K_D, V.K_F, V.K_G, V.K_H, V.K_J, V.K_K, V.K_L, V.K_COLON, V.K_QUOTE], + [V.K_Z, V.K_X, V.K_C, V.K_V, V.K_B, V.K_N, V.K_M, V.K_COMMA, V.K_PERIOD, V.K_SLASH], + [V.K_SPACE], + ], + + iso: [ + [V.K_BKQUOTE, V.K_1, V.K_2, V.K_3, V.K_4, V.K_5, V.K_6, V.K_7, V.K_8, V.K_9, V.K_0, V.K_HYPHEN, V.K_EQUAL], + [V.K_Q, V.K_W, V.K_E, V.K_R, V.K_T, V.K_Y, V.K_U, V.K_I, V.K_O, V.K_P, V.K_LBRKT, V.K_RBRKT], // differences from us: K_BKSLASH removed here + [V.K_A, V.K_S, V.K_D, V.K_F, V.K_G, V.K_H, V.K_J, V.K_K, V.K_L, V.K_COLON, V.K_QUOTE, V.K_BKSLASH], // differences from us: K_BKSLASH add here + [V.K_OE2, V.K_Z, V.K_X, V.K_C, V.K_V, V.K_B, V.K_N, V.K_M, V.K_COMMA, V.K_PERIOD, V.K_SLASH], // differences from us: adds K_OE2, "102nd key" + [V.K_SPACE], + ], + + // abnt2, jis, ks may be needed in the future +}; \ No newline at end of file diff --git a/developer/src/kmc-kmn/src/compiler/kmn-compiler-messages.ts b/developer/src/kmc-kmn/src/compiler/kmn-compiler-messages.ts index b85a7086e6f..b7bfd87a637 100644 --- a/developer/src/kmc-kmn/src/compiler/kmn-compiler-messages.ts +++ b/developer/src/kmc-kmn/src/compiler/kmn-compiler-messages.ts @@ -68,9 +68,10 @@ type KmcmpLibMessageParameters = {p:string[]}; * ``` */ export class KmnCompilerMessages { - // TODO: v18.0 we should consider moving error message generation in kmcmplib to - // kmc-kmn, which would avoid a number of legacy issues. Questions about - // parameterisation. + + //------------------------------------------------------------------------------| + // max length of detail message lines (checked by verifyCompilerMessagesObject) | + //------------------------------------------------------------------------------| static FATAL_UnexpectedException = SevFatal | 0x900; static Fatal_UnexpectedException = (o:{e: any}) => CompilerMessageSpecWithException( @@ -197,10 +198,32 @@ export class KmnCompilerMessages { static ERROR_FileNotFound = SevError | 0x90C; static Error_FileNotFound = (o:{filename: string}) => m( this.ERROR_FileNotFound, - `File ${def(o.filename)} was not found`, - `The file was not found on the disk. Verify that you have the correct path - to the file.` - ); + `File ${def(o.filename)} was not found`, ` + The file was not found on the disk. Verify that you have the correct path + to the file. + `); + + static WARN_EmbeddedOskDoesNotSupportBitmaps = SevWarn | 0x90D; + static Warn_EmbeddedOskDoesNotSupportBitmaps = (o:{keyId: string}) => m( + this.WARN_EmbeddedOskDoesNotSupportBitmaps, + `The On-Screen Keyboard key '${def(o.keyId)}' uses a bitmap, which is not supported in v19+ embedded On-Screen Keyboards`, ` + The v19+ On-Screen Keyboard is embedded into the .kmx file. However, bitmap + images are not supported for key caps. To support arbitrary images, use a + custom font for the On-Screen Keyboard. + `); + + static HINT_EmbeddedOskDoesNotSupportNonUnicode = SevHint | 0x90E; + static Hint_EmbeddedOskDoesNotSupportNonUnicode = (o:{keyId: string}) => m( + this.HINT_EmbeddedOskDoesNotSupportNonUnicode, + `The On-Screen Keyboard key '${def(o.keyId)}' is not Unicode, and will be ignored`, ` + The v19+ On-Screen Keyboard is embedded into the .kmx file. However, + non-Unicode key caps are not supported in the .kmx embedded On-Screen + Keyboard. Only the first non-Unicode key cap found will be reported. + `); + + //------------------------------------------------------------------------------| + // Messages below this point come from kmcmplib | + //------------------------------------------------------------------------------| // static STATUS_None = 0x000; // This is not a real error // static STATUS_EndOfFile = 0x001; // This is not a real error diff --git a/developer/src/kmc-kmn/src/compiler/osk.ts b/developer/src/kmc-kmn/src/compiler/osk.ts index e26c124b15b..81f97280859 100644 --- a/developer/src/kmc-kmn/src/compiler/osk.ts +++ b/developer/src/kmc-kmn/src/compiler/osk.ts @@ -1,6 +1,8 @@ -import { TouchLayout } from "@keymanapp/common-types"; +import { KvkFileReader, TouchLayout } from "@keymanapp/common-types"; import { VisualKeyboard } from "@keymanapp/common-types"; import { SchemaValidators } from "@keymanapp/common-types"; +import { KmnCompilerMessages } from "./kmn-compiler-messages.js"; +import { CompilerCallbacks, KvksFileReader } from "@keymanapp/developer-utils"; export interface StringRefUsage { filename: string; @@ -123,3 +125,41 @@ export function remapTouchLayout(source: TouchLayout.TouchLayoutFile, map: PuaMa return dirty; } + +export function loadKvkFile(kvksFilename: string, callbacks: CompilerCallbacks): VisualKeyboard.VisualKeyboard { + const data = callbacks.loadFile(kvksFilename); + if(!data) { + callbacks.reportMessage(KmnCompilerMessages.Error_FileNotFound({filename: kvksFilename})); + return null; + } + + const filename = callbacks.path.basename(kvksFilename); + let vk: VisualKeyboard.VisualKeyboard = null; + if(filename.endsWith('.kvk')) { + /* Legacy keyboards may reference a binary .kvk. That's not an error */ + // TODO: (lowpri) add hint to convert to .kvks? + const reader = new KvkFileReader(); + try { + vk = reader.read(data); + } catch(e) { + callbacks.reportMessage(KmnCompilerMessages.Error_InvalidKvkFile({filename, e})); + return null; + } + } else { + const reader = new KvksFileReader(); + let kvks = null; + try { + kvks = reader.read(data); + reader.validate(kvks); + } catch(e) { + callbacks.reportMessage(KmnCompilerMessages.Error_InvalidKvksFile({filename, e})); + return null; + } + const invalidVkeys: string[] = []; + vk = reader.transform(kvks, invalidVkeys); + for(const invalidVkey of invalidVkeys) { + callbacks.reportMessage(KmnCompilerMessages.Warn_InvalidVkeyInKvksFile({filename, invalidVkey})); + } + } + return vk; +} \ No newline at end of file diff --git a/developer/src/kmc-kmn/src/kmw-compiler/constants.ts b/developer/src/kmc-kmn/src/kmw-compiler/constants.ts index 1c41df145b8..a678dc87269 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/constants.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/constants.ts @@ -7,7 +7,16 @@ export enum TRequiredKey { export const CRequiredKeys: TRequiredKey[] = [TRequiredKey.K_LOPT, TRequiredKey.K_BKSP, TRequiredKey.K_ENTER]; // I4447 -// See also builder.js: specialCharacters; web/source/osk/oskKey.ts: specialCharacters +// Defines the various 'special' modifier/control/non-printing keys on keyboards. +// +// `CSpecialText*` must be kept in sync with: +// * /web/src/engine/osk/src/specialCharacters.ts +// * /developer/src/tike/xml/layoutBuilder/constants.js +// * /core/include/ldml/keyman_core_ldml.ts +// +// More information, and unit test, in /developer/src/kmc-kmn/tests/kmw/constants.tests.ts +// +// Note that this mapping here is version-mapped for verification purposes. export const CSpecialText10: string = '*Shift*\0*Enter*\0*Tab*\0*BkSp*\0*Menu*\0*Hide*\0*Alt*\0*Ctrl*\0*Caps*\0' + '*ABC*\0*abc*\0*123*\0*Symbol*\0*Currency*\0*Shifted*\0*AltGr*\0*TabLeft*\0', diff --git a/developer/src/kmc-kmn/src/kmw-compiler/visual-keyboard-compiler.ts b/developer/src/kmc-kmn/src/kmw-compiler/visual-keyboard-compiler.ts index 117d0f2affe..d5895e4d126 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/visual-keyboard-compiler.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/visual-keyboard-compiler.ts @@ -1,4 +1,4 @@ -import { KMX, KvkFile, VisualKeyboard } from "@keymanapp/common-types"; +import { KvkFile, VisualKeyboard, visualKeyboardShiftToLayerName } from "@keymanapp/common-types"; import { FTabStop, nl } from "./compiler-globals.js"; import { CKeymanWebKeyCodes } from "./keymanweb-key-codes.js"; import { RequotedString } from "./kmw-compiler.js"; @@ -22,60 +22,6 @@ function WideQuote(s: string): string { return result; } -function VkShiftStateToKmxShiftState(ShiftState: number): number { - - interface TVKToKMX { - VK: number; KMX: number; - } - - const Map: TVKToKMX[] = [ - {VK: KvkFile.BUILDER_KVK_SHIFT_STATE.KVKS_SHIFT, KMX: KMX.KMXFile.K_SHIFTFLAG}, - {VK: KvkFile.BUILDER_KVK_SHIFT_STATE.KVKS_CTRL, KMX: KMX.KMXFile.K_CTRLFLAG}, - {VK: KvkFile.BUILDER_KVK_SHIFT_STATE.KVKS_ALT, KMX: KMX.KMXFile.K_ALTFLAG}, - {VK: KvkFile.BUILDER_KVK_SHIFT_STATE.KVKS_LCTRL, KMX: KMX.KMXFile.LCTRLFLAG}, - {VK: KvkFile.BUILDER_KVK_SHIFT_STATE.KVKS_RCTRL, KMX: KMX.KMXFile.RCTRLFLAG}, - {VK: KvkFile.BUILDER_KVK_SHIFT_STATE.KVKS_LALT, KMX: KMX.KMXFile.LALTFLAG}, - {VK: KvkFile.BUILDER_KVK_SHIFT_STATE.KVKS_RALT, KMX: KMX.KMXFile.RALTFLAG} - ]; - - let result = 0; - for(let i = 0; i < Map.length; i++) { - if (ShiftState & Map[i].VK) { - result |= Map[i].KMX; - } - } - - return result; -} - - -function VKShiftToLayerName(shift: number): string { - - const masks: string[] = [ - 'leftctrl', - 'rightctrl', - 'leftalt', - 'rightalt', - 'shift', - 'ctrl', - 'alt' - ]; - - shift = VkShiftStateToKmxShiftState(shift); - if(shift == 0) { - return 'default'; - } - - let result = ''; - for(let i = 0; i < masks.length; i++) { - if(shift & (1 << i)) { - result += masks[i] + '-'; - } - } - return result.substring(0, result.length - 1); -} - - function VisualKeyboardToKLS(FVK: VisualKeyboard.VisualKeyboard): string { interface TLayer { @@ -111,7 +57,7 @@ function VisualKeyboardToKLS(FVK: VisualKeyboard.VisualKeyboard): string { for(let i = 0; i < layers.length; i++) { const layer = layers[i]; - result += `${FTabStop}${FTabStop}"${VKShiftToLayerName(layer.shift)}": [`; + result += `${FTabStop}${FTabStop}"${visualKeyboardShiftToLayerName(layer.shift)}": [`; for(let j = 0; j < layer.keys.length - 1; j++) { result += '"'+WideQuote(layer.keys[j] ?? '')+'",'; } 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 00000000000..0b88e2de868 --- /dev/null +++ b/developer/src/kmc-kmn/test/embed-osk-kvk.tests.ts @@ -0,0 +1,316 @@ +/* + * 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 transform 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 kmxPlus data 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 new file mode 100644 index 00000000000..c9383f5d4d5 --- /dev/null +++ b/developer/src/kmc-kmn/test/embed-osk.tests.ts @@ -0,0 +1,263 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import 'mocha'; +import { assert } from 'chai'; +import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; +import { KMX } from '@keymanapp/common-types'; +import { KmnCompiler } from '../src/main.js'; +import { EmbedOskInKmx } from '../src/compiler/embed-osk/embed-osk.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)).replace(/\\/g, '/'); +const keyboardsDir = __dirname + '/../../../../../common/test/keyboards/'; + +describe('Compiler OSK Embedding', function() { + + const callbacks = new TestCompilerCallbacks(); + + this.beforeEach(function() { + callbacks.clear(); + }); + + this.afterEach(function() { + if(this.currentTest.isFailed()) { + callbacks.printMessages(); + } + }); + + describe('kmcmplib infrastructure', function() { + + it('should compile a keyboard and reserve space for the KMX+ data for v19+', async function() { + // This tests kmcmplib, but is implemented in kmc-kmn for simplicity + + const compiler = new KmnCompiler(); + assert.isTrue(await compiler.init(callbacks, { + saveDebug: false, + shouldAddCompilerVersion: false, + // TODO-EMBED-OSK-IN-KMX: test_skip_osk_embed: true --> or mock stub for EmbedOskInKmx function + })); + assert.isTrue(compiler.verifyInitialized()); + + const fixtureDir = keyboardsDir + 'embed_osk/source/'; + const infile = fixtureDir + 'test_v19_kmxplus.kmn'; + const resultingKmxfile = __dirname + '/test_v19_kmxplus.kmx'; + const result = await compiler.run(infile, resultingKmxfile); + assert.isNotNull(result); + assert.isNotNull(result.artifacts.kmx); + + const expectedHeaderSize = KMX.KMXFile.COMP_KEYBOARD_SIZE + KMX.KMXFile.COMP_KEYBOARD_KMXPLUSINFO_SIZE; + + // We'll check that the stores and groups start after the KMX+ struct + + assert.isAtLeast(result.artifacts.kmx.data.byteLength, expectedHeaderSize); + + const kmx = new KMX.KMXFile(); + const binaryKeyboard = kmx.COMP_KEYBOARD.fromBuffer(result.artifacts.kmx.data); + assert.isAtLeast(binaryKeyboard.dpStoreArray, expectedHeaderSize); + assert.isAtLeast(binaryKeyboard.dpGroupArray, expectedHeaderSize); + + // Verify that the KMX+ flags are not set + + assert.equal(binaryKeyboard.dwFlags & KMX.KMXFile.KF_KMXPLUS, 0); + assert.equal(binaryKeyboard.dwFlags & KMX.KMXFile.KF_KMXPLUSOSK, 0); + + // the KMX+ struct itself should be empty at this point + + const bufferKmxPlus = new Uint8Array(result.artifacts.kmx.data.buffer, KMX.KMXFile.COMP_KEYBOARD_SIZE); + const binaryKmxPlus = kmx.COMP_KEYBOARD_KMXPLUSINFO.fromBuffer(bufferKmxPlus); + assert.equal(binaryKmxPlus.dpKMXPlus, 0); + assert.equal(binaryKmxPlus.dwKMXPlusSize, 0); + }); + }); + + describe('EmbedOskInKmx', function() { + + describe('EmbedOskInKmx.injectKmxPlusIntoKmxFile', function() { + const embedder = new EmbedOskInKmx(callbacks, {}); + + // This is not a valid KMX+ blob, but the function does not do any + // validation of the contents of the KMX+ blob, but just injects what it + // is given, so we can inject this and look for it at EOF + const sentinelKmxPlusBlob = new TextEncoder().encode('SENTINEL'); + + // None of these should ever be possible on outputs from kmcmplib in our + // code paths, so we'll treat these as internal errors rather than a + // compiler error + + it('should throw if passed a zero-byte KMX file', function() { + const zeroByteFile = new Uint8Array(0); + assert.throws(function() { embedder.unitTestEndpoints.injectKmxPlusIntoKmxFile(zeroByteFile, sentinelKmxPlusBlob) }); + }); + + it('should throw if passed a version 17 KMX file', function() { + // This file has a v17 header but is not a full file, just for test purposes + // taken from k_000__null_keyboard.kmx and tweaked to v.17, made it long + // enough for the KMX+ header but did not adjust offsets + const invalid17KmxFile = new Uint8Array([ + // COMP_KEYBOARD + 0x4B, 0x58, 0x54, 0x53, // dwIdentifier = 'KXTS' + 0x00, 0x11, 0x00, 0x00, // dwFileVersion = 17.0 + 0x00, 0x00, 0x00, 0x00, // dwCheckSum + 0x00, 0x00, 0x00, 0x00, // KeyboardID + 0x01, 0x00, 0x00, 0x00, // IsRegistered + 0x00, 0x00, 0x00, 0x00, // version (unused) + 0x06, 0x00, 0x00, 0x00, // cxStoreArray + 0x01, 0x00, 0x00, 0x00, // cxGroupArray + 0x40, 0x00, 0x00, 0x00, // dpStoreArray + 0x1A, 0x01, 0x00, 0x00, // dpGroupArray + 0xFF, 0xFF, 0xFF, 0xFF, // StartGroup[ansi] + 0x00, 0x00, 0x00, 0x00, // StartGroup[unicode] + 0x00, 0x00, 0x00, 0x00, // dwFlags + 0x00, 0x00, 0x00, 0x00, // dwHotKey + 0x3C, 0x01, 0x00, 0x00, // dpBitmapOffset + 0x00, 0x00, 0x00, 0x00, // dwBitmapSize + // COMP_KEYBOARD_KMXPLUSINFO + 0x00, 0x00, 0x00, 0x00, // dpKMXPlus + 0x00, 0x00, 0x00, 0x00, // dwKMXPlusSize + ]); + + assert.throws(function() { embedder.unitTestEndpoints.injectKmxPlusIntoKmxFile(invalid17KmxFile, sentinelKmxPlusBlob) }, 'Expected inputFile to be at least VERSION_190'); + }); + + it('should throw if passed a version 19 KMX file that is too short', function() { + // This file has a v19 header but is not a full file, just for test purposes + // taken from k_000__null_keyboard.kmx and tweaked to v.19. Missing space + // for the KMX+ header + const invalid19KmxFileShort = new Uint8Array([ + // COMP_KEYBOARD + 0x4B, 0x58, 0x54, 0x53, // dwIdentifier = 'KXTS' + 0x00, 0x13, 0x00, 0x00, // dwFileVersion = 19.0 + 0x00, 0x00, 0x00, 0x00, // dwCheckSum + 0x00, 0x00, 0x00, 0x00, // KeyboardID + 0x01, 0x00, 0x00, 0x00, // IsRegistered + 0x00, 0x00, 0x00, 0x00, // version (unused) + 0x06, 0x00, 0x00, 0x00, // cxStoreArray + 0x01, 0x00, 0x00, 0x00, // cxGroupArray + 0x40, 0x00, 0x00, 0x00, // dpStoreArray + 0x1A, 0x01, 0x00, 0x00, // dpGroupArray + 0xFF, 0xFF, 0xFF, 0xFF, // StartGroup[ansi] + 0x00, 0x00, 0x00, 0x00, // StartGroup[unicode] + 0x00, 0x00, 0x00, 0x00, // dwFlags + 0x00, 0x00, 0x00, 0x00, // dwHotKey + 0x3C, 0x01, 0x00, 0x00, // dpBitmapOffset + 0x00, 0x00, 0x00, 0x00, // dwBitmapSize + ]); + assert.throws(function() { embedder.unitTestEndpoints.injectKmxPlusIntoKmxFile(invalid19KmxFileShort, sentinelKmxPlusBlob) }, 'Expected inputFile to have space'); + }); + + it('should throw if passed a version 19 KMX file that has no reservation for KMX+ header', function() { + // This file has a v19 header but is not a full file, just for test purposes + // taken from k_000__null_keyboard.kmx and tweaked to v.19. Has space for + // KMX+ header but store and group offsets are not accounting for the space + const invalid19KmxFileNoReservation = new Uint8Array([ + // COMP_KEYBOARD + 0x4B, 0x58, 0x54, 0x53, // dwIdentifier = 'KXTS' + 0x00, 0x13, 0x00, 0x00, // dwFileVersion = 19.0 + 0x00, 0x00, 0x00, 0x00, // dwCheckSum + 0x00, 0x00, 0x00, 0x00, // KeyboardID + 0x01, 0x00, 0x00, 0x00, // IsRegistered + 0x00, 0x00, 0x00, 0x00, // version (unused) + 0x06, 0x00, 0x00, 0x00, // cxStoreArray + 0x01, 0x00, 0x00, 0x00, // cxGroupArray + 0x40, 0x00, 0x00, 0x00, // dpStoreArray -- note 0x40 offset + 0x1A, 0x01, 0x00, 0x00, // dpGroupArray + 0xFF, 0xFF, 0xFF, 0xFF, // StartGroup[ansi] + 0x00, 0x00, 0x00, 0x00, // StartGroup[unicode] + 0x00, 0x00, 0x00, 0x00, // dwFlags + 0x00, 0x00, 0x00, 0x00, // dwHotKey + 0x3C, 0x01, 0x00, 0x00, // dpBitmapOffset + 0x00, 0x00, 0x00, 0x00, // dwBitmapSize + // COMP_KEYBOARD_KMXPLUSINFO + 0x00, 0x00, 0x00, 0x00, // dpKMXPlus + 0x00, 0x00, 0x00, 0x00, // dwKMXPlusSize + ]); + assert.throws(function() { embedder.unitTestEndpoints.injectKmxPlusIntoKmxFile(invalid19KmxFileNoReservation, sentinelKmxPlusBlob) }, 'Expected reservation for COMP_KEYBOARD_KMXPLUSINFO'); + }); + + it('should successfully inject a KMX+ blob into a v19 KMX file', function() { + // This file has a v19 header but is not a full file, just for test purposes + // taken from k_000__null_keyboard.kmx and tweaked to v.19. Has space for + // KMX+ header but store and group offsets are not accounting for the space + const semiValid19KmxFile = new Uint8Array([ + // COMP_KEYBOARD + 0x4B, 0x58, 0x54, 0x53, // dwIdentifier = 'KXTS' + 0x00, 0x13, 0x00, 0x00, // dwFileVersion = 19.0 + 0x00, 0x00, 0x00, 0x00, // dwCheckSum + 0x00, 0x00, 0x00, 0x00, // KeyboardID + 0x01, 0x00, 0x00, 0x00, // IsRegistered + 0x00, 0x00, 0x00, 0x00, // version (unused) + 0x01, 0x00, 0x00, 0x00, // cxStoreArray + 0x01, 0x00, 0x00, 0x00, // cxGroupArray + 0x48, 0x00, 0x00, 0x00, // dpStoreArray -- note 0x48 offset + 0x4C, 0x00, 0x00, 0x00, // dpGroupArray -- note 0x4C offset + 0xFF, 0xFF, 0xFF, 0xFF, // StartGroup[ansi] + 0x00, 0x00, 0x00, 0x00, // StartGroup[unicode] + 0x00, 0x00, 0x00, 0x00, // dwFlags + 0x00, 0x00, 0x00, 0x00, // dwHotKey + 0x3C, 0x01, 0x00, 0x00, // dpBitmapOffset + 0x00, 0x00, 0x00, 0x00, // dwBitmapSize + // COMP_KEYBOARD_KMXPLUSINFO + 0x00, 0x00, 0x00, 0x00, // dpKMXPlus + 0x00, 0x00, 0x00, 0x00, // dwKMXPlusSize + // Fake additional data - just to verify that we don't clobber it + 0x01, 0x02, 0x03, 0x04, // "dpStoreArray" + 0x05, 0x06, 0x07, 0x08, // "dpGroupArray" + ]); + + // good path + + const data = embedder.unitTestEndpoints.injectKmxPlusIntoKmxFile(semiValid19KmxFile, sentinelKmxPlusBlob); + assert.isNotNull(data); + assert.equal(data.byteLength, semiValid19KmxFile.byteLength + sentinelKmxPlusBlob.byteLength); + + assert.deepEqual( + data.slice(0x00, 0x30), semiValid19KmxFile.slice(0x00, 0x30), + 'Expected start of header to be unmodified' + ); + + assert.deepEqual( + data.slice(0x34, 0x40), semiValid19KmxFile.slice(0x34, 0x40), + 'Expected end of header to be unmodified' + ); + + assert.deepEqual( + data.slice(0x30, 0x34), new Uint8Array([0x40, 0, 0, 0]), + 'Expected KF_KMXPLUSOSK flag to have been set in COMP_KEYBOARD.dwFlags' + ); + + assert.deepEqual( + data.slice(0x40, 0x44), new Uint8Array([semiValid19KmxFile.byteLength, 0, 0, 0]), + 'Expected COMP_KEYBOARD_KMXPLUSINFO.dpKMXPlus to have been updated to length of original file' + ); + + assert.deepEqual( + data.slice(0x44, 0x48), new Uint8Array([sentinelKmxPlusBlob.byteLength, 0, 0, 0]), + 'Expected COMP_KEYBOARD_KMXPLUSINFO.dwKMXPlusSize to have been updated' + ); + + assert.deepEqual( + data.slice(0x48, 0x50), new Uint8Array([1,2,3,4,5,6,7,8]), + 'Expected KMX data to have not been modified' + ); + + assert.deepEqual( + data.slice(0x50, 0x58), sentinelKmxPlusBlob, + 'Expected KMX+ data to have been appended' + ); + }); + }); + + describe('EmbedOskInKmx.embed', function() { + // const embedder = new EmbedOskInKmx(callbacks, {}); + + it('should embed a .kvks file correctly into a .kmx file', function() { + // + // TODO-EMBED-OSK-IN-KMX: implement + // const result = embedder.embed(kmx, kvksFilename, '', null); + this.skip(); + }); + }); + }); +}); diff --git a/developer/src/kmc-kmn/test/features.tests.ts b/developer/src/kmc-kmn/test/features.tests.ts index 1f82a03ed52..7fa2f060fb4 100644 --- a/developer/src/kmc-kmn/test/features.tests.ts +++ b/developer/src/kmc-kmn/test/features.tests.ts @@ -1,39 +1,50 @@ import 'mocha'; import { assert } from 'chai'; -import { KmnCompiler } from '../src/main.js'; +import { KMX, KmxFileReader } from '@keymanapp/common-types'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { makePathToFixture } from './helpers/index.js'; -import { KMX, KmxFileReader } from '@keymanapp/common-types'; + +import { KmnCompiler } from '../src/main.js'; describe('Keyboard compiler features', function() { - let compiler: KmnCompiler = null; let callbacks: TestCompilerCallbacks = null; this.beforeAll(async function() { - compiler = new KmnCompiler(); callbacks = new TestCompilerCallbacks(); - assert(await compiler.init(callbacks, {saveDebug: true})); - assert(compiler.verifyInitialized()); }); - beforeEach(function() { + this.beforeEach(function() { callbacks.clear(); + + }); + + this.afterEach(function() { + if(this.currentTest.isFailed()) { + callbacks.printMessages(); + } }); + + // Test each Keyman file version target const versions = [ - // TODO(lowpri): we should add a test for each version + also test automatic feature detection + // TODO-EMBED-OSK-IN-KMX: we should add a test for each version + also test automatic feature detection + // and also verify that targetVersion works as expected with that {major: '16.0', vstr: '160', vernum: KMX.KMXFile.VERSION_160}, {major: '17.0', vstr: '170', vernum: KMX.KMXFile.VERSION_170}, + {major: '19.0', vstr: '190', vernum: KMX.KMXFile.VERSION_190}, ]; for(const v of versions) { it(`should build a version ${v.major} keyboard`, async function() { + const compiler = new KmnCompiler(); + assert.isTrue(await compiler.init(callbacks, {saveDebug: true})); + assert.isTrue(compiler.verifyInitialized()); + const fixtureName = makePathToFixture('features', `version_${v.vstr}.kmn`); const result = await compiler.run(fixtureName, `version_${v.vstr}.kmx`); - if(result === null) callbacks.printMessages(); assert.isNotNull(result); const reader = new KmxFileReader(); @@ -41,4 +52,27 @@ describe('Keyboard compiler features', function() { assert.equal(keyboard.fileVersion, v.vernum); }); } + + [ + {s:'VERSION_60', t:undefined, e:KMX.KMXFile.VERSION_60}, + {s:'VERSION_170', t:KMX.KMXFile.VERSION_170, e:KMX.KMXFile.VERSION_170}, + {s:'VERSION_190', t:KMX.KMXFile.VERSION_190, e:KMX.KMXFile.VERSION_190}, + ].forEach(function(v) { + it(`should build a minimum version keyboard to ${v.s} with targetVersion=${v.t}`, async function() { + const compiler = new KmnCompiler(); + assert.isTrue(await compiler.init(callbacks, { + saveDebug: true, + targetVersion: v.t, + })); + assert.isTrue(compiler.verifyInitialized()); + const fixtureName = makePathToFixture('features', `unversioned.kmn`); + + const result = await compiler.run(fixtureName, `unversioned.kmx`); + assert.isNotNull(result); + + const reader = new KmxFileReader(); + const keyboard = reader.read(result.artifacts.kmx.data); + assert.equal(keyboard.fileVersion, v.e); + }); + }); }); diff --git a/developer/src/kmc-kmn/test/fixtures/embed-osk/khmer_angkor.kvks b/developer/src/kmc-kmn/test/fixtures/embed-osk/khmer_angkor.kvks new file mode 100644 index 00000000000..f0f9bc775d2 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/embed-osk/khmer_angkor.kvks @@ -0,0 +1,206 @@ + + +
+ 10.0 + khmer_angkor + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + ] + [ + / + . + + + + & + + * + @ + \ + } + { + - + ÷ + : + , + + ; + < + # + > + × + $ + +   + + + + + + + + + + + + + ᧿ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ុំ + + + + + + + + + + + + + + + + + + + « + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ! + + " + + % + + ( + ) + + = + ោះ + ុះ + + ? + + ាំ + + + + + + + + + + + + + + + + + + + + + េះ + + + + + + + + + + » + + +
diff --git a/developer/src/kmc-kmn/test/fixtures/features/unversioned.kmn b/developer/src/kmc-kmn/test/fixtures/features/unversioned.kmn new file mode 100644 index 00000000000..9844ceaa3a3 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/features/unversioned.kmn @@ -0,0 +1,9 @@ +c Description: Verifies that kmcmplib can compile a keyboard +c without a version statement, that either picks up targetVersion, +c or uses the keyboard's features to determine minimum version. + +store(&NAME) 'unversioned' + +begin unicode > use(main) + +group(main) using keys diff --git a/developer/src/kmc-kmn/test/fixtures/features/version_190.kmn b/developer/src/kmc-kmn/test/fixtures/features/version_190.kmn new file mode 100644 index 00000000000..7883a2cda4a --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/features/version_190.kmn @@ -0,0 +1,8 @@ +c Description: Verifies that kmcmplib can compile a v19.0 keyboard + +store(&NAME) 'version_190' +store(&VERSION) '19.0' + +begin unicode > use(main) + +group(main) using keys diff --git a/developer/src/kmc-kmn/test/kmw/constants.tests.ts b/developer/src/kmc-kmn/test/kmw/constants.tests.ts new file mode 100644 index 00000000000..69dab212ef1 --- /dev/null +++ b/developer/src/kmc-kmn/test/kmw/constants.tests.ts @@ -0,0 +1,152 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by mcdurdin on 2025-10-16 + * + * Key cap special values (such as "*Shift*") are defined in multiple modules; + * this data is not currently in a common module, as it would create unhelpful + * dependencies or require significant refactoring. So, instead, for now we just + * verify that the values line up. Note that the _imported_ files are copied in + * by build.sh before running the tests in order to avoid pathing issues. + * + * This is testing that the values in these files are equivalent: + * developer/src/tike/xml/layoutbuilder/constants.js + * web/src/engine/osk/src/specialCharacters.ts + * developer/src/kmc-kmn/kmw-compiler/constants.ts + * core/include/ldml/keyman_core_ldml.ts + * developer/docs/help/reference/file-types/keyman-touch-layout.md + */ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import 'mocha'; +import {assert} from 'chai'; + +import keymanWebSpecialCharacters from "./_imported_web_osk_specialCharacters.js"; +import { CSpecialText17, CSpecialText14, CSpecialText10, CSpecialText17ZWNJ } from "../../src/kmw-compiler/constants.js"; +import { builder } from "./_imported_layoutbuilder_constants.js"; +import { constants as coreLdmlConstants } from "@keymanapp/ldml-keyboard-constants"; + +/** Verify key cap constants across 4 modules: KMW treated as primary */ +describe('Key cap special text values from KeymanWeb', function() { + + it('should match key cap special text in Developer Touch Layout Builder', function() { + // The key cap special text objects in these two files should be exactly equal: + // developer/src/tike/xml/layoutbuilder/constants.js + // web/src/engine/osk/src/specialCharacters.ts + assert.deepEqual(builder.specialCharacters, keymanWebSpecialCharacters); + }); + + it('should match key cap special text in Developer kmc-kmn KMW compiler', function() { + // These two files should have the same strings for key caps: + // developer/src/kmc-kmn/kmw-compiler/constants.ts + // web/src/engine/osk/src/specialCharacters.ts + + // No values to compare here - just key names + + // TODO: the following key cap strings are not verified in the compiler, + // why? It also appears that the compiler does not verify when an + // unrecognized key cap string is used + const specialCharactersPatch = Object.keys(keymanWebSpecialCharacters).filter(e => + e !== "*LAlt*" && + e !== "*RAlt*" && + e !== "*LCtrl*" && + e !== "*RCtrl*" && + e !== "*LAltCtrl*" && + e !== "*RAltCtrl*" && + e !== "*LAltCtrlShift*" && + e !== "*RAltCtrlShift*" && + e !== "*AltShift*" && + e !== "*CtrlShift*" && + e !== "*AltCtrlShift*" && + e !== "*LAltShift*" && + e !== "*RAltShift*" && + e !== "*LCtrlShift*" && + e !== "*RCtrlShift*" + ).sort(); + + const compilerSpecialCharacters = [ + ...CSpecialText10.split('\0'), + ...CSpecialText14.split('\0'), + ...CSpecialText17.split('\0'), + CSpecialText17ZWNJ, + ].filter(e => e !== "") // remove blanks coming from the splitting + .sort(); + + assert.deepEqual(compilerSpecialCharacters, specialCharactersPatch); + }); + + // TODO-EMBED-OSK-IN-KMX: enable once we have complete the Core LDML headers + it.skip('should match key cap special text in Core constants', function() { + // These two files should have the same constant values for key caps: + // core/include/ldml/keyman_core_ldml.ts + // web/src/engine/osk/src/specialCharacters.ts + + // The Core constants and KeymanWeb constants vary a little: + // + // 1. We need to special case *ABC* and *abc* because they have identical + // values but we are case-insensitive in our LDML definitions + const specialCharactersPatch: any = {...keymanWebSpecialCharacters}; + specialCharactersPatch['*abc_lower*'] = specialCharactersPatch['*abc*']; + delete specialCharactersPatch['*abc*']; + + specialCharactersPatch['*ABC_upper*'] = specialCharactersPatch['*ABC*']; + delete specialCharactersPatch['*ABC*']; + + // 2. Map all the "*Name*" key names to "dis2_key_cap_name"... + const dis2_key_cap_expected: any = {}; + for(const key of Object.keys(specialCharactersPatch)) { + const newKey = key.replace(/^\*(.+)\*$/, 'dis2_key_cap_$1').toLowerCase(); + dis2_key_cap_expected[newKey] = specialCharactersPatch[key]; + } + + // 3. We only want to compare the dis2_key_cap_ values from the + // coreLdmlConstants object + const coreConstantsFiltered: any = {}; + for(const key of Object.keys(coreLdmlConstants)) { + if(key.match(/^dis2_key_cap_/)) { + coreConstantsFiltered[key] = (coreLdmlConstants)[key]; + } + } + + assert.deepEqual(coreConstantsFiltered, dis2_key_cap_expected); + }); + + it('should be documented correctly', function() { + // These two files should have the same constant values for key caps: + // developer/docs/help/reference/file-types/keyman-touch-layout.md + // web/src/engine/osk/src/specialCharacters.ts + const helpFile = + path.join(path.dirname(fileURLToPath(import.meta.url)), '../../../../../docs/help/reference/file-types/keyman-touch-layout.md'); + const helpLines = fs.readFileSync(helpFile, 'utf-8').replaceAll(/\r\n/g, '\n').split('\n'); + + // Find the relevant section in the file between start:special_key_caps and + // end:special_key_caps + const line0 = helpLines.findIndex(line => line.includes('start:special_key_caps')); + assert.notEqual(line0, -1); + const line1 = helpLines.findIndex(line => line.includes('end:special_key_caps')); + assert.notEqual(line1, -1); + + const content = helpLines.slice(line0+1, line1); + + const markdownConstants: any = {}; + + // Iterate over cells in the relevant section of the file and build an + // array of values, assuming the format of the markdown doesn't change + let nextCap: string = null; + for(const line of content) { + const mCap = line.match(/`(\*.+\*)`<\/td>/i); + if(mCap) { + nextCap = mCap[1]; + continue; + } + const mVal = line.match(/(.+);<\/td>/); + if(mVal) { + markdownConstants[nextCap] = parseInt(mVal[1], 16); + } + } + + assert.deepEqual(markdownConstants, keymanWebSpecialCharacters); + }); +}); diff --git a/developer/src/kmc-kmn/test/tsconfig.json b/developer/src/kmc-kmn/test/tsconfig.json index dee653dfac4..82f24a7385c 100644 --- a/developer/src/kmc-kmn/test/tsconfig.json +++ b/developer/src/kmc-kmn/test/tsconfig.json @@ -10,6 +10,8 @@ }, "include": [ "**/*.tests.ts", + "./kmw/_imported_layoutbuilder_constants.js", + "./kmw/_imported_web_osk_specialCharacters.ts", "./helpers/index.ts", "./kmw/util.ts" ], diff --git a/developer/src/kmc-ldml/src/compiler/compiler.ts b/developer/src/kmc-ldml/src/compiler/compiler.ts index d66631e0b56..e0b616be4ea 100644 --- a/developer/src/kmc-ldml/src/compiler/compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/compiler.ts @@ -27,7 +27,7 @@ import { StrsCompiler, ElemCompiler, ListCompiler, UsetCompiler } from './empty- import LDMLKeyboardXMLSourceFile = LDMLKeyboard.LDMLKeyboardXMLSourceFile; import KMXPlusFile = KMXPlus.KMXPlusFile; import DependencySections = KMXPlus.DependencySections; -import { SectionIdent, constants } from '@keymanapp/ldml-keyboard-constants'; +import { KMXPlusVersion, SectionIdent, constants } from '@keymanapp/ldml-keyboard-constants'; import { KmnCompiler } from '@keymanapp/kmc-kmn'; import { KMXPlusMetadataCompiler } from './metadata-compiler.js'; import { LdmlKeyboardVisualKeyboardCompiler } from './visual-keyboard-compiler.js'; @@ -93,6 +93,7 @@ export interface LdmlKeyboardCompilerResult extends KeymanCompilerResult { export class LdmlKeyboardCompiler implements KeymanCompiler { private callbacks: CompilerCallbacks; private options: LdmlCompilerOptions; + private kmxPlusTargetVersion: KMXPlusVersion; // uset parser private usetparser?: LdmlKeyboardTypes.UnicodeSetParser = undefined; @@ -111,6 +112,14 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { this.reader = new LDMLKeyboardXMLSourceFileReader(this.options.readerOptions, callbacks); // wrap the callbacks so that the eventresolver is called this.callbacks = new ResolvingCompilerCallbacks(this.reader, this.options, callbacks); + + // resolve and check command line parameters + + this.kmxPlusTargetVersion = this.targetVersionToKmxPlusVersion(this.options.targetVersion); + if(!this.kmxPlusTargetVersion) { + return false; + } + return true; } @@ -165,7 +174,7 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { }); } - const kmxBinary = kmxBuilder.compile(); + const kmxBinary = kmxBuilder.compile(this.kmxPlusTargetVersion); const kvkWriter = new KvkFileWriter(); const kvkBinary = vkData ? kvkWriter.write(vkData) : null; @@ -232,7 +241,7 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { } private buildSections(source: LDMLKeyboardXMLSourceFile) { - return SECTION_COMPILERS.map(c => new c(source, this.callbacks)); + return SECTION_COMPILERS.map(c => new c(source, this.callbacks, this.kmxPlusTargetVersion)); } /** @@ -357,10 +366,12 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { * @returns KMXPlusFile intermediate file */ public async compile(source: LDMLKeyboardXMLSourceFile, postValidate?: boolean): Promise { + // TODO-EMBED-OSK-IN-KMX: add a unitTestEndpoints prop and make this private + const sections = this.buildSections(source); let passed = true; - const kmx = new KMXPlusFile(); + const kmx = new KMXPlusFile(this.kmxPlusTargetVersion); for (const section of sections) { if (!section.validate()) { @@ -424,5 +435,20 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { return passed ? kmx : null; } + + private targetVersionToKmxPlusVersion(version?: KMX.KMX_Version): KMXPlusVersion { + if(version === undefined || version === null) { + return KMXPlusVersion.Version17; + } + if(version === KMX.KMX_Version.VERSION_170) { + return KMXPlusVersion.Version17; + } + if(version === KMX.KMX_Version.VERSION_190) { + return KMXPlusVersion.Version19; + } + + this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidTargetVersion({ version })); + return null; + } } diff --git a/developer/src/kmc-ldml/src/compiler/disp.ts b/developer/src/kmc-ldml/src/compiler/disp.ts index b38ef725312..3e402bc64c9 100644 --- a/developer/src/kmc-ldml/src/compiler/disp.ts +++ b/developer/src/kmc-ldml/src/compiler/disp.ts @@ -1,4 +1,4 @@ -import { constants } from "@keymanapp/ldml-keyboard-constants"; +import { constants, KMXPlusVersion } from "@keymanapp/ldml-keyboard-constants"; import { LDMLKeyboard } from '@keymanapp/developer-utils'; import { KMXPlus } from "@keymanapp/common-types"; @@ -65,32 +65,56 @@ export class DispCompiler extends SectionCompiler { result.baseCharacter = sections.strs.allocString(this.keyboard3.displays?.displayOptions?.baseCharacter, { unescape: true, compileContext:this.keyboard3?.displays?.displayOptions }); // displays - result.disps = this.keyboard3.displays?.display.map(display => ({ - to: sections.strs.allocString(display.output, { - stringVariables: true, - markers: true, - unescape: true, - compileContext: display, - }, sections), - id: sections.strs.allocString(display.keyId, { compileContext: display }), // not escaped, not substituted - display: sections.strs.allocString(display.display, { - stringVariables: true, - unescape: true, - compileContext: display, - }, sections), - })) || []; // TODO-LDML: need coverage for the [] + if(this.targetVersion == KMXPlusVersion.Version17) { + result.disps = this.keyboard3.displays?.display.map(display => ({ + to: sections.strs.allocString(display.output, { + stringVariables: true, + markers: true, + unescape: true, + compileContext: display, + }, sections), + id: sections.strs.allocString(display.keyId, { compileContext: display }), // not escaped, not substituted + display: sections.strs.allocString(display.display, { + stringVariables: true, + unescape: true, + compileContext: display, + }, sections), + flags: 0, + toId: null as KMXPlus.StrsItem, + })) || []; // TODO-LDML: need coverage for the [] - result.disps.sort((a: DispItem, b: DispItem) => { - // sort 'id' first (empty string will be lower) - const idDiff = a.id.compareTo(b.id); - if (idDiff != 0) { - return idDiff; - } else { - // sort by 'to' - return a.to.compareTo(b.to); - } - }); + result.disps.sort((a: DispItem, b: DispItem) => { + // sort 'id' first (empty string will be lower) + const idDiff = a.id.compareTo(b.id); + if (idDiff != 0) { + return idDiff; + } else { + // sort by 'to' + return a.to.compareTo(b.to); + } + }); + } else { // v19 + result.disps = this.keyboard3.displays?.display.map(display => ({ + to: null as KMXPlus.StrsItem, + id: null as KMXPlus.StrsItem, + toId: display.output + ? sections.strs.allocString(display.output, { + stringVariables: true, + markers: true, + unescape: true, + compileContext: display, + }, sections) + : sections.strs.allocString(display.keyId, { compileContext: display }), // not escaped, not substituted + display: sections.strs.allocString(display.display, { + stringVariables: true, + unescape: true, + compileContext: display, + }, sections), + flags: display.output ? 0 : constants.disp_item_flags_is_id, // TODO-EMBED-OSK-IN-KMX: we need to provide other flags, such as frame keys + })) || []; // TODO-LDML: need coverage for the [] + result.disps.sort((a: DispItem, b: DispItem) => a.toId.compareTo(b.toId)); + } return result; } } diff --git a/developer/src/kmc-ldml/src/compiler/empty-compiler.ts b/developer/src/kmc-ldml/src/compiler/empty-compiler.ts index a88e861f582..c267092bf5c 100644 --- a/developer/src/kmc-ldml/src/compiler/empty-compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/empty-compiler.ts @@ -1,4 +1,4 @@ -import { SectionIdent, constants } from '@keymanapp/ldml-keyboard-constants'; +import { KMXPlusVersion, SectionIdent, constants } from '@keymanapp/ldml-keyboard-constants'; import { SectionCompiler } from "./section-compiler.js"; import { util, KMXPlus, LdmlKeyboardTypes } from "@keymanapp/common-types"; import { CompilerCallbacks, LDMLKeyboard } from "@keymanapp/developer-utils"; @@ -11,8 +11,8 @@ import { LdmlCompilerMessages } from './ldml-compiler-messages.js'; */ export abstract class EmptyCompiler extends SectionCompiler { private _id: SectionIdent; - constructor(id: SectionIdent, source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks) { - super(source, callbacks); + constructor(id: SectionIdent, source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks, targetVersion: KMXPlusVersion) { + super(source, callbacks, targetVersion); this._id = id; } @@ -25,8 +25,8 @@ export abstract class EmptyCompiler extends SectionCompiler { } export class StrsCompiler extends EmptyCompiler { - constructor(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks) { - super(constants.section.strs, source, callbacks); + constructor(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks, targetVersion: KMXPlusVersion) { + super(constants.section.strs, source, callbacks, targetVersion); } public compile(sections: KMXPlus.DependencySections): KMXPlus.Section { return new KMXPlus.Strs(); @@ -99,8 +99,8 @@ export class StrsCompiler extends EmptyCompiler { } export class ElemCompiler extends EmptyCompiler { - constructor(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks) { - super(constants.section.elem, source, callbacks); + constructor(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks, targetVersion: KMXPlusVersion) { + super(constants.section.elem, source, callbacks, targetVersion); } public compile(sections: KMXPlus.DependencySections): KMXPlus.Section { return new KMXPlus.Elem(sections); @@ -112,8 +112,8 @@ export class ElemCompiler extends EmptyCompiler { } export class ListCompiler extends EmptyCompiler { - constructor(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks) { - super(constants.section.list, source, callbacks); + constructor(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks, targetVersion: KMXPlusVersion) { + super(constants.section.list, source, callbacks, targetVersion); } public compile(sections: KMXPlus.DependencySections): KMXPlus.Section { return new KMXPlus.List(sections.strs); @@ -125,8 +125,8 @@ export class ListCompiler extends EmptyCompiler { } export class UsetCompiler extends EmptyCompiler { - constructor(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks) { - super(constants.section.uset, source, callbacks); + constructor(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks, targetVersion: KMXPlusVersion) { + super(constants.section.uset, source, callbacks, targetVersion); } public compile(sections: KMXPlus.DependencySections): KMXPlus.Section { return new KMXPlus.Uset(); diff --git a/developer/src/kmc-ldml/src/compiler/keys.ts b/developer/src/kmc-ldml/src/compiler/keys.ts index 8656e7fbf2f..fa52bc7b705 100644 --- a/developer/src/kmc-ldml/src/compiler/keys.ts +++ b/developer/src/kmc-ldml/src/compiler/keys.ts @@ -276,7 +276,7 @@ export class KeysCompiler extends SectionCompiler { // now add the reserved key(s). r.set(reserved_gap, KeysCompiler.asReserved({ - flags: constants.keys_key_flags_gap | constants.keys_key_flags_extend, + flags: KMXPlus.KeysKeysFlags.gap | KMXPlus.KeysKeysFlags.extend, id: sections.strs.allocString(reserved_gap), flicks: '', longPress: no_list, @@ -365,10 +365,10 @@ export class KeysCompiler extends SectionCompiler { const key = keyBag.get(keyId); if (!key) continue; // missing key - let flags = 0; + let flags: KMXPlus.KeysKeysFlags = 0; const { flickId, gap, longPressDefaultKeyId, longPressKeyIds, multiTapKeyIds, layerId, output } = key; if (!!gap) { - flags |= constants.keys_key_flags_gap; + flags |= KMXPlus.KeysKeysFlags.gap; } const id = sections.strs.allocString(key.id, { compileContext: key }); const longPress: ListItem = sections.list.allocListFromSpaces( diff --git a/developer/src/kmc-ldml/src/compiler/layr.ts b/developer/src/kmc-ldml/src/compiler/layr.ts index baaf9282b54..4e0069200f8 100644 --- a/developer/src/kmc-ldml/src/compiler/layr.ts +++ b/developer/src/kmc-ldml/src/compiler/layr.ts @@ -7,7 +7,7 @@ import { translateLayerAttrToModifier, validModifier } from '../util/util.js'; import DependencySections = KMXPlus.DependencySections; import Layr = KMXPlus.Layr; -import LayrList = KMXPlus.LayrList; +import LayrForm = KMXPlus.LayrForm; import LayrRow = KMXPlus.LayrRow; export class LayrCompiler extends SectionCompiler { @@ -68,7 +68,7 @@ export class LayrCompiler extends SectionCompiler { public compile(sections: DependencySections): Layr { const sect = new Layr(); - sect.lists = this.keyboard3.layers.map((layers) => { + sect.forms = this.keyboard3.layers.map((layers) => { const hardware = sections.strs.allocString(layers.formId, {compileContext: layers}); // Already validated in validate const layerEntries = []; @@ -90,12 +90,16 @@ export class LayrCompiler extends SectionCompiler { }); } } - const list: LayrList = { + const form: LayrForm = { hardware, minDeviceWidth: layers.minDeviceWidth || 0, layers: layerEntries, + baseLayout: sections.strs.allocString('', {compileContext: sect}), // TODO-EMBED-OSK-IN-KMX + fontFaceName: sections.strs.allocString('', {compileContext: sect}), // TODO-EMBED-OSK-IN-KMX + fontSizePct: 100, // TODO-EMBED-OSK-IN-KMX + flags: 0, // TODO-EMBED-OSK-IN-KMX }; - return list; + return form; }); return sect; } diff --git a/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts b/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts index 1f879f475b9..b947f29f649 100644 --- a/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts +++ b/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts @@ -292,6 +292,12 @@ export class LdmlCompilerMessages { `**Hint**: Use "${def(o.recommended)}"`, ); + static ERROR_InvalidTargetVersion = SevError | 0x0031; + static Error_InvalidTargetVersion = (o: {version: number}) => m( + this.ERROR_InvalidTargetVersion, + `Target version ${def(o.version)} is not a valid version. Only 17.0 and 19.0 target versions are currently supported for LDML keyboards."`, + ); + // // Transform syntax errors begin at ...F00 (SevErrorTransform) diff --git a/developer/src/kmc-ldml/src/compiler/section-compiler.ts b/developer/src/kmc-ldml/src/compiler/section-compiler.ts index dd83a9deda7..000bfe62fc2 100644 --- a/developer/src/kmc-ldml/src/compiler/section-compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/section-compiler.ts @@ -1,14 +1,14 @@ import { KMXPlus } from "@keymanapp/common-types"; import { CompilerCallbacks, LDMLKeyboard } from "@keymanapp/developer-utils"; -import { SectionIdent, constants } from '@keymanapp/ldml-keyboard-constants'; +import { KMXPlusVersion, SectionIdent, constants } from '@keymanapp/ldml-keyboard-constants'; /** newable interface to SectionCompiler c'tor */ -export type SectionCompilerNew = new (source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks) => SectionCompiler; +export type SectionCompilerNew = new (source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks, targetVersion: KMXPlusVersion) => SectionCompiler; export abstract class SectionCompiler { protected readonly keyboard3: LDMLKeyboard.LKKeyboard; protected readonly callbacks: CompilerCallbacks; - constructor(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks) { + constructor(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks, public readonly targetVersion: KMXPlusVersion) { this.keyboard3 = source.keyboard3; this.callbacks = callbacks; } diff --git a/developer/src/kmc-ldml/src/compiler/tran.ts b/developer/src/kmc-ldml/src/compiler/tran.ts index 0a3a781f6f4..bf0d0e4f37e 100644 --- a/developer/src/kmc-ldml/src/compiler/tran.ts +++ b/developer/src/kmc-ldml/src/compiler/tran.ts @@ -1,4 +1,4 @@ -import { constants, SectionIdent } from "@keymanapp/ldml-keyboard-constants"; +import { constants, KMXPlusVersion, SectionIdent } from "@keymanapp/ldml-keyboard-constants"; import { KMXPlus, LdmlKeyboardTypes, util } from '@keymanapp/common-types'; import { CompilerCallbacks, LDMLKeyboard } from "@keymanapp/developer-utils"; import { ObjectWithCompileContext } from "@keymanapp/common-types"; @@ -53,8 +53,8 @@ export abstract class TransformCompiler { - constructor(source: LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks) { - super(source, callbacks); + constructor(source: LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks, targetVersion: KMXPlusVersion) { + super(source, callbacks, targetVersion); this.type = 'simple'; } protected newTran(): Tran { @@ -479,8 +479,8 @@ export class TranCompiler extends TransformCompiler<'simple', Tran /*, TranItem* }; export class BkspCompiler extends TransformCompiler<'backspace', Bksp /*, BkspItem*/> { - constructor(source: LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks) { - super(source, callbacks); + constructor(source: LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks, targetVersion: KMXPlusVersion) { + super(source, callbacks, targetVersion); this.type = 'backspace'; } protected newTran(): Bksp { diff --git a/developer/src/kmc-ldml/src/compiler/vars.ts b/developer/src/kmc-ldml/src/compiler/vars.ts index ed27393312f..673ae2467ea 100644 --- a/developer/src/kmc-ldml/src/compiler/vars.ts +++ b/developer/src/kmc-ldml/src/compiler/vars.ts @@ -1,4 +1,4 @@ -import { SectionIdent, constants } from "@keymanapp/ldml-keyboard-constants"; +import { KMXPlusVersion, SectionIdent, constants } from "@keymanapp/ldml-keyboard-constants"; import { KMXPlus, LdmlKeyboardTypes } from '@keymanapp/common-types'; import { ObjectWithCompileContext } from "@keymanapp/common-types"; import { LDMLKeyboard, CompilerCallbacks } from '@keymanapp/developer-utils'; @@ -29,8 +29,8 @@ export class VarsCompiler extends SectionCompiler { return defaults; } - constructor(source: LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks) { - super(source, callbacks); + constructor(source: LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks, targetVersion: KMXPlusVersion) { + super(source, callbacks, targetVersion); } public validate(): boolean { diff --git a/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts b/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts index 480ac5add52..53e73336262 100644 --- a/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts @@ -5,9 +5,10 @@ * to .kvk format. This is an interim solution until Keyman Core supports * interrogation of the KMX+ data for OSK. */ -import { ModifierKeyConstants, KMXPlus, VisualKeyboard } from "@keymanapp/common-types"; +import { KMXPlus, VisualKeyboard, translateLdmlModifiersToVisualKeyboardShift } from "@keymanapp/common-types"; import { CompilerCallbacks } from "@keymanapp/developer-utils"; import { LdmlCompilerMessages } from "./ldml-compiler-messages.js"; +import { constants } from "@keymanapp/ldml-keyboard-constants"; // This is a partial polyfill for findLast, so not polluting Array.prototype // https://medium.com/@stheodorejohn/findlast-method-polyfill-in-javascript-bridging-browser-gaps-c3baf6aabae1 @@ -26,16 +27,6 @@ function findLast(arr: any, callback: any) { return undefined; } - -const LDML_MODIFIER_TO_KVK_MODIFIER = new Map(); -LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.LCTRLFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_LCTRL); -LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.RCTRLFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_RCTRL); -LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.LALTFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_LALT); -LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.RALTFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_RALT); -LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.K_SHIFTFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_SHIFT); -LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.K_CTRLFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_CTRL); -LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.K_ALTFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_ALT); - export class LdmlKeyboardVisualKeyboardCompiler { public constructor(private callbacks: CompilerCallbacks) { } @@ -60,13 +51,13 @@ export class LdmlKeyboardVisualKeyboardCompiler { let hasVisualKeyboard = false; - for(const layersList of source.layr.lists) { - const formId = layersList.hardware.value; + for(const layersForm of source.layr.forms) { + const formId = layersForm.hardware.value; if(formId == 'touch') { continue; } - for(const layer of layersList.layers) { + for(const layer of layersForm.layers) { const res = this.compileHardwareLayer(source, result, layer, formId); if(res === false) { // failed to compile the layer @@ -97,7 +88,7 @@ export class LdmlKeyboardVisualKeyboardCompiler { hardware = 'us'; // TODO-LDML: US Only. We need to clean this up for other hardware forms - const shift = this.translateLayerModifiersToVisualKeyboardShift(layer.mod); + const shift = translateLdmlModifiersToVisualKeyboardShift(layer.mod); if(shift === null) { // Caps (num, scroll) is not a supported shift state in .kvk return null; @@ -136,33 +127,15 @@ export class LdmlKeyboardVisualKeyboardCompiler { } private getDisplayFromKey(keydef: KMXPlus.KeysKeys, source: KMXPlus.KMXPlusData) { - const display = source.disp?.disps?.find(d => d.id.value == keydef.id.value || d.to.value == keydef.to.value); + // if(source.disp.disps[0].toId) + const display = source.disp?.disps?.find(d => + d.toId !== null + ? (d.toId.value == keydef.id.value && d.flags & constants.disp_item_flags_is_id) || + (d.toId.value == keydef.to.value && !(d.flags & constants.disp_item_flags_is_id)) + : d.id.value == keydef.id.value || d.to.value == keydef.to.value); const value = display?.display.value ?? keydef.to.value; // strip markers from the output (these are valid in keydef.to, but not in display.display, nor in kvk) return value.replaceAll(/\uffff\u0008./g, ''); } - private translateLayerModifiersToVisualKeyboardShift(modifiers: number): VisualKeyboard.VisualKeyboardShiftState { - - if(modifiers == 0) { - return VisualKeyboard.VisualKeyboardShiftState.KVKS_NORMAL; - } - - if(modifiers & - (ModifierKeyConstants.CAPITALFLAG | ModifierKeyConstants.NUMLOCKFLAG | ModifierKeyConstants.SCROLLFLAG) - ) { - // Caps/Num/Scroll are not supported in .kvk, in combination or alone - return null; - } - - let shift: VisualKeyboard.VisualKeyboardShiftState = 0; - - for(const mod of LDML_MODIFIER_TO_KVK_MODIFIER.keys()) { - if(modifiers & mod) { - shift |= LDML_MODIFIER_TO_KVK_MODIFIER.get(mod); - } - } - - return shift; - } } diff --git a/developer/src/kmc-ldml/src/util/serialize.ts b/developer/src/kmc-ldml/src/util/serialize.ts index 2a7391d0c6a..6f6fe365811 100644 --- a/developer/src/kmc-ldml/src/util/serialize.ts +++ b/developer/src/kmc-ldml/src/util/serialize.ts @@ -177,11 +177,11 @@ export function kmxToXml(kmx: KMXPlus.KMXPlusFile): string { } function getLayers() { - if (!layr?.lists?.length) { + if (!layr?.forms?.length) { return {}; } return { - layers: layr.lists.map(({ hardware, minDeviceWidth, layers }) => ({ + layers: layr.forms.map(({ hardware, minDeviceWidth, layers }) => ({ ...stringToAttr('formId', hardware), ...numberToAttr('minDeviceWidth', minDeviceWidth), layer: layers.map(({ id, mod, rows }) => ({ diff --git a/developer/src/kmc-ldml/test/compiler-e2e.tests.ts b/developer/src/kmc-ldml/test/compiler-e2e.tests.ts index 7c05d39d227..24cdbb53885 100644 --- a/developer/src/kmc-ldml/test/compiler-e2e.tests.ts +++ b/developer/src/kmc-ldml/test/compiler-e2e.tests.ts @@ -1,3 +1,4 @@ +import * as fs from 'node:fs'; import 'mocha'; import {assert} from 'chai'; import hextobin from '@keymanapp/hextobin'; @@ -7,43 +8,121 @@ import { LdmlKeyboardCompiler } from '../src/compiler/compiler.js'; import { kmxToXml } from '../src/util/serialize.js'; import { writeFileSync } from 'node:fs'; import { LdmlCompilerMessages } from '../src/main.js'; -import { util } from '@keymanapp/common-types'; +import { KMX, util } from '@keymanapp/common-types'; +import { KMXPlusBuilder, SectionBuilders } from '@keymanapp/developer-utils'; +import { constants, KMXPlusVersion } from '@keymanapp/ldml-keyboard-constants'; + +const debug=false; /** Overall compiler tests */ describe('compiler-tests', function() { this.slow(500); // 0.5 sec -- json schema validation takes a while - before(function() { + beforeEach(function() { compilerTestCallbacks.clear(); }); - it('should-build-fixtures', async function() { - this.timeout(4000); - // Let's build basic.xml - // It should match basic.kmx (built from basic.txt) + afterEach(function() { + if (this.currentTest.state !== 'passed') { + compilerTestCallbacks.printMessages(); + } + }); - const inputFilename = makePathToFixture('basic.xml'); - const binaryFilename = makePathToFixture('basic.txt'); + [ + [17, KMX.KMX_Version.VERSION_170], + [19, KMX.KMX_Version.VERSION_190], + ].forEach(([vernum, version]) => { + it(`should-build-fixtures for v${vernum}.0`, async function() { + this.timeout(4000); + // Let's build basic.xml + // It should match basic.kmx (built from basic.txt) + + const inputFilename = makePathToFixture('basic.xml'); + const binaryFilename = makePathToFixture(`basic-${vernum}.txt`); + + // Compare output + const expected = await hextobin(binaryFilename, undefined, {silent:true}); + + // now compare it to use with run() + // Let's build basic.xml + // It should match basic-vv.kmx (built from basic.txt) + const k = new LdmlKeyboardCompiler(); + await k.init(compilerTestCallbacks, { ...compilerTestOptions, targetVersion: version, saveDebug: true, shouldAddCompilerVersion: false }); + + const { artifacts } = await k.run(inputFilename, "basic-xml.kmx"); // need the exact name passed to build-fixtures + assert.isNotNull(artifacts); + const { kmx, kvk } = artifacts; + assert.isNotNull(kmx); + assert.isNotNull(kmx.data); + if(debug) { + fs.writeFileSync(makePathToFixture(`basic-${vernum}-actual.kmx`), kmx.data); + fs.writeFileSync(makePathToFixture(`basic-${vernum}-expected.kmx`), expected); + } + assert.deepEqual(kmx.data, expected); + + // TODO-LDML: compare the .kvk file to something else? + assert.isNotNull(kvk?.data); + }); + }); - // Compare output - const expected = await hextobin(binaryFilename, undefined, {silent:true}); + it('should not build a v19 file with incorrect section versions for sect, disp, and layr', async function() { + const inputFilename = makePathToFixture('basic.xml'); + const kmxPlusBuilder = await runKmxPlusCompiler(inputFilename, KMX.KMX_Version.VERSION_190); + + assert.equal(kmxPlusBuilder.sect.sect?.header.ident, constants.sectionid_sec2); + + // verify sections that should be v19 - sect (aka sec2), disp, and layr + assertSectionVersions( + kmxPlusBuilder.sect, + ['sect','disp','layr'], + KMXPlusVersion.Version19 + ); + + // verify sections that should be v17 - all should be present + assertSectionVersions( + kmxPlusBuilder.sect, + ['bksp','elem','keys','list','loca','meta','strs','tran','uset','vars'], + KMXPlusVersion.Version17 + ); + }); - // now compare it to use with run() - // Let's build basic.xml - // It should match basic.kmx (built from basic.txt) - const k = new LdmlKeyboardCompiler(); - await k.init(compilerTestCallbacks, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }); + it('should not build a v17 file with incorrect section versions', async function() { + const inputFilename = makePathToFixture('basic.xml'); + const kmxPlusBuilder = await runKmxPlusCompiler(inputFilename, KMX.KMX_Version.VERSION_170); - const { artifacts } = await k.run(inputFilename, "basic-xml.kmx"); // need the exact name passed to build-fixtures - assert.isNotNull(artifacts); - const { kmx, kvk } = artifacts; - assert.isNotNull(kmx); - assert.deepEqual(kmx?.data, expected); + assert.equal(kmxPlusBuilder.sect.sect?.header.ident, constants.hex_section_id(constants.section.sect)); - // TODO-LDML: compare the .kvk file to something else? - assert.isNotNull(kvk?.data); + assertSectionVersions( + kmxPlusBuilder.sect, + ['bksp','disp','elem','keys','layr','list','loca','meta','sect','strs','tran','uset','vars'], + KMXPlusVersion.Version17 + ); }); + // Helper functions + + function assertSectionVersions(sect: SectionBuilders, idents: (keyof SectionBuilders)[], version: KMXPlusVersion) { + idents.forEach(ident => { + assert.isNotNull(sect[ident]); + assert.isNotNull(sect[ident].header); + assert.equal(sect[ident].header.version, version); + }); + } + + async function runKmxPlusCompiler(inputFilename: string, version: KMX.KMX_Version) { + const compiler = new LdmlKeyboardCompiler(); + assert.isTrue(await compiler.init(compilerTestCallbacks, { ...compilerTestOptions, targetVersion: version })); + const source = compiler.load(inputFilename); + assert.isNotNull(source); + const kmxPlusFile = await compiler.compile(source, true); + assert.isNotNull(kmxPlusFile); + + const kmxPlusBuilder = new KMXPlusBuilder(kmxPlusFile); + assert.isNotNull(kmxPlusBuilder.compile()); + return kmxPlusBuilder; + } + + it('should-validate-on-run compiling sections/strs/invalid-illegal.xml', async function() { this.timeout(4000); const inputFilename = makePathToFixture('sections/strs/invalid-illegal.xml'); diff --git a/developer/src/kmc-ldml/test/dependencies.tests.ts b/developer/src/kmc-ldml/test/dependencies.tests.ts index 5171648c4a6..ec600737a86 100644 --- a/developer/src/kmc-ldml/test/dependencies.tests.ts +++ b/developer/src/kmc-ldml/test/dependencies.tests.ts @@ -1,13 +1,13 @@ import 'mocha'; import {assert} from 'chai'; import { SECTION_COMPILERS } from '../src/compiler/compiler.js'; -import { SectionIdent } from '@keymanapp/ldml-keyboard-constants'; +import { KMXPlusVersion, SectionIdent } from '@keymanapp/ldml-keyboard-constants'; describe('test of section compiler dependencies', () => { it('should have dependencies in the right order', () => { const sects : Set = new Set(); for (const sect of SECTION_COMPILERS) { // construct the compiler - const c = new sect({ keyboard3: null }, null); // For now, this is OK for the inspection + const c = new sect({ keyboard3: null }, null, KMXPlusVersion.Version17); // For now, this is OK for the inspection const id = c.id; assert.ok(id); assert.isFalse(sects.has(id), `Duplicate compiler ${id} in SECTION_COMPILERS`); diff --git a/developer/src/kmc-ldml/test/disp.tests.ts b/developer/src/kmc-ldml/test/disp.tests.ts index 8b17b35c55c..13618570299 100644 --- a/developer/src/kmc-ldml/test/disp.tests.ts +++ b/developer/src/kmc-ldml/test/disp.tests.ts @@ -7,19 +7,22 @@ import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js' import Disp = KMXPlus.Disp; import { withOffset } from '@keymanapp/developer-utils'; +import { KMXPlusVersion } from '@keymanapp/ldml-keyboard-constants'; describe('disp', function () { this.slow(500); // 0.5 sec -- json schema validation takes a while + // TODO-EMBED-OSK-IN-KMX: add v19 tests + it('should compile minimal disp', async function() { - const disp = await loadSectionFixture(DispCompiler, 'sections/disp/minimal.xml', compilerTestCallbacks) as Disp; + const disp = await loadSectionFixture(DispCompiler, 'sections/disp/minimal.xml', compilerTestCallbacks, KMXPlusVersion.Version17) as Disp; assert.equal(compilerTestCallbacks.messages.length, 0); assert.ok(disp?.disps); assert.equal(disp.disps.length, 0); }); it('should compile typical disp', async function() { - const disp = await loadSectionFixture(DispCompiler, 'sections/disp/typical.xml', compilerTestCallbacks) as Disp; + const disp = await loadSectionFixture(DispCompiler, 'sections/disp/typical.xml', compilerTestCallbacks, KMXPlusVersion.Version17) as Disp; assert.sameDeepMembers(compilerTestCallbacks.messages, []); assert.ok(disp?.disps); @@ -28,7 +31,7 @@ describe('disp', function () { assert.equal(disp.disps[0].display?.value, '`'); }); it('should compile maximal disp', async function() { - const disp = await loadSectionFixture(DispCompiler, 'sections/disp/maximal.xml', compilerTestCallbacks) as Disp; + const disp = await loadSectionFixture(DispCompiler, 'sections/disp/maximal.xml', compilerTestCallbacks, KMXPlusVersion.Version17) as Disp; assert.equal(compilerTestCallbacks.messages.length, 0); assert.equal(disp?.baseCharacter?.value, 'x'); assert.ok(disp?.disps); @@ -41,7 +44,7 @@ describe('disp', function () { assert.equal(disp.disps[2].display?.value, '(g)'); }); it('should compile escaped disp', async function() { - const disp = await loadSectionFixture(DispCompiler, 'sections/disp/escaped.xml', compilerTestCallbacks) as Disp; + const disp = await loadSectionFixture(DispCompiler, 'sections/disp/escaped.xml', compilerTestCallbacks, KMXPlusVersion.Version17) as Disp; assert.equal(compilerTestCallbacks.messages.length, 0); assert.equal(disp?.baseCharacter?.value, 'x'); assert.ok(disp?.disps); @@ -52,38 +55,38 @@ describe('disp', function () { assert.equal(disp.disps[1].display?.value, '(f)'); }); it('should compile options-only disp', async function() { - const disp = await loadSectionFixture(DispCompiler, 'sections/disp/maximal.xml', compilerTestCallbacks) as Disp; + const disp = await loadSectionFixture(DispCompiler, 'sections/disp/maximal.xml', compilerTestCallbacks, KMXPlusVersion.Version17) as Disp; assert.equal(compilerTestCallbacks.messages.length, 0); assert.equal(disp?.baseCharacter?.value, 'x'); assert.ok(disp?.disps); }); it('should compile disp without converting markers', async function() { - const disp = await loadSectionFixture(DispCompiler, 'sections/disp/not-a-marker.xml', compilerTestCallbacks) as Disp; + const disp = await loadSectionFixture(DispCompiler, 'sections/disp/not-a-marker.xml', compilerTestCallbacks, KMXPlusVersion.Version17) as Disp; assert.equal(compilerTestCallbacks.messages.length, 0); assert.ok(disp?.disps); assert.equal(disp.disps.length, 1); assert.equal(disp.disps[0].display?.value, '\\m{hat}'); }); it('should reject duplicate tos', async function() { - const disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-dupto.xml', compilerTestCallbacks) as Disp; + const disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-dupto.xml', compilerTestCallbacks, KMXPlusVersion.Version17) as Disp; assert.isNull(disp); assert.equal(compilerTestCallbacks.messages.length, 1); assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayIsRepeated({ display: '(e)' }, withOffset(330))); }); it('should reject duplicate ids', async function() { - const disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-dupid.xml', compilerTestCallbacks) as Disp;1 + const disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-dupid.xml', compilerTestCallbacks, KMXPlusVersion.Version17) as Disp; assert.isNull(disp); assert.equal(compilerTestCallbacks.messages.length, 1); assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayIsRepeated({ display: '(e)' }, withOffset(329))); }); it('should reject if neither to nor id', async function() { - const disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-none.xml', compilerTestCallbacks) as Disp; + const disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-none.xml', compilerTestCallbacks, KMXPlusVersion.Version17) as Disp; assert.isNull(disp); assert.equal(compilerTestCallbacks.messages.length, 1); assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayNeedsToOrId({ display: '(f)'}, withOffset(182))); }); it('should reject if both to and id', async function() { - const disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-both.xml', compilerTestCallbacks) as Disp; + const disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-both.xml', compilerTestCallbacks, KMXPlusVersion.Version17) as Disp; assert.isNull(disp); assert.equal(compilerTestCallbacks.messages.length, 1); assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayNeedsToOrId({ display: '(e)' }, withOffset(182))); @@ -96,6 +99,6 @@ describe('disp', function () { LdmlCompilerMessages.Error_MissingStringVariable({id: "missingoutput"}), ], }, - ]); + ], KMXPlusVersion.Version17); }); diff --git a/developer/src/kmc-ldml/test/fixtures/README.md b/developer/src/kmc-ldml/test/fixtures/README.md new file mode 100644 index 00000000000..69351c595a3 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/README.md @@ -0,0 +1,25 @@ +# Fixtures for KMX+ / LDML format files and kmc-ldml compiler + +## basic-xx.txt fixtures + +The set of files named basic-xx.txt describe the expected output of running kmc +against basic.xml with version xx schema and target version. They are used in +the end-to-end test test-compiler-e2e.ts. + +Any changes to the compiler or basic.xml will likely result in changes to the +compiled file. Structural differences should be updated manually in the txt +files to ensure that we are getting the expected result for the e2e test. The +following may be helpful for working with these files when updating the binary +format: + + ```sh + cd developer/src/kmc + ./build.sh configure build # if needed + ./build.sh build-fixtures + ``` + +This will compile both the .xml and the .txt to build/test/fixtures. + +For the format of the files, see “hextobin.ts”. + +P.S. surprisingly, the Dart language highlighter in VSCode does a helpful job on these files. diff --git a/developer/src/kmc-ldml/test/fixtures/basic.txt b/developer/src/kmc-ldml/test/fixtures/basic-17.txt similarity index 96% rename from developer/src/kmc-ldml/test/fixtures/basic.txt rename to developer/src/kmc-ldml/test/fixtures/basic-17.txt index 81748b400e3..3d9891c9cb1 100644 --- a/developer/src/kmc-ldml/test/fixtures/basic.txt +++ b/developer/src/kmc-ldml/test/fixtures/basic-17.txt @@ -1,25 +1,12 @@ # # Keyman is copyright (C) SIL International. MIT License. # -# basic.txt describes the expected output of running kmc against basic.xml. It is used in -# the end-to-end test test-compiler-e2e.ts. +# basic-17.txt describes the expected output of running kmc against basic.xml +# with v17 schema and v17 target version. It is used in the end-to-end test +# test-compiler-e2e.ts. # -# Any changes to the compiler or basic.xml will likely result in changes to the compiled file. -# While structural differences should be updated manually in this file to ensure that we are -# getting the expected result for the e2e test, the checksum can be safely retrieved from the -# updated compilation result. The following may be helpful for working with this file when -# updating the binary format: +# See README.md for more information. # -# cd developer/src/kmc -# ./build.sh configure build # if needed -# ./build.sh build-fixtures -# -# This will compile both the .xml and the .txt to build/test/fixtures and also emit the -# checksum for basic-xml.kmx so you can patch that into this file. -# -# For the format of this file, see “hextobin.ts” -# -# P.S. surprisingly, the Dart language highlighter in VSCode does a helpful job on this file. block(kmxheader) # struct COMP_KEYBOARD { 4b 58 54 53 # KMX_DWORD dwIdentifier; // 0000 Keyman compiled keyboard id @@ -96,7 +83,7 @@ block(store_vk_path_string) 62 00 61 00 73 00 69 00 63 00 2d 00 78 00 6d 00 6c 00 2e 00 6b 00 76 00 6b 00 00 00 # 'basic-xml.kvk' block(sect) # struct COMP_KMXPLUS_SECT { - 73 65 63 74 # KMX_DWORD header.ident; // 0000 Section name + 73 65 63 74 # KMX_DWORD header.ident; // 0000 Section name 'sect' diff(sect,endsect) # KMX_DWORD header.size; // 0004 Section length diff(sect,eof) # KMX_DWORD total; // 0008 KMXPlus entire length sizeof(sectitems,8) # KMX_DWORD count; // 000C number of section headers @@ -427,7 +414,7 @@ block(layr) # struct COMP_KMXPLUS_LAYR { # TODO-LDML: lots of comment-out ahead. Need to revisit. -block(list) # struct COMP_KMXPLUS_LAYR_LIST { +block(list) # struct COMP_KMXPLUS_LIST { 6c 69 73 74 # KMX_DWORD header.ident; // 0000 Section name - list diff(list,endList) # KMX_DWORD header.size; // 0004 Section length 03 00 00 00 # KMX_DWORD listCount (should be 2) diff --git a/developer/src/kmc-ldml/test/fixtures/basic-19.txt b/developer/src/kmc-ldml/test/fixtures/basic-19.txt new file mode 100644 index 00000000000..75806e72934 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/basic-19.txt @@ -0,0 +1,683 @@ +# +# Keyman is copyright (C) SIL International. MIT License. +# +# basic-19.txt describes the expected output of running kmc against basic.xml +# with v19 schema and v19 target version. It is used in the end-to-end test +# test-compiler-e2e.ts. +# +# Differences from v17: +# * KMX: COMP_KEYBOARD.dwFileVersion +# * KMX+: `sec2` section vs `sect` section +# * KMX+: each section has an additional version header field +# * KMX+: `disp` section is v19 +# * KMX+: `layr` section is v19 +# +# See README.md for more information. +# + +block(kmxheader) # struct COMP_KEYBOARD { + 4b 58 54 53 # KMX_DWORD dwIdentifier; // 0000 Keyman compiled keyboard id + + 00 13 00 00 # KMX_DWORD dwFileVersion; // 0004 Version of the file - Keyman 4.0 is 0x0400 + + 00 00 00 00 # KMX_DWORD dwCheckSum; // 0008 deprecated in 16.0, always 0 + 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 + + sizeof(stores,12) # KMX_DWORD cxStoreArray; // 0018 in array entries + 00 00 00 00 # KMX_DWORD cxGroupArray; // 001C in array entries + + offset(stores) # KMX_DWORD dpStoreArray; // 0020 [LPSTORE] address of first item in store array + 00 00 00 00 # KMX_DWORD dpGroupArray; // 0024 [LPGROUP] address of first item in group array + + ff ff ff ff # KMX_DWORD StartGroup[2]; // 0028 index of starting groups [2 of them] + ff ff ff ff # + + 20 00 00 00 # KMX_DWORD dwFlags; // 0030 Flags for the keyboard file + + 00 00 00 00 # KMX_DWORD dwHotKey; // 0034 standard windows hotkey (hiword=shift/ctrl/alt stuff, loword=vkey) + + 00 00 00 00 # KMX_DWORD dpBitmapOffset; // 0038 offset of the bitmaps in the file + 00 00 00 00 # KMX_DWORD dwBitmapSize; // 003C size in bytes of the bitmaps + # }; + +block(kmxplusinfo) # struct COMP_KEYBOARD_KMXPLUSINFO { + offset(sec2) # KMX_DWORD dpKMXPlus; // 0040 offset of KMXPlus data from BOF, header is first + diff(sec2,eof) # KMX_DWORD dwKMXPlusSize; // 0044 size in bytes of entire KMXPlus data + # }; + +block(stores) # struct COMP_STORE { + 07 00 00 00 # KMX_DWORD dwSystemID; - TSS_NAME + offset(store_name_name) # KMX_DWORD dpName; + offset(store_name_string) # KMX_DWORD dpString; + # }; + # excluding this so we don’t have compiled version changes + # 14 00 00 00 # KMX_DWORD dwSystemID; - TSS_COMPILEDVERSION + # offset(store_compiledversion_name) # KMX_DWORD dpName; + # offset(store_compiledversion_string) # KMX_DWORD dpString; + # }; + 24 00 00 00 # KMX_DWORD dwSystemID; - TSS_KEYBOARDVERSION + offset(store_keyboardversion_name) # KMX_DWORD dpName; + offset(store_keyboardversion_string) # KMX_DWORD dpString; + # }; + 26 00 00 00 # KMX_DWORD dwSystemID; - TSS_TARGETS + offset(store_targets_name) # KMX_DWORD dpName; + offset(store_targets_string) # KMX_DWORD dpString; + # }; + + 18 00 00 00 # TSS_VISUALKEYBOARD + 00 00 00 00 # KMX_DWORD dpName; + offset(store_vk_path_string) # KMX_DWORD dpString; + +block(store_name_name) + 26 00 4e 00 41 00 4d 00 45 00 00 00 # '&NAME' +block(store_name_string) + 54 00 65 00 73 00 74 00 4b 00 62 00 64 00 00 00 # 'TestKbd' + +block(store_keyboardversion_name) + 26 00 4b 00 45 00 59 00 42 00 4f 00 41 00 52 00 + 44 00 56 00 45 00 52 00 53 00 49 00 4f 00 4e 00 + 00 00 # '&KEYBOARDVERSION' +block(store_keyboardversion_string) + 31 00 2e 00 30 00 2e 00 30 00 00 00 # '1.0.0' + +block(store_targets_name) + 26 00 54 00 41 00 52 00 47 00 45 00 54 00 53 00 00 00 # '&TARGETS' +block(store_targets_string) + 64 00 65 00 73 00 6b 00 74 00 6f 00 70 00 00 00 # 'desktop' +block(store_vk_path_string) + 62 00 61 00 73 00 69 00 63 00 2d 00 78 00 6d 00 6c 00 2e 00 6b 00 76 00 6b 00 00 00 # 'basic-xml.kvk' + +block(sec2) # struct COMP_KMXPLUS_SEC2 { + 73 65 63 32 # KMX_DWORD header.ident; // 0000 Section name 'sec2' + diff(sec2,endsec2) # KMX_DWORD header.size; // 0004 Section length + 00 13 00 00 # KMX_DWORD header.version; // 0008 Section version, 19 = 0x13 + diff(sec2,eof) # KMX_DWORD total; // 000C KMXPlus entire length + sizeof(sectitems,8) # KMX_DWORD count; // 0010 number of section headers + # }; + # Next sections are sec2 entries + # KMX_DWORD sec2; // 0010+ Section identity + # KMX_DWORD offset; // 0014+ Section offset relative to dpKMXPlus of section + +block(sectitems) + 62 6b 73 70 + diff(sec2,bksp) + + 64 69 73 70 + diff(sec2,disp) + + 65 6c 65 6d + diff(sec2,elem) + + 6b 65 79 73 + diff(sec2,keys) + + 6c 61 79 72 + diff(sec2,layr) + + 6c 69 73 74 + diff(sec2,list) + + 6c 6f 63 61 + diff(sec2,loca) + + 6d 65 74 61 + diff(sec2,meta) + + 73 74 72 73 + diff(sec2,strs) + + 74 72 61 6e + diff(sec2,tran) + + 75 73 65 74 + diff(sec2,uset) + + 76 61 72 73 + diff(sec2,vars) + +block(endsec2) + +# ---------------------------------------------------------------------------------------------------- +# bksp +# ---------------------------------------------------------------------------------------------------- + +block(bksp) + # TODO-LDML: see 'tran' for a more programmatic way to calculate the fields + 62 6b 73 70 + sizeof(bksp) + 00 11 00 00 # KMX_DWORD header.version; // 0008 Section version, 17 = 0x11 + 01 00 00 00 # KMX_DWORD groupCount + 01 00 00 00 # KMX_DWORD transformCount + 00 00 00 00 # KMX_DWORD reorderCount + + # group 0 + 00 00 00 00 # KMX_DWORD type = transform + 01 00 00 00 # KMX_DWORD count = 1 + 00 00 00 00 # KMX_DWORD index = 0 + + # transforms 0 + index(strNull,strElemTranFrom1b,2) # KMX_DWORD str from ^e ## << ?? + index(strNull,strNull,2) # KMX_DWORD str to 0 + index(strNull,strNull,2) # KMX_DWORD str mapFrom 0 + index(strNull,strNull,2) # KMX_DWORD str mapTo 0 + + +# ---------------------------------------------------------------------------------------------------- +# disp (v19) +# ---------------------------------------------------------------------------------------------------- + +block(disp) # struct COMP_KMXPLUS_DISP { + 64 69 73 70 # KMX_DWORD header.ident; // 0000 Section name - disp + sizeof(disp) # KMX_DWORD header.size; // 0004 Section length + 00 13 00 00 # KMX_DWORD header.version; // 0008 Section version, 19 = 0x13 + 02 00 00 00 # KMX_DWORD count; // 000C number of entries + index(strNull,strElemBkspFrom2,2) # KMX_DWORD baseCharacter // 0010 baseCharacter = 'e' + # }; + + # entry 0 + index(strNull,strA,2) # KMX_DWORD toId // 0000 baseCharacter = 'a' + index(strNull,strElemTranFrom1,2) # KMX_DWORD display // 0004 display = '^' + 00 00 00 00 # KMX_DWORD flags // 0008 isId=0 + + # entry 1 + index(strNull,strElemBkspFrom2,2) # KMX_DWORD toId // 0000 id = 'e' + index(strNull,strElemTranFrom1b,2) # KMX_DWORD display // 0004 '^e' + 01 00 00 00 # KMX_DWORD flags // 0008 isId=1 + + + +# ---------------------------------------------------------------------------------------------------- +# elem +# ---------------------------------------------------------------------------------------------------- + +block(elem) # struct COMP_KMXPLUS_ELEM { + 65 6c 65 6d # KMX_DWORD header.ident; // 0000 Section name - elem + diff(elem,endelem) # KMX_DWORD header.size; // 0004 Section length + 00 11 00 00 # KMX_DWORD header.version; // 0008 Section version, 17 = 0x11 + index(elemNull,endelem) # KMX_DWORD count; // 000C number of entries + # }; + +#strings + #elem #0 null element + 00 00 00 00 # KMX_DWORD offset; // 0010+ offset from this blob + sizeof(elemNull,8) # KMX_DWORD length; // 0014+ str length (ELEMENT units) + + #elem #1 count=03 + #elem #2 count=03 + #elem #3 count=01 + + + diff(elem,elemSet) # KMX_DWORD offset; // 0010+ offset from this blob + sizeof(elemSet,8) # KMX_DWORD length; // 0014+ str length (ELEMENT units) + + diff(elem,elemOrdrFrom) # KMX_DWORD offset; // 0010+ offset from this blob + sizeof(elemOrdrFrom,8) # KMX_DWORD length; // 0014+ str length (ELEMENT units) + + diff(elem,elemOrdrBefore) # KMX_DWORD offset; // 0010+ offset from this blob + sizeof(elemOrdrBefore,8) # KMX_DWORD length; // 0014+ str length (ELEMENT units) + +# now the elements +block(elemNull) + +block(elemSet) + 61 00 00 00 # KMX_DWORD element; 'a' + 00 00 00 00 # KMX_DWORD flags; + 62 00 00 00 # KMX_DWORD element; 'b' + 00 00 00 00 # KMX_DWORD flags; + 63 00 00 00 # KMX_DWORD element; 'c' + 00 00 00 00 # KMX_DWORD flags; + +block(elemOrdrFrom) + # from="\u{1A60}[\u1A75-\u1A79]\u{1A45}" order="10 55 10" + 60 1a 00 00 # KMX_DWORD element; '᩠' // str: output string or UTF-32LE codepoint + 00 00 0A 00 # KMX_DWORD flags; // flag and order values - cp + 00 00 00 00 # TODO-LDML: uset #0 # KMX_DWORD element; [uset] // str: output string or UTF-32LE codepoint + 02 00 37 00 # KMX_DWORD flags; // flag and order values - unicodeset + 45 1A 00 00 # KMX_DWORD element; 'ᩅ' // str: output string or UTF-32LE codepoint + 00 00 0A 00 # KMX_DWORD flags; // flag and order values - unicodeset + +block(elemOrdrBefore) + # before="\u{1A6B}" + 6b 1a 00 00 # KMX_DWORD element; 'ᩫ' // str: output string or UTF-32LE codepoint + 00 00 00 00 # KMX_DWORD flags; // flag and order values - unicodeset + +block(endelem) + +# ---------------------------------------------------------------------------------------------------- +# keys +# ---------------------------------------------------------------------------------------------------- + +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 + 00 11 00 00 # KMX_DWORD header.version; // 0008 Section version, 17 = 0x11 + 05 00 00 00 # KMX_DWORD keyCount + 01 00 00 00 # KMX_DWORD flicksCount + 00 00 00 00 # KMX_DWORD flickCount + 30 00 00 00 # KMX_DWORD kmapCount; // 0008 number of kmap - #48, one per key + # keys + # (#0000) a + 61 00 00 00 # 'a' + 00 00 00 00 # KMX_DWORD flags = 0 + index(strNull,strA,2) # KMXPLUS_STR 'a' (key id) + 00 00 00 00 # KMXPLUS_STR switch + 0A 00 00 00 # KMX_DWORD width*10 + 00 00 00 00 # TODO: index(listNull,listNull,4) # LIST longPress + 00 00 00 00 # STR longPressDefault + 00 00 00 00 # TODO: index(listNull,listNull,4) # LIST multiTap + 00 00 00 00 # flicks + # (#0001) e + 65 00 00 00 # 'e' + 00 00 00 00 # KMX_DWORD flags = 0 + index(strNull,strElemBkspFrom2,2) # KMXPLUS_STR 'e' (key id) + 00 00 00 00 # KMXPLUS_STR switch + 0A 00 00 00 # KMX_DWORD width*10 + 00 00 00 00 # TODO: index(listNull,listNull,4) # LIST longPress + 00 00 00 00 # STR longPressDefault + 00 00 00 00 # TODO: index(listNull,listNull,4) # LIST multiTap + 00 00 00 00 # flicks + # (#0002) gap (reserved) + 00 00 00 00 # nothing + 03 00 00 00 # KMX_DWORD flags = extend|gap + index(strNull,strGapReserved,2) # KMXPLUS_STR 'gap (reserved)' (key id) + 00 00 00 00 # KMXPLUS_STR switch + 0A 00 00 00 # KMX_DWORD width*10 (full width) + 00 00 00 00 # TODO: index(listNull,listNull,4) # LIST longPress + 00 00 00 00 # STR longPressDefault + 00 00 00 00 # TODO: index(listNull,listNull,4) # LIST multiTap + 00 00 00 00 # flicks + # (#0003) hmaqtugha + 27 01 00 00 # UTF-32 'U+0127' + 00 00 00 00 # KMX_DWORD (flags: none) + index(strNull,strHmaqtugha,2) # KMXPLUS_STR 'hmaqtugha' + 00 00 00 00 # KMXPLUS_STR switch + 0A 00 00 00 # KMX_DWORD width*10 + 02 00 00 00 # TODO: index(listNull,indexAe,4) # LIST longPress 'a e' + 00 00 00 00 # STR longPressDefault + 00 00 00 00 # TODO: index(listNull,listNull,4) # LIST multiTap + 00 00 00 00 # flicks 0 + # (#0004) that + index(strNull,strKeys,2) # KMXPLUS_STR '...' + 01 00 00 00 # KMX_DWORD flags = extend + index(strNull,strThat,2) # KMXPLUS_STR 'that' + 00 00 00 00 # KMXPLUS_STR switch + 0A 00 00 00 # KMX_DWORD width*10 + 00 00 00 00 # TODO: index(listNull,listNull,4) # LIST longPress + 00 00 00 00 # STR longPressDefault + 00 00 00 00 # TODO: index(listNull,listNull,4) # LIST multiTap + 00 00 00 00 # flicks 0 + # flicks + # flicks 0 - null + 00 00 00 00 # KMX_DWORD count + 00 00 00 00 # KMX_DWORD flick + 00 00 00 00 # KMX_STR id + # flick + # Right now there aren’t any flick elements. + #00 00 00 00 # LIST directions + #00 00 00 01 # flags + #00 00 00 00 # str: to + + # kmapdata: + # note that 'a' and 'e' are omitted, as they aren't on the layers (just from gestures) + + # moving these to 1-liners so they can be sorted! the structure is as follows: + ## 31 00 00 00 # KMX_DWORD vkey + ## 00 00 00 00 # KMX_DWORD modifiers (none) + ## 04 00 00 00 # KMX_DWORD key index (that) + +# following lines generated with this snippet: +# +# /** from scanCodes-implied.xml */ +# const scans = ("29 02 03 04 05 06 07 08 09 0A 0B 0C 0D" + " 10 11 12 13 14 15 16 17 18 19 1A 1B 2B" + " 1E 1F 20 21 22 23 24 25 26 27 28" + " 2C 2D 2E 2F 30 31 32 33 34 35").split(/ /); +# +# for (let s of scans) { +# const n = Number.parseInt(s, 16); +# const v = CLDRScanToUSVirtualKeyCodes[n]; // virtual-key-constants.ts +# if (!v) { +# console.error(`! ${s}`); +# } else { +# console.log(` ${Number(v).toString(16)} 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved)`); +# } +# } + +# - vkey - - modifier - - key id - + 20 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 30 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 31 00 00 00 00 00 00 00 04 00 00 00 # "that" + 32 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 33 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 34 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 35 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 36 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 37 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 38 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 39 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 41 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 42 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 43 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 44 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 45 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 46 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 47 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 48 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 49 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 4a 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 4b 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 4c 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 4d 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 4e 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 4f 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 50 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 51 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 52 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 53 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 54 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 55 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 56 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 57 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 58 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 59 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + 5a 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + ba 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + bb 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + bc 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + bd 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + be 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + bf 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + c0 00 00 00 00 00 00 00 03 00 00 00 # "hmaqtugha" + db 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + dc 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + dd 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + de 00 00 00 00 00 00 00 02 00 00 00 # gap (reserved) + +# ---------------------------------------------------------------------------------------------------- +# layr +# ---------------------------------------------------------------------------------------------------- + +block(layr) # struct COMP_KMXPLUS_LAYR { + 6c 61 79 72 # KMX_DWORD header.ident; // 0000 Section name - layr + sizeof(layr) # KMX_DWORD header.size; // 0004 Section length + 00 13 00 00 # KMX_DWORD header.version; // 0008 Section version, 19 = 0x13 + 01 00 00 00 # KMX_DWORD formCount + 01 00 00 00 # KMX_DWORD layerCount + 01 00 00 00 # KMX_DWORD rowCount + 02 00 00 00 # KMX_DWORD keyCount + # form 0 + index(strNull,strUs,2) # KMXPLUS_STR hardware = 'us' + 00 00 00 00 # KMX_DWORD layer; + 01 00 00 00 # KMX_DWORD count + 7B 00 00 00 # KMX_DWORD minDeviceWidth; // 123 + 00 00 00 00 # KMXPLUS_STR baseLayout v19 + 00 00 00 00 # KMXPLUS_STR fontFaceName v19 + 64 00 00 00 # KMX_DWORD fontSizePct v19 // 100% + 00 00 00 00 # KMX_DWORD flags v19 + + # layers 0 + 00 00 00 00 # KMXPLUS_STR id; + 00 00 00 00 # KMX_DWORD mod + 00 00 00 00 # KMX_DWORD row index + 01 00 00 00 # KMX_DWORD count + # rows 0 + 00 00 00 00 # KMX_DWORD key index + 02 00 00 00 # KMX_DWORD count + # keys + index(strNull,strHmaqtugha,2) # KMXPLUS_STR locale; // 'hmaqtugha' + index(strNull,strThat,2) # KMXPLUS_STR locale; // 'that' + +# ---------------------------------------------------------------------------------------------------- +# list +# ---------------------------------------------------------------------------------------------------- + +# TODO-LDML: lots of comment-out ahead. Need to revisit. + +block(list) # struct COMP_KMXPLUS_LIST { + 6c 69 73 74 # KMX_DWORD header.ident; // 0000 Section name - list + diff(list,endList) # KMX_DWORD header.size; // 0004 Section length + 00 11 00 00 # KMX_DWORD header.version; // 0008 Section version, 17 = 0x11 + 03 00 00 00 # KMX_DWORD listCount (should be 2) + 03 00 00 00 # KMX_DWORD indexCount (should be 2) + # list #0 the null list + block(listNull) + 00 00 00 00 #index(indexNull,indexNull,2) # KMX_DWORD list index (0) + 00 00 00 00 # KMX_DWORD lists[0].count + block(listA) + 00 00 00 00 # first index + 01 00 00 00 #count + block(listAe) + 01 00 00 00 # index(indexAe,indexNull,2) # KMX_DWORD list index (also 0) + 02 00 00 00 # KMX_DWORD count + block(endLists) + # indices + #block(indexNull) + # No null index + # index(strNull,strNull,2) # KMXPLUS_STR string index + block(indexA) + index(strNull,strA,2) # a + block(indexAe) + index(strNull,strA,2) # KMXPLUS_STR a + index(strNull,strElemBkspFrom2,2) # KMXPLUS_STR e + block(endIndices) + block(endList) + +# ---------------------------------------------------------------------------------------------------- +# loca +# ---------------------------------------------------------------------------------------------------- + +block(loca) # struct COMP_KMXPLUS_LOCA { + 6c 6f 63 61 # KMX_DWORD header.ident; // 0000 Section name - loca + sizeof(loca) # KMX_DWORD header.size; // 0004 Section length + 00 11 00 00 # KMX_DWORD header.version; // 0008 Section version, 17 = 0x11 + 01 00 00 00 # KMX_DWORD count; // 000C number of locales + index(strNull,strLocale,2) # KMXPLUS_STR locale; // 0010+ locale string entry = 'mt' + # }; +# ---------------------------------------------------------------------------------------------------- +# meta +# ---------------------------------------------------------------------------------------------------- + +block(meta) # struct COMP_KMXPLUS_META { + 6d 65 74 61 # KMX_DWORD header.ident; // 0000 Section name - meta + sizeof(meta) # KMX_DWORD header.size; // 0004 Section length + 00 11 00 00 # KMX_DWORD header.version; // 0008 Section version, 17 = 0x11 + index(strNull,strAuthor,2) # KMXPLUS_STR author; + index(strNull,strConformsTo,2) # KMXPLUS_STR conform; + index(strNull,strLayout,2) # KMXPLUS_STR layout; + index(strNull,strName,2) # KMXPLUS_STR name; + index(strNull,strIndicator,2) # KMXPLUS_STR indicator; + index(strNull,strVersion,2) # KMXPLUS_STR version; + 00 00 00 00 # KMX_DWORD settings; + # }; + +# ---------------------------------------------------------------------------------------------------- +# strs +# ---------------------------------------------------------------------------------------------------- + +block(strs) # struct COMP_KMXPLUS_STRS { + 73 74 72 73 # KMX_DWORD header.ident; // 0000 Section name - strs + diff(strs,endstrs) # KMX_DWORD header.size; // 0004 Section length + 00 11 00 00 # KMX_DWORD header.version; // 0008 Section version, 17 = 0x11 + index(strNull,endstrs,2) # KMX_DWORD count; // 000C count of str entries + # }; + + # Next sections are string entries + # KMX_DWORD offset; // 0010+ offset from this blob + # KMX_DWORD length; // 0014+ str length (UTF-16LE units) + + diff(strs,strNull) sizeof(strNull,2) + diff(strs,strVersion) sizeof(strVersion,2) + diff(strs,strConformsTo) sizeof(strConformsTo,2) + diff(strs,strName) sizeof(strName,2) + diff(strs,strFromSet) sizeof(strFromSet,2) + diff(strs,strUSet) sizeof(strUSet,2) + diff(strs,strAmarker) sizeof(strAmarker,2) + diff(strs,strSentinel0001r) sizeof(strSentinel0001r,2) + diff(strs,strElemTranFrom1) sizeof(strElemTranFrom1,2) + diff(strs,strElemTranFrom1a) sizeof(strElemTranFrom1a,2) + diff(strs,strElemTranFrom1b) sizeof(strElemTranFrom1b,2) + diff(strs,strA) sizeof(strA,2) + diff(strs,strSet) sizeof(strSet,2) + diff(strs,strSet2) sizeof(strSet2,2) + diff(strs,strTranTo) sizeof(strTranTo,2) + diff(strs,strElemBkspFrom2) sizeof(strElemBkspFrom2,2) + diff(strs,strGapReserved) sizeof(strGapReserved,2) + diff(strs,strHmaqtugha) sizeof(strHmaqtugha,2) + diff(strs,strLocale) sizeof(strLocale,2) + diff(strs,strLayout) sizeof(strLayout,2) + diff(strs,strAuthor) sizeof(strAuthor,2) + diff(strs,strThat) sizeof(strThat,2) + diff(strs,strUs) sizeof(strUs,2) + diff(strs,strVse) sizeof(strVse,2) + diff(strs,strVst) sizeof(strVst,2) + diff(strs,strVus) sizeof(strVus,2) + diff(strs,strKeys) sizeof(strKeys,2) + diff(strs,strIndicator) sizeof(strIndicator,2) + diff(strs,strSentinel0001) sizeof(strSentinel0001,2) + + # String table -- block(x) is used to store the null u16char at end of each string + # without interfering with sizeof() calculation above + +#str #00 + block(strNull) block(x) 00 00 # the zero-length string + block(strVersion) 31 00 2e 00 30 00 2e 00 30 00 block(x) 00 00 # '1.0.0' + block(strConformsTo) 34 00 35 00 block(x) 00 00 # '45' + block(strName) 54 00 65 00 73 00 74 00 4b 00 62 00 64 00 block(x) 00 00 # 'TestKbd' + block(strFromSet) 5B 00 5C 00 75 00 31 00 41 00 37 00 35 00 2D 00 5C 00 75 00 31 00 41 00 37 00 39 00 5D 00 block(x) 00 00 # [\u1a75-\u1a79] + block(strUSet) 5b 00 61 00 62 00 63 00 5d 00 block(x) 00 00 # '[abc]' + block(strAmarker) 5C 00 6D 00 7B 00 61 00 7D 00 block(x) 00 00 # '\m{a}' + block(strSentinel0001r) 5c 00 75 00 66 00 66 00 66 00 66 00 5c 00 75 00 30 00 30 00 30 00 38 00 5C 00 75 00 30 00 30 00 30 00 31 00 block(x) 00 00 # UC_SENTINEL CODE_DEADKEY \u0001 (regex form) + block(strElemTranFrom1) 5E 00 block(x) 00 00 # '^' + block(strElemTranFrom1a) 5E 00 61 00 block(x) 00 00 # '^a' + block(strElemTranFrom1b) 5E 00 65 00 block(x) 00 00 # '^e' + block(strA) 61 00 block(x) 00 00 # 'a' + block(strSet) 61 00 20 00 62 00 20 00 63 00 block(x) 00 00 # 'a b c' +#str #0A + block(strSet2) 61 00 62 00 63 00 block(x) 00 00 # 'abc' + block(strTranTo) 61 00 02 03 block(x) 00 00 # 'â' (U+0061 U+0302) + block(strElemBkspFrom2) 65 00 block(x) 00 00 # 'e' + block(strGapReserved) 67 00 61 00 70 00 20 00 28 00 72 00 65 00 73 00 65 00 72 00 76 00 65 00 64 00 29 00 block(x) 00 00 # 'gap (reserved)' + block(strHmaqtugha) 68 00 6d 00 61 00 71 00 74 00 75 00 67 00 68 00 61 00 block(x) 00 00 # 'hmaqtugha' +#str #10 + block(strLocale) 6d 00 74 00 block(x) 00 00 # 'mt' + block(strLayout) 71 00 77 00 65 00 72 00 74 00 79 00 block(x) 00 00 # 'qwerty' + block(strAuthor) 73 00 72 00 6c 00 32 00 39 00 35 00 block(x) 00 00 # 'srl295' + block(strThat) 74 00 68 00 61 00 74 00 block(x) 00 00 # 'that' + block(strUs) 75 00 73 00 block(x) 00 00 # 'us' (layout) + block(strVse) 76 00 73 00 65 00 block(x) 00 00 # 'vse' + block(strVst) 76 00 73 00 74 00 block(x) 00 00 # 'vst' + block(strVus) 76 00 75 00 73 00 block(x) 00 00 # 'vus' +#str #1a + block(strKeys) 90 17 b6 17 block(x) 00 00 # 'ថា' + # + block(strIndicator) 3d d8 40 de block(x) 00 00 # '🙀' + block(strSentinel0001) FF FF 08 00 01 00 block(x) 00 00 # UC_SENTINEL CODE_DEADKEY U+0001 + + +block(endstrs) # end of strs block + +# ---------------------------------------------------------------------------------------------------- +# tran +# ---------------------------------------------------------------------------------------------------- + +block(tran) # struct COMP_KMXPLUS_TRAN { + 74 72 61 6e # KMX_DWORD header.ident; // 0000 Section name - tran + diff(tran,tranEnd) # KMX_DWORD header.size; // 0004 Section length + 00 11 00 00 # KMX_DWORD header.version; // 0008 Section version, 17 = 0x11 + diff(tranGroupStart,tranTransformStart,12) # KMX_DWORD groupCount; + diff(tranTransformStart,tranReorderStart,16) # KMX_DWORD transformCount; + diff(tranReorderStart,tranEnd,8) # KMX_DWORD reorderCount; + + block(tranGroupStart) # COMP_KMXPLUS_TRAN_GROUP + # group 0 + 00 00 00 00 # KMX_DWORD type = transform + 02 00 00 00 # KMX_DWORD count + diff(tranTransformStart,tranTransform0,16) # KMX_DWORD index + + # group 1 + 00 00 00 00 # KMX_DWORD type = transform + 01 00 00 00 # KMX_DWORD count + diff(tranTransformStart,tranTransform2,16) # KMX_DWORD index + + # group 2 + 01 00 00 00 # KMX_DWORD type = reorder + 01 00 00 00 # KMX_DWORD count + diff(tranReorderStart,tranReorder0,8) # KMX_DWORD index + + # transforms + block(tranTransformStart) # COMP_KMXPLUS_TRAN_TRANSFORM + block(tranTransform0) + index(strNull,strElemTranFrom1a,2) # KMXPLUS_STR from; + index(strNull,strTranTo,2) # KMXPLUS_STR to; + index(strNull,strNull,2) # mapFrom + index(strNull,strNull,2) # mapTo + + block(tranTransform1) + index(strNull,strA,2) # KMXPLUS_STR from; 'a' + index(strNull,strSentinel0001,2) # KMXPLUS_STR to; \m{a} (plain form) + index(strNull,strNull,2) # mapFrom + index(strNull,strNull,2) # mapTo + + block(tranTransform2) # Next group + index(strNull,strSentinel0001r,2) # KMXPLUS_STR from; (\m{a}) (regex form) + index(strNull,strNull,2) # KMXPLUS_STR to; (none) + index(strNull,strNull,2) # mapFrom + index(strNull,strNull,2) # mapTo + + # reorders + block(tranReorderStart) # COMP_KMXPLUS_TRAN_REORDER + block(tranReorder0) + index(elemNull,elemOrdrFrom) # KMXPLUS_ELEM elements; + index(elemNull,elemOrdrBefore) # KMXPLUS_ELEM before; + +block(tranEnd) + +block(uset) + 75 73 65 74 # KMX_DWORD header.ident; // 0000 Section name - uset + diff(uset,usetEnd) # KMX_DWORD header.size; // 0004 Section length + 00 11 00 00 # KMX_DWORD header.version; // 0008 Section version, 17 = 0x11 + 01 00 00 00 # lists + 01 00 00 00 # elements + # lists + 00 00 00 00 # first list + 01 00 00 00 # size: 1 + index(strNull,strFromSet,2) # str + # ranges + # range @0 + 75 1A 00 00 # start + 79 1A 00 00 # end + block(usetEnd) + +block(vars) # struct COMP_KMXPLUS_VARS { + 76 61 72 73 # KMX_DWORD header.ident; // 0000 Section name - vars + diff(vars,varsEnd) # KMX_DWORD header.size; // 0004 Section length + 00 11 00 00 # KMX_DWORD header.version; // 0008 Section version, 17 = 0x11 + 01 00 00 00 # KMX_DWORD markers - list 1 ['a'] + diff(varsBegin,varsEnd,16) # KMX_DWORD varCount + + block(varsBegin) + # var 0 + 00 00 00 00 # KMX_DWORD type = str + index(strNull,strA,2) # KMXPLUS_STR id 'a' + index(strNull,strAmarker,2) # KMXPLUS_STR value '\m{a}' + 00 00 00 00 # KMXPLUS_ELEM + + # var 1 + 01 00 00 00 # KMX_DWORD type = set + index(strNull,strVse,2) # KMXPLUS_STR id 'vse' + index(strNull,strSet,2) # KMXPLUS_STR value 'a b c' + 01 00 00 00 # KMXPLUS_ELEM elem 'a b c' see 'elemSet' + + # var 2 + 00 00 00 00 # KMX_DWORD type = string + index(strNull,strVst,2) # KMXPLUS_STR id 'vst' + index(strNull,strSet2,2) # KMXPLUS_STR value 'abc' + 00 00 00 00 # KMXPLUS_ELEM elem + + # var 3 + 02 00 00 00 # KMX_DWORD type = string + index(strNull,strVus,2) # KMXPLUS_STR id 'vus' + index(strNull,strUSet,2) # KMXPLUS_STR value '[abc]' + 00 00 00 00 # KMXPLUS_ELEM elem +block(varsEnd) + +block(eof) # end of file diff --git a/developer/src/kmc-ldml/test/helpers/index.ts b/developer/src/kmc-ldml/test/helpers/index.ts index be6216f9c18..8b89a580034 100644 --- a/developer/src/kmc-ldml/test/helpers/index.ts +++ b/developer/src/kmc-ldml/test/helpers/index.ts @@ -22,6 +22,7 @@ import Section = KMXPlus.Section; import { ElemCompiler, ListCompiler, StrsCompiler } from '../../src/compiler/empty-compiler.js'; import { KmnCompiler } from '@keymanapp/kmc-kmn'; import { VarsCompiler } from '../../src/compiler/vars.js'; +import { KMXPlusVersion } from '@keymanapp/ldml-keyboard-constants'; /** * Builds a path to the fixture with the given path components. @@ -54,7 +55,7 @@ afterEach(function() { }); -export async function loadSectionFixture(compilerClass: SectionCompilerNew, filename: string, callbacks: TestCompilerCallbacks, dependencies?: SectionCompilerNew[], postValidateFail?: boolean): Promise
{ +export async function loadSectionFixture(compilerClass: SectionCompilerNew, filename: string, callbacks: TestCompilerCallbacks, targetVersion: KMXPlusVersion, dependencies?: SectionCompilerNew[], postValidateFail?: boolean): Promise
{ callbacks.messages = []; const inputFilename = makePathToFixture(filename); const data = callbacks.loadFile(inputFilename); @@ -74,7 +75,7 @@ export async function loadSectionFixture(compilerClass: SectionCompilerNew, file return null; // mimic kmc behavior - bail if validate fails } - const compiler = new compilerClass(source, callbacks); + const compiler = new compilerClass(source, callbacks, targetVersion); if(!compiler.validate()) { return null; @@ -111,7 +112,7 @@ async function loadDepsFor(sections: DependencySections, parentCompiler: Section dependencies = [ StrsCompiler, ListCompiler, ElemCompiler, VarsCompiler ]; } for (const dep of dependencies) { - const compiler = new dep(source, callbacks); + const compiler = new dep(source, callbacks, parentCompiler.targetVersion); assert.notEqual(compiler.id, parentId, `${parentId} depends on itself`); const didValidate = compiler.validate(); if (!callbacks.hasError()) { @@ -285,7 +286,7 @@ export function scrubContextFromMessages(messages: CompilerEvent[]): CompilerEve * @param compiler argument to loadSectionFixture() * @param callbacks argument to loadSectionFixture() */ -export function testCompilationCases(compiler: SectionCompilerNew, cases : CompilationCase[], dependencies?: (SectionCompilerNew)[]) { +export function testCompilationCases(compiler: SectionCompilerNew, cases : CompilationCase[], targetVersion: KMXPlusVersion, dependencies?: (SectionCompilerNew)[]) { // we need our own callbacks rather than using the global so messages don't get mixed const callbacks = new TestCompilerCallbacks(); for (const testcase of cases) { @@ -296,10 +297,10 @@ export function testCompilationCases(compiler: SectionCompilerNew, cases : Compi callbacks.clear(); // special case for an expected exception if (testcase.throws) { - assert.throws(async () => await loadSectionFixture(compiler, testcase.subpath, callbacks, testcase.dependencies || dependencies), testcase.throws, 'expected exception from compilation'); + assert.throws(async () => await loadSectionFixture(compiler, testcase.subpath, callbacks, targetVersion, testcase.dependencies || dependencies), testcase.throws, 'expected exception from compilation'); return; } - const section = await loadSectionFixture(compiler, testcase.subpath, callbacks, testcase.dependencies || dependencies); + const section = await loadSectionFixture(compiler, testcase.subpath, callbacks, targetVersion, testcase.dependencies || dependencies); let messagesToCheck = callbacks.messages; // scrub offsets from messages to reduce churn in the test casws if (!testcase.retainOffsetInMessages && callbacks.messages) { diff --git a/developer/src/kmc-ldml/test/keys.tests.ts b/developer/src/kmc-ldml/test/keys.tests.ts index 97171cf6fcf..a0e76085550 100644 --- a/developer/src/kmc-ldml/test/keys.tests.ts +++ b/developer/src/kmc-ldml/test/keys.tests.ts @@ -4,7 +4,7 @@ import { KeysCompiler } from '../src/compiler/keys.js'; import { assertCodePoints, compilerTestCallbacks, loadSectionFixture, testCompilationCases } from './helpers/index.js'; import { KMXPlus, Constants, LdmlKeyboardTypes } from '@keymanapp/common-types'; import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js'; -import { constants } from '@keymanapp/ldml-keyboard-constants'; +import { constants, KMXPlusVersion } from '@keymanapp/ldml-keyboard-constants'; import { MetaCompiler } from '../src/compiler/meta.js'; const keysDependencies = [ ...BASIC_DEPENDENCIES, MetaCompiler ]; import Keys = KMXPlus.Keys; @@ -49,7 +49,7 @@ describe('keys', function () { const [q] = keys.keys.filter(({ id }) => id.value === 'q'); assert.ok(q); - assert.isFalse(!!(q.flags & constants.keys_key_flags_gap)); + assert.isFalse(!!(q.flags & KMXPlus.KeysKeysFlags.gap)); assert.equal(q.width, 32, 'q\'s width'); // ceil(3.14159 * 10.0) assert.equal(q.flicks, 'flick0'); // note this is a string, not a StrsItem assert.equal(q.longPress.toString(), 'a-acute e-acute i-acute'); @@ -92,7 +92,7 @@ describe('keys', function () { const [q] = keys.keys.filter(({ id }) => id.value === 'q'); assert.ok(q); - assert.isFalse(!!(q.flags & constants.keys_key_flags_gap)); + assert.isFalse(!!(q.flags & KMXPlus.KeysKeysFlags.gap)); assert.equal(q.width, 32, 'q\'s width'); // ceil(3.14159 * 10.0) assert.equal(q.flicks, 'flick0'); // note this is a string, not a StrsItem assert.equal(q.longPress.toString(), 'a-acute e-acute i-acute'); @@ -134,7 +134,7 @@ describe('keys', function () { const [q] = keys.keys.filter(({ id }) => id.value === 'q'); assert.ok(q); - assert.isFalse(!!(q.flags & constants.keys_key_flags_gap)); + assert.isFalse(!!(q.flags & KMXPlus.KeysKeysFlags.gap)); assert.equal(q.width, 32); // ceil(3.1 * 10) assert.equal(q.flicks, 'flick0'); // note this is a string, not a StrsItem assert.equal(q.longPress.toString(), 'a-acute e-acute i-acute'); @@ -163,11 +163,11 @@ describe('keys', function () { const [Qgap] = keys.keys.filter(({ id }) => id.value === 'Q'); assert.ok(Qgap); - assert.isTrue(!!(Qgap.flags & constants.keys_key_flags_gap), 'Q’s gap='); + assert.isTrue(!!(Qgap.flags & KMXPlus.KeysKeysFlags.gap), 'Q’s gap='); const [Wshift] = keys.keys.filter(({ id }) => id.value === 'W'); assert.isNotNull(Wshift); - assert.isFalse(!!(Wshift.flags & constants.keys_key_flags_gap)); + assert.isFalse(!!(Wshift.flags & KMXPlus.KeysKeysFlags.gap)); assert.equal(Wshift.switch.value, 'shift'); }, }, @@ -213,14 +213,14 @@ describe('keys', function () { assert.equal(interrobang.to.value, `‽`, `Interrobang's value`); }, }, - ], keysDependencies); + ], KMXPlusVersion.Version17, keysDependencies); }); describe('keys.kmap', function () { this.slow(500); // 0.5 sec -- json schema validation takes a while it('should compile minimal kmap data', async function() { - const keys = await loadSectionFixture(KeysCompiler, 'sections/keys/minimal.xml', compilerTestCallbacks, keysDependencies) as Keys; + const keys = await loadSectionFixture(KeysCompiler, 'sections/keys/minimal.xml', compilerTestCallbacks, KMXPlusVersion.Version17, keysDependencies) as Keys; assert.isNotNull(keys); assert.equal(compilerTestCallbacks.messages.length, 0); // skip reserved (gap) keys @@ -443,10 +443,10 @@ describe('keys.kmap', function () { ], 'modifiers for c'); }, }, - ], keysDependencies); + ], KMXPlusVersion.Version17, keysDependencies); it('should reject layouts with too many hardware rows', async function() { - const keys = await loadSectionFixture(KeysCompiler, 'sections/keys/invalid-hardware-too-many-rows.xml', compilerTestCallbacks, keysDependencies) as Keys; + const keys = await loadSectionFixture(KeysCompiler, 'sections/keys/invalid-hardware-too-many-rows.xml', compilerTestCallbacks, KMXPlusVersion.Version17, keysDependencies) as Keys; assert.isNull(keys); assert.equal(compilerTestCallbacks.messages.length, 1); @@ -454,7 +454,7 @@ describe('keys.kmap', function () { }); it('should reject layouts with too many hardware keys', async function() { - const keys = await loadSectionFixture(KeysCompiler, 'sections/keys/invalid-hardware-too-many-keys.xml', compilerTestCallbacks, keysDependencies) as Keys; + const keys = await loadSectionFixture(KeysCompiler, 'sections/keys/invalid-hardware-too-many-keys.xml', compilerTestCallbacks, KMXPlusVersion.Version17, keysDependencies) as Keys; assert.isNull(keys); assert.equal(compilerTestCallbacks.messages.length, 1); @@ -462,14 +462,14 @@ describe('keys.kmap', function () { }); it('should reject layouts with undefined keys', async function() { - const keys = await loadSectionFixture(KeysCompiler, 'sections/keys/invalid-undefined-key.xml', compilerTestCallbacks, keysDependencies) as Keys; + const keys = await loadSectionFixture(KeysCompiler, 'sections/keys/invalid-undefined-key.xml', compilerTestCallbacks, KMXPlusVersion.Version17, keysDependencies) as Keys; assert.isNull(keys); assert.equal(compilerTestCallbacks.messages.length, 1); assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_KeyNotFoundInKeyBag({col: 1, form: 'hardware', keyId: 'foo', layer: 'base', row: 1}, withOffset(271) as LDMLKeyboard.LKRow)); }); it('should reject layouts with invalid keys', async function() { - const keys = await loadSectionFixture(KeysCompiler, 'sections/keys/invalid-key-missing-attrs.xml', compilerTestCallbacks, keysDependencies) as Keys; + const keys = await loadSectionFixture(KeysCompiler, 'sections/keys/invalid-key-missing-attrs.xml', compilerTestCallbacks, KMXPlusVersion.Version17, keysDependencies) as Keys; assert.isNull(keys); assert.equal(compilerTestCallbacks.messages.length, 1); assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_KeyMissingToGapOrSwitch( @@ -478,7 +478,7 @@ describe('keys.kmap', function () { )); }); it('should accept layouts with gap/switch keys', async function() { - const keys = await loadSectionFixture(KeysCompiler, 'sections/keys/gap-switch.xml', compilerTestCallbacks, keysDependencies) as Keys; + const keys = await loadSectionFixture(KeysCompiler, 'sections/keys/gap-switch.xml', compilerTestCallbacks, KMXPlusVersion.Version17, keysDependencies) as Keys; assert.isNotNull(keys); assert.equal(compilerTestCallbacks.messages.length, 0); assert.equal(keys.keys.length, 4 + KeysCompiler.reserved_count); diff --git a/developer/src/kmc-ldml/test/layr.tests.ts b/developer/src/kmc-ldml/test/layr.tests.ts index 64e4a3d373a..42766c44830 100644 --- a/developer/src/kmc-ldml/test/layr.tests.ts +++ b/developer/src/kmc-ldml/test/layr.tests.ts @@ -4,7 +4,7 @@ import { LayrCompiler } from '../src/compiler/layr.js'; import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js'; import { compilerTestCallbacks, testCompilationCases } from './helpers/index.js'; import { KMXPlus } from '@keymanapp/common-types'; -import { constants } from '@keymanapp/ldml-keyboard-constants'; +import { constants, KMXPlusVersion } from '@keymanapp/ldml-keyboard-constants'; import Layr = KMXPlus.Layr; import LayrRow = KMXPlus.LayrRow; @@ -21,6 +21,8 @@ function allKeysOk(row : LayrRow, str : string, msg? : string) { describe('layr', function () { this.slow(500); // 0.5 sec -- json schema validation takes a while + // TODO-EMBED-OSK-IN-KMX: add v19 tests + testCompilationCases(LayrCompiler, [ { subpath: 'sections/keys/minimal.xml', @@ -29,12 +31,12 @@ describe('layr', function () { assert.ok(layr); assert.equal(compilerTestCallbacks.messages.length, 0); - assert.equal(layr.lists?.length, 1); - const list0 = layr.lists[0]; - assert.ok(list0); - assert.equal(list0.layers.length, 1); - assert.equal(list0.hardware.value, 'us'); - const layer0 = list0.layers[0]; + assert.equal(layr.forms?.length, 1); + const form0 = layr.forms[0]; + assert.ok(form0); + assert.equal(form0.layers.length, 1); + assert.equal(form0.hardware.value, 'us'); + const layer0 = form0.layers[0]; assert.ok(layer0); assert.equal(layer0.rows.length, 1); const row0 = layer0.rows[0]; @@ -50,13 +52,13 @@ describe('layr', function () { subpath: 'sections/keys/maximal.xml', callback(sect) { const layr = sect; - assert.equal(layr.lists?.length, 2); + assert.equal(layr.forms?.length, 2); - const listHardware = layr.lists.find(v => v.hardware.value === 'iso'); - assert.ok(listHardware); - assert.equal(listHardware.minDeviceWidth, 0); - assert.equal(listHardware.layers.length, 2); - const hardware0 = listHardware.layers[0]; + const formHardware = layr.forms.find(v => v.hardware.value === 'iso'); + assert.ok(formHardware); + assert.equal(formHardware.minDeviceWidth, 0); + assert.equal(formHardware.layers.length, 2); + const hardware0 = formHardware.layers[0]; assert.ok(hardware0); assert.equal(hardware0.id.value, 'base'); assert.equal(hardware0.mod, constants.keys_mod_none); @@ -64,7 +66,7 @@ describe('layr', function () { assert.ok(hardware0row0); assert.equal(hardware0row0.keys.length, 2); allKeysOk(hardware0row0,'Q W', 'hardware0row0'); - const hardware1 = listHardware.layers[1]; + const hardware1 = formHardware.layers[1]; assert.ok(hardware1); assert.equal(hardware1.rows.length, 1); assert.equal(hardware1.id.value, 'shift'); @@ -74,11 +76,11 @@ describe('layr', function () { assert.equal(hardware1row0.keys.length, 3); allKeysOk(hardware1row0,'q w amarker', 'hardware1row0'); - const listTouch = layr.lists.find(v => v.hardware.value === constants.layr_list_hardware_touch); - assert.ok(listTouch); - assert.equal(listTouch.minDeviceWidth, 300); - assert.equal(listTouch.layers.length, 1); - const touch0 = listTouch.layers[0]; + const formTouch = layr.forms.find(v => v.hardware.value === constants.layr_form_hardware_touch); + assert.ok(formTouch); + assert.equal(formTouch.minDeviceWidth, 300); + assert.equal(formTouch.layers.length, 1); + const touch0 = formTouch.layers[0]; assert.ok(touch0); assert.equal(touch0.rows.length, 1); assert.equal(touch0.id.value, 'base'); @@ -123,8 +125,8 @@ describe('layr', function () { callback(sect) { const layr = sect; assert.ok(layr); - assert.equal(layr.lists.length, 1, 'layr.lists.length'); - const layers = layr.lists[0]; + assert.equal(layr.forms.length, 1, 'layr.forms.length'); + const layers = layr.forms[0]; const bymod = layers.layers.map(({id,mod,rows})=>([ id.value, mod, rows[0].keys[0].value, ])); @@ -149,12 +151,12 @@ describe('layr', function () { assert.ok(layr); assert.equal(compilerTestCallbacks.messages.length, 0); - assert.equal(layr.lists?.length, 1); - const list0 = layr.lists[0]; - assert.ok(list0); - assert.equal(list0.layers.length, 1); - assert.equal(list0.hardware.value, 'us'); - const layer0 = list0.layers[0]; + assert.equal(layr.forms?.length, 1); + const form0 = layr.forms[0]; + assert.ok(form0); + assert.equal(form0.layers.length, 1); + assert.equal(form0.hardware.value, 'us'); + const layer0 = form0.layers[0]; assert.ok(layer0); assert.equal(layer0.rows.length, 2); assert.equal(layer0.id.value, 'base'); @@ -180,5 +182,5 @@ describe('layr', function () { LdmlCompilerMessages.Error_InvalidLayerWidth({ minDeviceWidth }), ] })), - ]); + ], KMXPlusVersion.Version17); }); diff --git a/developer/src/kmc-ldml/test/loca.tests.ts b/developer/src/kmc-ldml/test/loca.tests.ts index ffe4f656c79..0d794992ac9 100644 --- a/developer/src/kmc-ldml/test/loca.tests.ts +++ b/developer/src/kmc-ldml/test/loca.tests.ts @@ -7,12 +7,13 @@ import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js' import Loca = KMXPlus.Loca; import { withOffset } from '@keymanapp/developer-utils'; +import { KMXPlusVersion } from '@keymanapp/ldml-keyboard-constants'; describe('loca', function () { this.slow(500); // 0.5 sec -- json schema validation takes a while it('should compile minimal loca data', async function() { - const loca = await loadSectionFixture(LocaCompiler, 'sections/loca/minimal.xml', compilerTestCallbacks) as Loca; + const loca = await loadSectionFixture(LocaCompiler, 'sections/loca/minimal.xml', compilerTestCallbacks, KMXPlusVersion.Version17) as Loca; assert.isObject(loca); assert.equal(compilerTestCallbacks.messages.length, 0); @@ -22,7 +23,7 @@ describe('loca', function () { }); it('should compile multiple locales', async function() { - const loca = await loadSectionFixture(LocaCompiler, 'sections/loca/multiple.xml', compilerTestCallbacks) as Loca; + const loca = await loadSectionFixture(LocaCompiler, 'sections/loca/multiple.xml', compilerTestCallbacks, KMXPlusVersion.Version17) as Loca; assert.isObject(loca); // Note: multiple.xml includes fr-FR twice, with differing case, which should be canonicalized @@ -42,7 +43,7 @@ describe('loca', function () { }); it('should reject structurally invalid locales', async function() { - const loca = await loadSectionFixture(LocaCompiler, 'sections/loca/invalid-locale.xml', compilerTestCallbacks) as Loca; + const loca = await loadSectionFixture(LocaCompiler, 'sections/loca/invalid-locale.xml', compilerTestCallbacks, KMXPlusVersion.Version17) as Loca; assert.isNull(loca); assert.equal(compilerTestCallbacks.messages.length, 1); // We'll only test one invalid BCP 47 tag to verify that we are properly calling BCP 47 validation routines. diff --git a/developer/src/kmc-ldml/test/messages.tests.ts b/developer/src/kmc-ldml/test/messages.tests.ts index 60df66fb095..1fad6b48666 100644 --- a/developer/src/kmc-ldml/test/messages.tests.ts +++ b/developer/src/kmc-ldml/test/messages.tests.ts @@ -20,8 +20,8 @@ describe('LdmlCompilerMessages', function () { const fakeOffsetNumber = 1234; const fakeOffsetObject = withOffset(fakeOffsetNumber); for(const key of keys) { - // exclude this one, does not need line numbers - if (key == 'Error_InvalidFile') continue; + // exclude these ones, do not need line numbers + if (key == 'Error_InvalidFile' || key == 'Error_InvalidTargetVersion') continue; if (typeof m[key] == 'function') { total++; const f = m[key] as Function; diff --git a/developer/src/kmc-ldml/test/meta.tests.ts b/developer/src/kmc-ldml/test/meta.tests.ts index beeed509154..d0f729ddd11 100644 --- a/developer/src/kmc-ldml/test/meta.tests.ts +++ b/developer/src/kmc-ldml/test/meta.tests.ts @@ -8,12 +8,13 @@ import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js' import KeyboardSettings = KMXPlus.KeyboardSettings; import Meta = KMXPlus.Meta; import { LDMLKeyboard, withOffset } from '@keymanapp/developer-utils'; +import { KMXPlusVersion } from '@keymanapp/ldml-keyboard-constants'; describe('meta', function () { this.slow(500); // 0.5 sec -- json schema validation takes a while it('should compile minimal metadata', async function() { - const meta = await loadSectionFixture(MetaCompiler, 'sections/meta/minimal.xml', compilerTestCallbacks) as Meta; + const meta = await loadSectionFixture(MetaCompiler, 'sections/meta/minimal.xml', compilerTestCallbacks, KMXPlusVersion.Version17) as Meta; assert.equal(compilerTestCallbacks.messages.length, 0); assert.isEmpty(meta.author.value); // TODO-LDML: default author string "unknown"? @@ -25,7 +26,7 @@ describe('meta', function () { }); it('should compile maximal metadata', async function() { - const meta = await loadSectionFixture(MetaCompiler, 'sections/meta/maximal.xml', compilerTestCallbacks) as Meta; + const meta = await loadSectionFixture(MetaCompiler, 'sections/meta/maximal.xml', compilerTestCallbacks, KMXPlusVersion.Version17) as Meta; assert.equal(compilerTestCallbacks.messages.length, 0); assert.equal(meta.author.value, 'The Keyman Team'); @@ -37,19 +38,19 @@ describe('meta', function () { }); it('should hint when normalization=disabled', async function() { - const meta = await loadSectionFixture(MetaCompiler, 'sections/meta/hint-normalization.xml', compilerTestCallbacks) as Meta; + const meta = await loadSectionFixture(MetaCompiler, 'sections/meta/hint-normalization.xml', compilerTestCallbacks, KMXPlusVersion.Version17) as Meta; assert.isNotNull(meta); assert.equal(compilerTestCallbacks.messages.length, 1); assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Hint_NormalizationDisabled(withOffset(177) as LDMLKeyboard.LKSettings)); }); it('should reject invalid version', async function() { - let meta = await loadSectionFixture(MetaCompiler, 'sections/meta/invalid-version-1.0.xml', compilerTestCallbacks) as Meta; + let meta = await loadSectionFixture(MetaCompiler, 'sections/meta/invalid-version-1.0.xml', compilerTestCallbacks, KMXPlusVersion.Version17) as Meta; assert.isNull(meta); assert.equal(compilerTestCallbacks.messages.length, 1); assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_InvalidVersion({version:'1.0'}, withOffset(40) as LDMLKeyboard.LKKeyboard)); - meta = await loadSectionFixture(MetaCompiler, 'sections/meta/invalid-version-v1.0.3.xml', compilerTestCallbacks) as Meta; + meta = await loadSectionFixture(MetaCompiler, 'sections/meta/invalid-version-v1.0.3.xml', compilerTestCallbacks, KMXPlusVersion.Version17) as Meta; assert.isNull(meta); assert.equal(compilerTestCallbacks.messages.length, 1); assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_InvalidVersion({version:'v1.0.3'}, withOffset(40) as LDMLKeyboard.LKKeyboard)); diff --git a/developer/src/kmc-ldml/test/tran.tests.ts b/developer/src/kmc-ldml/test/tran.tests.ts index 111b6962671..3df4c944b68 100644 --- a/developer/src/kmc-ldml/test/tran.tests.ts +++ b/developer/src/kmc-ldml/test/tran.tests.ts @@ -9,7 +9,7 @@ import { KMXPlus, LdmlKeyboardTypes } from '@keymanapp/common-types'; import Tran = KMXPlus.Tran;// for tests… import Bksp = KMXPlus.Bksp;// for tests… -import { constants } from '@keymanapp/ldml-keyboard-constants'; +import { constants, KMXPlusVersion } from '@keymanapp/ldml-keyboard-constants'; import { MetaCompiler } from '../src/compiler/meta.js'; const tranDependencies = [ ...BASIC_DEPENDENCIES, UsetCompiler, MetaCompiler ]; const bkspDependencies = tranDependencies; @@ -437,7 +437,7 @@ describe('tran', function () { }, } - ], tranDependencies); + ], KMXPlusVersion.Version17, tranDependencies); }); describe('bksp', function () { @@ -471,6 +471,6 @@ describe('bksp', function () { }), ], }, - ], bkspDependencies); + ], KMXPlusVersion.Version17, bkspDependencies); }); diff --git a/developer/src/kmc-ldml/test/vars.tests.ts b/developer/src/kmc-ldml/test/vars.tests.ts index bb0ee5d389e..5f078ddd391 100644 --- a/developer/src/kmc-ldml/test/vars.tests.ts +++ b/developer/src/kmc-ldml/test/vars.tests.ts @@ -6,7 +6,7 @@ import { KmnCompilerMessages } from '@keymanapp/kmc-kmn'; import { testCompilationCases } from './helpers/index.js'; import { KMXPlus, KMX } from '@keymanapp/common-types'; import { BASIC_DEPENDENCIES } from '../src/compiler/empty-compiler.js'; -import { constants } from '@keymanapp/ldml-keyboard-constants'; +import { constants, KMXPlusVersion } from '@keymanapp/ldml-keyboard-constants'; // now that 'everything' depends on vars, we need an explicit dependency here const varsDependencies = BASIC_DEPENDENCIES.filter(c => c !== VarsCompiler); @@ -209,7 +209,7 @@ describe('vars', function () { ], strictErrors: true }, -], varsDependencies); +], KMXPlusVersion.Version17, varsDependencies); describe('should match some marker constants', () => { // neither of these live here, but, common/web/types does not import ldml-keyboard-constants otherwise. @@ -242,6 +242,6 @@ describe('vars', function () { }), ], }, - ], varsDependencies); + ], KMXPlusVersion.Version17, varsDependencies); }); }); diff --git a/developer/src/kmc-package/src/compiler/kmp-compiler.ts b/developer/src/kmc-package/src/compiler/kmp-compiler.ts index 4999336ceef..9ac3656cf3f 100644 --- a/developer/src/kmc-package/src/compiler/kmp-compiler.ts +++ b/developer/src/kmc-package/src/compiler/kmp-compiler.ts @@ -609,6 +609,12 @@ export class KmpCompiler implements KeymanCompiler { } } + // TODO-EMBED-OSK-IN-KMX: rewrite .kmx+ font data in the same way: + // meta: we need to rewrite the .KMX: + // 1. load the kmx and kmx+ headers, verify the file has KMX+ data and is v19+, otherwise skip + // 2. seek to the sect section, verify it is v19+, otherwise skip + // 3. seek to strs section, iterate over all strings, looking for magic token "*OSK-FONT-MAGIC-TOKEN-OSK-FONT*" (32 chars) + // 4. if found, rewrite the string to the font facename, and update the string length header; zero out remaining bytes private setKvkFontData(kpsFilename: string, data: KmpJsonFile.KmpJsonFile, filename: string, kvk: Uint8Array) { // find the appropriate font to set const kvkId = this.callbacks.path.basename(filename.toLowerCase(), '.kvk'); diff --git a/developer/src/kmc/src/commands/build.ts b/developer/src/kmc/src/commands/build.ts index 20f1092a9fd..a23181354c3 100644 --- a/developer/src/kmc/src/commands/build.ts +++ b/developer/src/kmc/src/commands/build.ts @@ -27,6 +27,7 @@ export function declareBuild(program: Command) { .option('-W, --no-compiler-warnings-as-errors', 'Warnings do not fail the build; overrides project-level warnings-as-errors option') .option('-m, --message ', 'Adjust severity of info, hint or warning message to Disable (default), Info, Hint, Warn or Error (option can be repeated)', (value, previous) => previous.concat([value]), []) + .option('--target-version ', 'Target version of Keyman for compiled objects (default is minimum version that supports all features in the object)') .option('--no-compiler-version', 'Exclude compiler version metadata from output') .option('--no-warn-deprecated-code', 'Turn off warnings for deprecated code styles'); diff --git a/developer/src/kmc/src/messages/infrastructureMessages.ts b/developer/src/kmc/src/messages/infrastructureMessages.ts index d384aa7d866..373736fbd34 100644 --- a/developer/src/kmc/src/messages/infrastructureMessages.ts +++ b/developer/src/kmc/src/messages/infrastructureMessages.ts @@ -197,5 +197,10 @@ export class InfrastructureMessages { `Failed to generate new project '${def(o.id)}'.`, )}); + static ERROR_InvalidTargetVersion = SevError | 0x0029; + static Error_InvalidTargetVersion = (o:{targetVersion:string}) => m( + this.ERROR_InvalidTargetVersion, + `Target version parameter '${def(o.targetVersion)}' is not a valid Keyman version.`, + ); } diff --git a/developer/src/kmc/src/util/extendedCompilerOptions.ts b/developer/src/kmc/src/util/extendedCompilerOptions.ts index 53f68288f04..8dd3eed9c8a 100644 --- a/developer/src/kmc/src/util/extendedCompilerOptions.ts +++ b/developer/src/kmc/src/util/extendedCompilerOptions.ts @@ -1,6 +1,7 @@ import { CompilerBaseOptions, CompilerCallbacks, CompilerError, CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageOverride, CompilerMessageOverrideMap, CompilerOptions } from '@keymanapp/developer-utils'; import { InfrastructureMessages } from '../messages/infrastructureMessages.js'; import { CompilerMessageSource, messageNamespaceKeys, messageSources } from '../messages/messageNamespaces.js'; +import { KMX } from '@keymanapp/common-types'; export interface ExtendedCompilerOptions extends CompilerOptions { /** @@ -186,6 +187,12 @@ export function commanderOptionsToCompilerOptions(options: any, callbacks: Compi return null; } + const targetVersion = options.targetVersion ? KMX.versionStringToKmxVersion(options.targetVersion) : undefined; + if(targetVersion === null) { + callbacks.reportMessage(InfrastructureMessages.Error_InvalidTargetVersion({targetVersion: options.targetVersion})); + return null; + } + // We don't want to rename command line options to match the precise // properties that we have in CompilerOptions, but nor do we want to rename // CompilerOptions properties... @@ -196,6 +203,7 @@ export function commanderOptionsToCompilerOptions(options: any, callbacks: Compi saveDebug: options.debug, compilerWarningsAsErrors: options.compilerWarningsAsErrors, warnDeprecatedCode: options.warnDeprecatedCode, + targetVersion, // ExtendedOptions forPublishing: options.forPublishing, messageOverrides: overrides, diff --git a/developer/src/kmc/test/build.tests.ts b/developer/src/kmc/test/build.tests.ts index f8356db54b6..3828c1f2e3c 100644 --- a/developer/src/kmc/test/build.tests.ts +++ b/developer/src/kmc/test/build.tests.ts @@ -1,11 +1,17 @@ -import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; -import { clearOptions } from '../src/util/options.js'; -import { assert } from 'chai'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; import 'mocha'; -import { BuildProject } from '../src/commands/buildClasses/BuildProject.js'; +import { assert } from 'chai'; + +import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { makePathToFixture } from './helpers/index.js'; -const callbacks = new TestCompilerCallbacks(); +import { clearOptions } from '../src/util/options.js'; +import { BuildProject } from '../src/commands/buildClasses/BuildProject.js'; +import { BuildLdmlKeyboard } from '../src/commands/buildClasses/BuildLdmlKeyboard.js'; +import { KMX, KmxFileReader } from '@keymanapp/common-types'; +import { LdmlCompilerMessages } from '@keymanapp/kmc-ldml'; interface CompilerWarningsAsErrorsTruthTable { cli: boolean; @@ -13,42 +19,84 @@ interface CompilerWarningsAsErrorsTruthTable { result: boolean; }; -describe('compilerWarningsAsErrors', function () { - beforeEach(() => { +describe('build', function() { + const callbacks: TestCompilerCallbacks = new TestCompilerCallbacks(); + let outPath: string = null; + + this.beforeEach(function() { callbacks.clear(); clearOptions(); + outPath = fs.mkdtempSync(path.join(os.tmpdir(), 'kmc-')); + }); + + this.afterEach(function() { + if(this.currentTest.isFailed()) { + callbacks.printMessages(); + console.error(`Output kept at ${outPath}`); + } else { + if(outPath) fs.rmSync(outPath, {recursive: true, force: true}); + } + outPath = null; }); - // The CLI option should override the project setting - - const truthTable: CompilerWarningsAsErrorsTruthTable[] = [ - {cli:undefined, kpj:undefined, result:true}, // default setting (false) wins - {cli:undefined, kpj:false, result:true}, // kpj setting wins - {cli:undefined, kpj:true, result:false}, // kpj setting wins - {cli:false, kpj:undefined, result:true}, // cli setting wins - {cli:false, kpj:false, result:true}, // cli setting wins - {cli:false, kpj:true, result:true}, // cli setting wins - {cli:true, kpj:undefined, result:false}, // cli setting wins - {cli:true, kpj:false, result:false}, // cli setting wins - {cli:true, kpj:true, result:false}, // cli setting wins - ] - - for(const truth of truthTable) { - it(`should ${truth.result?'':'fail to '}build a project when kpj option=${truth.kpj} and cli option=${truth.cli}`, async function() { - const builder = new BuildProject(); - const path = makePathToFixture('compiler-warnings-as-errors', - `compiler_warnings_as_errors_${truth.kpj === true ? 'true' : (truth.kpj === false ? 'false' : 'undefined')}.kpj`); - const result = await builder.build(path, null, callbacks, { - compilerWarningsAsErrors: truth.cli, + describe('compilerWarningsAsErrors', function () { + + // The CLI option should override the project setting + + const truthTable: CompilerWarningsAsErrorsTruthTable[] = [ + {cli:undefined, kpj:undefined, result:true}, // default setting (false) wins + {cli:undefined, kpj:false, result:true}, // kpj setting wins + {cli:undefined, kpj:true, result:false}, // kpj setting wins + {cli:false, kpj:undefined, result:true}, // cli setting wins + {cli:false, kpj:false, result:true}, // cli setting wins + {cli:false, kpj:true, result:true}, // cli setting wins + {cli:true, kpj:undefined, result:false}, // cli setting wins + {cli:true, kpj:false, result:false}, // cli setting wins + {cli:true, kpj:true, result:false}, // cli setting wins + ] + + for(const truth of truthTable) { + it(`should ${truth.result?'':'fail to '}build a project when kpj option=${truth.kpj} and cli option=${truth.cli}`, async function() { + const builder = new BuildProject(); + const path = makePathToFixture('compiler-warnings-as-errors', + `compiler_warnings_as_errors_${truth.kpj === true ? 'true' : (truth.kpj === false ? 'false' : 'undefined')}.kpj`); + const result = await builder.build(path, null, callbacks, { + compilerWarningsAsErrors: truth.cli, + }); + if(truth.result) { + assert.isTrue(result); + } else { + assert.isFalse(result); + } }); - if(truth.result != result) { - callbacks.printMessages(); - } - if(truth.result) { + } + }); + + describe('targetVersion', function() { + [ + [17, KMX.KMXFile.VERSION_170], + [19, KMX.KMXFile.VERSION_190], + ].forEach(function(v) { + it(`should build a v${v[0]}.0 LDML keyboard`, async function() { + const builder = new BuildLdmlKeyboard(); + const infile = makePathToFixture('ldml', 'basic.xml'); + const outfile = path.join(outPath, 'basic.kmx'); + const result = await builder.build(infile, outfile, callbacks, { targetVersion: v[1] }); assert.isTrue(result); - } else { - assert.isFalse(result); - } + + const reader = new KmxFileReader(); + const keyboard = reader.read(callbacks.loadFile(outfile)); + assert.equal(keyboard.fileVersion, v[1]); + }); }); - } -}); + + it(`should error if an unsupported version is passed when compiling an LDML keyboard`, async function() { + const builder = new BuildLdmlKeyboard(); + const infile = makePathToFixture('ldml', 'basic.xml'); + const outfile = path.join(outPath, 'basic.kmx'); + const result = await builder.build(infile, outfile, callbacks, { targetVersion: KMX.KMX_Version.VERSION_70 }); + assert.isFalse(result); + assert.isTrue(callbacks.hasMessage(LdmlCompilerMessages.ERROR_InvalidTargetVersion)); + }); + }); +}); \ No newline at end of file diff --git a/developer/src/kmc/test/fixtures/ldml/basic.xml b/developer/src/kmc/test/fixtures/ldml/basic.xml new file mode 100644 index 00000000000..168ee0ef9a0 --- /dev/null +++ b/developer/src/kmc/test/fixtures/ldml/basic.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmcmplib/include/kmcmplibapi.h b/developer/src/kmcmplib/include/kmcmplibapi.h index 536d4fff0d4..a0087f3d4c7 100644 --- a/developer/src/kmcmplib/include/kmcmplibapi.h +++ b/developer/src/kmcmplib/include/kmcmplibapi.h @@ -31,6 +31,7 @@ struct KMCMP_COMPILER_OPTIONS { bool warnDeprecatedCode; bool shouldAddCompilerVersion; int target; // CKF_KEYMAN, CKF_KEYMANWEB + int targetVersion; // 0 = unspecified, otherwise Keyman version }; // @@ -71,9 +72,11 @@ struct KMCMP_COMPILER_RESULT_EXTRA { int targets; /// COMPILETARGETS__MASK = COMPILETARGETS_KMX | COMPILETARGETS_JS std::string kmnFilename; std::string kvksFilename; + std::string touchLayoutFilename; std::string displayMapFilename; std::vector stores; std::vector groups; + int targetVersion; }; struct KMCMP_COMPILER_RESULT { diff --git a/developer/src/kmcmplib/src/CompileKeyboardBuffer.cpp b/developer/src/kmcmplib/src/CompileKeyboardBuffer.cpp index 155b1252589..1714149c403 100644 --- a/developer/src/kmcmplib/src/CompileKeyboardBuffer.cpp +++ b/developer/src/kmcmplib/src/CompileKeyboardBuffer.cpp @@ -31,7 +31,7 @@ bool CompileKeyboardBuffer(KMX_BYTE* infile, int sz, PFILE_KEYBOARD fk) } fk->KeyboardID = 0; - fk->version = 0; + fk->version = kmcmp::TargetVersion; fk->dpStoreArray = NULL; fk->dpGroupArray = NULL; fk->cxStoreArray = 0; @@ -39,7 +39,9 @@ bool CompileKeyboardBuffer(KMX_BYTE* infile, int sz, PFILE_KEYBOARD fk) fk->StartGroup[0] = fk->StartGroup[1] = -1; fk->szName[0] = 0; fk->szCopyright[0] = 0; - fk->dwFlags = KF_AUTOMATICVERSION; + // If the targetVersion option is passed in, then we don't use automatic + // versioning, and we ignore the &VERSION store + fk->dwFlags = kmcmp::TargetVersion == 0 ? KF_AUTOMATICVERSION : 0; fk->currentGroup = 0xFFFFFFFF; fk->currentStore = 0; fk->cxDeadKeyArray = 0; @@ -51,6 +53,7 @@ bool CompileKeyboardBuffer(KMX_BYTE* infile, int sz, PFILE_KEYBOARD fk) fk->extra->displayMapFilename = ""; fk->extra->stores.clear(); fk->extra->groups.clear(); + fk->extra->targetVersion = VERSION_60; /* fk->szMessage[0] = 0; fk->szLanguageName[0] = 0;*/ fk->dwBitmapSize = 0; @@ -195,5 +198,7 @@ namespace kmcmp { extraGroup.name = string_from_u16string(group->szName); fk->extra->groups.push_back(extraGroup); } + + fk->extra->targetVersion = fk->version; } } diff --git a/developer/src/kmcmplib/src/Compiler.cpp b/developer/src/kmcmplib/src/Compiler.cpp index 099a5e69785..c278bbd2902 100644 --- a/developer/src/kmcmplib/src/Compiler.cpp +++ b/developer/src/kmcmplib/src/Compiler.cpp @@ -121,6 +121,7 @@ namespace kmcmp{ KMX_BOOL FOldCharPosMatching = FALSE; int CompileTarget; int BeginLine[4]; + int TargetVersion; KMX_BOOL IsValidCallStore(PFILE_STORE fs); void CheckStoreUsage(PFILE_KEYBOARD fk, int storeIndex, KMX_BOOL fIsStore, KMX_BOOL fIsOption, KMX_BOOL fIsCall); @@ -1132,6 +1133,12 @@ KMX_BOOL ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE s break; case TSS_VERSION: + if(kmcmp::TargetVersion != 0) { + // If the targetVersion option is passed in, then we don't use automatic + // versioning, and we ignore the &VERSION store + // TODO-EMBED-OSK-IN-KMX: consider a compiler hint? + return TRUE; + } if ((fk->dwFlags & KF_AUTOMATICVERSION) == 0) { ReportCompilerMessage(KmnCompilerMessages::ERROR_VersionAlreadyIncluded); return FALSE; @@ -1156,6 +1163,7 @@ KMX_BOOL ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE s else if (u16ncmp(p, u"15.0", 4) == 0) fk->version = VERSION_150; // Adds support for U_xxxx_yyyy #2858 else if (u16ncmp(p, u"16.0", 4) == 0) fk->version = VERSION_160; // KMXPlus else if (u16ncmp(p, u"17.0", 4) == 0) fk->version = VERSION_170; // Flicks and gestures + else if (u16ncmp(p, u"19.0", 4) == 0) fk->version = VERSION_190; // Embedded OSK else { ReportCompilerMessage(KmnCompilerMessages::ERROR_InvalidVersion); @@ -1200,6 +1208,7 @@ KMX_BOOL ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE s sp->dpString = q; } break; + case TSS_KMW_RTL: case TSS_KMW_HELPTEXT: if(!VerifyKeyboardVersion(fk, VERSION_70)) { @@ -1290,6 +1299,7 @@ KMX_BOOL ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE s return FALSE; } // Used by KMW compiler + fk->extra->touchLayoutFilename = string_from_u16string(sp->dpString); break; case TSS_KEYBOARDVERSION: // I4140 @@ -3355,6 +3365,12 @@ KMX_DWORD WriteCompiledKeyboard(PFILE_KEYBOARD fk, KMX_BYTE**data, size_t& dataS wcslen(fk->szMessage)*2 + 2 +*/ fk->dwBitmapSize; + if(fk->version >= VERSION_190) { + // For version 19+, we always reserve space for the KMX+ + // header, but we'll write it out as null in kmcmplib + size += sizeof(COMP_KEYBOARD_KMXPLUSINFO); + } + for (i = 0, fgp = fk->dpGroupArray; i < fk->cxGroupArray; i++, fgp++) { if (kmcmp::FSaveDebug) size += u16len(fgp->szName) * 2 + 2; @@ -3396,21 +3412,13 @@ KMX_DWORD WriteCompiledKeyboard(PFILE_KEYBOARD fk, KMX_BYTE**data, size_t& dataS offset = sizeof(COMP_KEYBOARD); - /*ck->dpLanguageName = offset; - wcscpy((PWSTR)(buf + offset), fk->szLanguageName); - offset += wcslen(fk->szLanguageName)*2 + 2; - - ck->dpName = offset; - wcscpy((PWSTR)(buf + offset), fk->szName); - offset += wcslen(fk->szName)*2 + 2; - - ck->dpCopyright = offset; - wcscpy((PWSTR)(buf + offset), fk->szCopyright); - offset += wcslen(fk->szCopyright)*2 + 2; - - ck->dpMessage = offset; - wcscpy((PWSTR)(buf + offset), fk->szMessage); - offset += wcslen(fk->szMessage)*2 + 2;*/ + if(fk->version >= VERSION_190) { + // Reserved space for KMX+ data + COMP_KEYBOARD_KMXPLUSINFO *kmxPlusInfo = reinterpret_cast(buf + offset); + kmxPlusInfo->dpKMXPlus = 0; + kmxPlusInfo->dwKMXPlusSize = 0; + offset += sizeof(COMP_KEYBOARD_KMXPLUSINFO); + } ck->dpStoreArray = (KMX_DWORD)offset; sp = (PCOMP_STORE)(buf + offset); diff --git a/developer/src/kmcmplib/src/CompilerInterfaces.cpp b/developer/src/kmcmplib/src/CompilerInterfaces.cpp index 0e424c93b00..b70e9a33b47 100644 --- a/developer/src/kmcmplib/src/CompilerInterfaces.cpp +++ b/developer/src/kmcmplib/src/CompilerInterfaces.cpp @@ -22,6 +22,7 @@ EXTERN bool kmcmp_CompileKeyboard( AWarnDeprecatedCode_GLOBAL_LIB = options.warnDeprecatedCode; kmcmp::FShouldAddCompilerVersion = options.shouldAddCompilerVersion; kmcmp::CompileTarget = options.target; + kmcmp::TargetVersion = options.targetVersion; if (!messageProc || !loadFileProc || !pszInfile) { ReportCompilerMessage(KmnCompilerMessages::FATAL_BadCallParams); diff --git a/developer/src/kmcmplib/src/CompilerInterfacesWasm.cpp b/developer/src/kmcmplib/src/CompilerInterfacesWasm.cpp index 1ddd45dd3f8..9414b94c495 100644 --- a/developer/src/kmcmplib/src/CompilerInterfacesWasm.cpp +++ b/developer/src/kmcmplib/src/CompilerInterfacesWasm.cpp @@ -124,6 +124,7 @@ EMSCRIPTEN_BINDINGS(compiler_interface) { .property("warnDeprecatedCode", &KMCMP_COMPILER_OPTIONS::warnDeprecatedCode) .property("shouldAddCompilerVersion", &KMCMP_COMPILER_OPTIONS::shouldAddCompilerVersion) .property("target", &KMCMP_COMPILER_OPTIONS::target) + .property("targetVersion", &KMCMP_COMPILER_OPTIONS::targetVersion) ; emscripten::class_("CompilerResult") diff --git a/developer/src/kmcmplib/src/kmcmplib.h b/developer/src/kmcmplib/src/kmcmplib.h index 91edfe1b260..a485a0652c5 100644 --- a/developer/src/kmcmplib/src/kmcmplib.h +++ b/developer/src/kmcmplib/src/kmcmplib.h @@ -16,6 +16,7 @@ namespace kmcmp { extern int CompileTarget; extern int BeginLine[4]; extern int currentLine; + extern int TargetVersion; extern NamedCodeConstants *CodeConstants; extern kmcmp_LoadFileProc loadfileproc; diff --git a/developer/src/kmcmplib/tests/api.tests.cpp b/developer/src/kmcmplib/tests/api.tests.cpp index ea44e3fade0..2809572570c 100644 --- a/developer/src/kmcmplib/tests/api.tests.cpp +++ b/developer/src/kmcmplib/tests/api.tests.cpp @@ -64,6 +64,7 @@ void test_kmcmp_CompileKeyboard(char *kmn_file) { options.warnDeprecatedCode = true; options.shouldAddCompilerVersion = false; options.target = CKF_KEYMAN; + options.targetVersion = 0; test_assert(!kmcmp_CompileKeyboard(kmn_file, options, msgproc, loadfileProc, nullptr, result)); test_assert(error_vec.size() == 1); test_assert(error_vec[0] == KmnCompilerMessages::ERROR_InfileNotExist); // zero byte no longer gives us KmnCompilerMessages::ERROR_CannotReadInfile diff --git a/developer/src/kmcmplib/tests/gtest-compiler.tests.cpp b/developer/src/kmcmplib/tests/gtest-compiler.tests.cpp index 7e65bfdd553..c525d7ce146 100644 --- a/developer/src/kmcmplib/tests/gtest-compiler.tests.cpp +++ b/developer/src/kmcmplib/tests/gtest-compiler.tests.cpp @@ -34,6 +34,7 @@ namespace kmcmp { extern std::string messageFilename; extern int BeginLine[4]; extern int CompileTarget; + extern int TargetVersion; extern KMX_BOOL FMnemonicLayout; } @@ -57,6 +58,7 @@ class CompilerTest : public testing::Test { kmcmp::nErrors = 0; kmcmp::currentLine = 0; kmcmp::ErrChr = 0; + kmcmp::TargetVersion = 0; kmcmp::messageFilename = ""; kmcmp::BeginLine[BEGIN_ANSI] = -1; kmcmp::BeginLine[BEGIN_UNICODE] = -1; diff --git a/developer/src/kmcmplib/tests/kmcompx.tests.cpp b/developer/src/kmcmplib/tests/kmcompx.tests.cpp index 827199967e6..f562c329653 100644 --- a/developer/src/kmcmplib/tests/kmcompx.tests.cpp +++ b/developer/src/kmcmplib/tests/kmcompx.tests.cpp @@ -57,6 +57,7 @@ int main(int argc, char *argv[]) options.warnDeprecatedCode = true; options.shouldAddCompilerVersion = false; options.target = CKF_KEYMAN; + options.targetVersion = 0; if(kmcmp_CompileKeyboard(kmn_file, options, msgproc, loadfileProc, nullptr, result)) { char* testname = strrchr( (char*) kmn_file, '/') + 1; diff --git a/developer/src/kmdecomp/kmdecomp.cpp b/developer/src/kmdecomp/kmdecomp.cpp index c6a085fe2e0..32e0502e44d 100644 --- a/developer/src/kmdecomp/kmdecomp.cpp +++ b/developer/src/kmdecomp/kmdecomp.cpp @@ -23,7 +23,7 @@ #include "../../../common/windows/cpp/include/keymansentry.h" #include "../../../common/windows/cpp/include/keymanversion.h" #include "../../../common/windows/cpp/include/legacy_kmx_memory.h" -#include "../../../common/windows/cpp/include/legacy_kmx_file.h" +#include "../../../common/include/kmx_file.h" BOOL LoadKeyboard(LPSTR fileName, LPKEYBOARD *lpKeyboard, LPBYTE *lpBitmap, DWORD *cbBitmap); diff --git a/developer/src/kmdecomp/savekeyboard.cpp b/developer/src/kmdecomp/savekeyboard.cpp index 3d02c2943c0..68269ac0ba7 100644 --- a/developer/src/kmdecomp/savekeyboard.cpp +++ b/developer/src/kmdecomp/savekeyboard.cpp @@ -30,8 +30,7 @@ #define _SILENCE_ALL_CXX17_DEPRECATION_WARNINGS #include - -#include "../../../common/windows/cpp/include/legacy_kmx_file.h" +#include "../../../common/include/kmx_file.h" #include "../../../common/windows/cpp/include/legacy_kmx_memory.h" #include "../../../common/windows/cpp/include/vkeys.h" @@ -96,6 +95,7 @@ const PWCHAR StoreTokens[] = { // I4652 L"", // TSS_BEGIN_POSTKEYSTROKE SSN__PREFIX L"NEWLAYER", SSN__PREFIX L"OLDLAYER", + SSN__PREFIX L"DISPLAYMAP", NULL }; diff --git a/developer/src/tike/xml/layoutbuilder/constants.js b/developer/src/tike/xml/layoutbuilder/constants.js index 117020e5d51..7680725d402 100644 --- a/developer/src/tike/xml/layoutbuilder/constants.js +++ b/developer/src/tike/xml/layoutbuilder/constants.js @@ -552,7 +552,14 @@ $(function() { this.lookupKeyNames.sort(); // Defines the PUA code mapping for the various 'special' modifier/control/non-printing keys on keyboards. - // This is lifted directly from specialCharacters.ts and must be kept in sync. See also CompileKeymanWeb.pas: CSpecialText10 + // This is lifted directly from web/.../specialCharacters.ts and must be kept in sync. + // + // `specialCharacters` must be kept in sync with the same values in: + // * /web/src/engine/osk/src/specialCharacters.ts + // * /developer/src/kmc-kmn/src/kmw-compiler/constants.ts + // * /core/include/ldml/keyman_core_ldml.ts + // + // More information, and unit test, in /developer/src/kmc-kmn/tests/kmw/constants.tests.ts this.specialCharacters = { '*Shift*': 8, '*Enter*': 5, diff --git a/docs/build/windows.md b/docs/build/windows.md index edc756340ff..4f3b02c898b 100644 --- a/docs/build/windows.md +++ b/docs/build/windows.md @@ -184,8 +184,8 @@ In bash, run the following commands: cd /c/Projects/keyman git clone https://github.com/emscripten-core/emsdk cd emsdk -emsdk install 3.1.58 -emsdk activate 3.1.58 +./emsdk install 3.1.58 +./emsdk activate 3.1.58 cd upstream/emscripten npm install ``` @@ -360,23 +360,23 @@ git clone https://github.com/keymanapp/CEF4Delphi_Binary C:\Projects\keyman\CEF4 * Ant * Gradle * Maven -* JDK 11 (Temurin11) +* JDK 21 #### JDK 11 -Use Powershell + Chocolatey to install JDK 11: +Use Powershell + winget to install JDK: ```ps1 # Elevated PowerShell - -# for *much* faster download, hide progress bar (PowerShell/PowerShell#2138) -$ProgressPreference = 'SilentlyContinue' -choco install temurin11 +# For Keyman 17: +winget install Microsoft.OpenJDK.11 +# For Keyman 18+: +winget install Microsoft.OpenJDK.21 ``` -**Multiple versions of Java:** If you need to build Keyman for Android 16.0 or +**Multiple versions of Java:** If you need to build Keyman for Android 17.0 or older versions, you can set `JAVA_HOME_11` to the JDK 11 path and -`JAVA_HOME` to the JDK 8 path. This will build both versions correctly +`JAVA_HOME_21` to the JDK 21 path. This will build both versions correctly from command line. But note that you do need to update your `JAVA_HOME` env var to the associated version before opening Android Studio and loading any Android projects. `JAVA_HOME_11` is mostly used by CI. @@ -411,9 +411,5 @@ certificates for the build. * sentry-cli (optional) - Uploading symbols for Sentry-based error reporting - - bash: - ```bash - # bash - curl -sL https://sentry.io/get-cli/ | bash - ``` + - https://github.com/getsentry/sentry-cli/releases/tag/1.70.0 + (TODO: update to newer version) \ No newline at end of file diff --git a/docs/epics.md b/docs/epics.md new file mode 100644 index 00000000000..6a7468b05d3 --- /dev/null +++ b/docs/epics.md @@ -0,0 +1,16 @@ +# Epics + +This document lists epics in the Keyman project. + + Name | Lead | Target release | Spec +-----------------------|-----------|----------------|-------------------- + epic/embed-osk-in-kmx | @mcdurdin | 19.0 | [embed-osk-in-kmx] + + +Note: when establishing a new epic branch, add the details of the epic to this +file, as a clean way to start an epic without unreviewed code changes in the +base epic PR. + +--- + +[embed-osk-in-kmx]: https://docs.google.com/document/d/15EjtSH7NAsGrdapfB3E7SlS6CqI-2zPy3PNyfUiAv54/edit?tab=t.0 diff --git a/docs/file-formats/kmx-file-format.md b/docs/file-formats/kmx-file-format.md index e13575ae4a7..931dbadc3d2 100644 --- a/docs/file-formats/kmx-file-format.md +++ b/docs/file-formats/kmx-file-format.md @@ -79,6 +79,7 @@ the `KMX_Version` enum: | `0x00000F00` | `VERSION_150` | Keyman 15.0 | | `0x00001000` | `VERSION_160` | Keyman 16.0 | | `0x00001100` | `VERSION_170` | Keyman 17.0 | +| `0x00001300` | `VERSION_190` | Keyman 19.0 | ### `StartGroup` @@ -100,6 +101,7 @@ The following flags are defined for .kmx keyboards: | `0x00000008` | `KF_LOGICALLAYOUT` | Unused, should never be set | | `0x00000010` | `KF_AUTOMATICVERSION` | The compiler determined the minimum version of Keyman automatically (see [`&Version`]) | | `0x00000020` | `KF_KMXPLUS` | 16.0+: A `COMP_KEYBOARD_KMXPLUSINFO` structure is present immediately after `COMP_KEYBOARD` | +| `0x00000040` | `KF_KMXPLUSOSK` | 19.0+: The `COMP_KEYBOARD_KMXPLUSINFO` structure contains only OSK, and not a LDML keyboard | ### `dwHotKey`: keyboard hotkeys diff --git a/docs/file-formats/kmx-plus-file-format.md b/docs/file-formats/kmx-plus-file-format.md index 6c252de5247..72237425f20 100644 --- a/docs/file-formats/kmx-plus-file-format.md +++ b/docs/file-formats/kmx-plus-file-format.md @@ -9,7 +9,7 @@ This document discusses the binary format for KMXPlus, which contains keyboards converted from source LDML data. -Draft spec PR: +Specification: ## C7043.1 Principles @@ -120,7 +120,7 @@ Each element string is made up of elements with the following item structure: ### C7043.2.5 Removed: old-format `keys` -_this section has been merged into the new `keys.kmap`_ +_this section has been merged into the new `key2.kmap`_ ### C7043.2.6 `loca`—Locales @@ -150,7 +150,7 @@ locale IDs (starting at offset 16) are in sorted binary order. | 8 | 32 | author | str: Keyboard author | |12 | 32 | conform | str: CLDR 'conformsTo' version | |16 | 32 | layout | str: layout type | -|20 | 32 | name | str: keyboard nme | +|20 | 32 | name | str: keyboard name | |24 | 32 | indicator | str: indicator | |28 | 32 | version | str: keyboard version | |32 | 32 | settings | int: keyboard settings | @@ -294,32 +294,35 @@ Represents layers on the keyboard. |---|------|------------|------------------------------------------| | 0 | 32 | ident | `layr` | | 4 | 32 | size | int: Length of section | -| 8 | 32 | listCount | int: Number of layer lists | +| 8 | 32 | formCount | int: Number of layer forms | |12 | 32 | layerCount | int: number of layer entries | |16 | 32 | rowCount | int: number of row entries | |20 | 32 | keyCount | int: number of key entries | -|24 | var | lists | layer list sub-table | +|24 | var | forms | layer form sub-table | | - | var | layers | layers sub-table | | - | var | rows | rows sub-table | | - | var | keys | keys sub-table | -### `layr.lists` subtable +### `layr.forms` subtable -Each layer list corresponds to one `` element. -There are `listCount` total lists. +Each layer form corresponds to one `` element. +There are `formCount` total forms. + +Note: renamed from `lists` to `forms` in Keyman 19, to increase clarity +and differentiate from other uses of `list`. | ∆ | Bits | Name | Description | |---|------|------------------|--------------------------------------------| | 0+| 32 | hardware | str: name of hardware layout. | | 4+| 32 | layer | int: index to first layer element | -| 8+| 32 | count | int: number of layer elements in this list | +| 8+| 32 | count | int: number of layer elements in this form | |12+| 32 | minDeviceWidth | int: min device width in millimeters, or 0 | -- `hardware` is the name of a form, or the string `touch` +- `hardware` is the name of a form, or the string `"touch"`. See UTS #35 section 7 for details about these values. -Layer lists are sorted by `hardware` string, then minDeviceWidth ascending. +Layer forms are sorted by `hardware` string, then minDeviceWidth ascending. ### `layr.layers` subtable @@ -336,7 +339,7 @@ both with `id="abc"`, but one with key flags of 0x0000 and one with keyflags of | 8+| 32 | row | int: index into rows area (next section) | |12+| 32 | count | int: number of `rows` elements for this layer | -- for `mod` see `keys.key.mod` +- for `mod` see `key2.kmap.mod` ### `layr.rows` subtable @@ -400,17 +403,17 @@ on the `id` field. For each key: -| ∆ | Bits | Name | Description | -|---|------|---------------- |----------------------------------------------------------| -| 0+| 32 | to | str: output string OR UTF-32LE codepoint | -| 4+| 32 | flags | int: per-key flags | -| 8+| 32 | id | str: key id | -|12+| 32 | switch | str: layer id to switch to | -|16+| 32 | width | int: key width*10 (supports 0.1 as min width) | +| ∆ | Bits | Name | Description | +|---|------|---------------- |-----------------------------------------------------------------| +| 0+| 32 | to | str: output string OR UTF-32LE codepoint | +| 4+| 32 | flags | int: per-key flags | +| 8+| 32 | id | str: key id | +|12+| 32 | switch | str: layer id to switch to | +|16+| 32 | width | int: key width*10 (supports 0.1 as min width) | |20+| 32 | longPress | list: index into `list` section with longPress key id list or 0 | -|24+| 32 | longPressDefault | str: default longpress key id or 0 | +|24+| 32 | longPressDefault | str: default longpress key id or 0 | |28+| 32 | multiTap | list: index into `list` section with multiTap key id list or 0 | -|32+| 32 | flicks | int: index into `key2.flicks` subtable | +|32+| 32 | flicks | int: index into `key2.flicks` subtable | - `id`: The original string id from XML. This may be 0 to save space (i.e. omit the string id). @@ -491,7 +494,7 @@ For each key: | 0x0020 | `ctrl` | `K_CTRLFLAG` | Either Control | | 0x0040 | `alt` | `K_ALTFLAG` | Either Alt | | 0x0100 | `caps` | `CAPITALFLAG` | Caps lock | -| 0x10000 | `other` | n/a | Other (not used in conjunction with others) | +| 0x10000 | `other` | n/a | Other (not used in conjunction with others) | TODO-LDML: Note that conforming to other keyman values, left versus right shift cannot be distinguished. diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMBinaryFileFormat.h b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMBinaryFileFormat.h index 3951d5831d4..90dce7da5a0 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMBinaryFileFormat.h +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMBinaryFileFormat.h @@ -52,8 +52,9 @@ struct COMP_KEYBOARD { #define VERSION_150 0x00000F00 #define VERSION_160 0x00001000 #define VERSION_170 0x00001100 +#define VERSION_190 0x00001300 #define VERSION_MIN VERSION_50 -#define VERSION_MAX VERSION_170 +#define VERSION_MAX VERSION_190 struct COMP_STORE { DWORD dwSystemID; @@ -119,8 +120,10 @@ struct COMP_STORE { #define TSS_NEWLAYER 42 #define TSS_OLDLAYER 43 #define TSS__KEYMAN_150_MAX 43 +/* Keyman 17.0 system stores */ +#define TSS_DISPLAYMAP 44 -#define TSS__MAX 43 +#define TSS__MAX 44 /* KVK file format definitions */ diff --git a/web/src/engine/osk/src/specialCharacters.ts b/web/src/engine/osk/src/specialCharacters.ts index c13e63715e5..4efac09233b 100644 --- a/web/src/engine/osk/src/specialCharacters.ts +++ b/web/src/engine/osk/src/specialCharacters.ts @@ -1,5 +1,12 @@ // Defines the PUA code mapping for the various 'special' modifier/control/non-printing keys on keyboards. -// `specialCharacters` must be kept in sync with the same variable in constants.js. See also CompileKeymanWeb.pas: CSpecialText10 +// +// `specialCharacters` must be kept in sync with the same values in: +// * /developer/src/tike/xml/layoutBuilder/constants.js +// * /developer/src/kmc-kmn/src/kmw-compiler/constants.ts +// * /core/include/ldml/keyman_core_ldml.ts +// +// More information, and unit test, in /developer/src/kmc-kmn/tests/kmw/constants.tests.ts + const specialCharacters = { '*Shift*': 8, '*Enter*': 5, diff --git a/windows/src/engine/keyman32/keyman64.h b/windows/src/engine/keyman32/keyman64.h index e46dad17829..7299abc31d2 100644 --- a/windows/src/engine/keyman32/keyman64.h +++ b/windows/src/engine/keyman32/keyman64.h @@ -75,7 +75,7 @@ #include #include #include -#include "../../../../common/windows/cpp/include/legacy_kmx_file.h" +#include "../../../../common/include/kmx_file.h" #include "../../../../common/windows/cpp/include/registry.h" #include "../../../../common/windows/cpp/include/unicode.h" #include "../../../../common/windows/cpp/include/xstring.h" @@ -126,6 +126,12 @@ #define EXTRAINFO_FLAG_SERIALIZED_USER_KEY_EVENT 0x4B4D0000 +// Reserved for hotkey remapping, aligns with HK_ defined in kmx_file.h +#define HK_RALT_INVALID 0x00100000 +#define HK_RCTRL_INVALID 0x00200000 +#define HK_RSHIFT_INVALID 0x00400000 + + /***************************************************************************/ // wm_keyman diff --git a/windows/src/engine/keyman32/keymanengine.h b/windows/src/engine/keyman32/keymanengine.h index 2304bc5f382..91a5166b7ae 100644 --- a/windows/src/engine/keyman32/keymanengine.h +++ b/windows/src/engine/keyman32/keymanengine.h @@ -38,7 +38,7 @@ #include #include #include -#include "../../../../common/windows/cpp/include/legacy_kmx_file.h" +#include "../../../../common/include/kmx_file.h" #include #include // for imx integration #include // for intermediate context diff --git a/windows/src/engine/kmtip/kmkey.cpp b/windows/src/engine/kmtip/kmkey.cpp index 609c98e4314..14a50478995 100644 --- a/windows/src/engine/kmtip/kmkey.cpp +++ b/windows/src/engine/kmtip/kmkey.cpp @@ -37,7 +37,7 @@ #include "pch.h" #include "kmtip.h" #include "editsess.h" -#include "../../../../common/windows/cpp/include/legacy_kmx_file.h" +#include "../../../../common/include/kmx_file.h" #include "../../../../common/windows/cpp/include/registry.h" #include "..\keymanmc\keymanmc.h" diff --git a/windows/src/engine/mcompile/mc_kmxfile.h b/windows/src/engine/mcompile/mc_kmxfile.h deleted file mode 100644 index 557ba968c73..00000000000 --- a/windows/src/engine/mcompile/mc_kmxfile.h +++ /dev/null @@ -1,66 +0,0 @@ -#include - -#ifndef _KMXFILE_H -#define _KMXFILE_H - -typedef struct tagSTORE { - DWORD dwSystemID; - PWSTR dpName; - PWSTR dpString; -} STORE, *LPSTORE; - -typedef struct tagKEY { - WCHAR Key; - DWORD Line; - DWORD ShiftFlags; - PWSTR dpOutput; - PWSTR dpContext; -} KEY, *LPKEY; - - -typedef struct tagGROUP { - PWSTR dpName; - LPKEY dpKeyArray; // [LPKEY] address of first item in key array - PWSTR dpMatch; - PWSTR dpNoMatch; - DWORD cxKeyArray; // in array entries - BOOL fUsingKeys; // group(xx) [using keys] <-- specified or not -} GROUP, *LPGROUP; - - -typedef struct tagKEYBOARD { - DWORD dwIdentifier; // Keyman compiled keyboard id - - DWORD dwFileVersion; // Version of the file - Keyman 4.0 is 0x0400 - - DWORD dwCheckSum; // As stored in keyboard. DEPRECATED as of 16.0 - DWORD xxkbdlayout; // as stored in HKEY_LOCAL_MACHINE//system//currentcontrolset//control//keyboard layouts - DWORD IsRegistered; // layout id, from same registry key - DWORD version; // keyboard version - - DWORD cxStoreArray; // in array entries - DWORD cxGroupArray; // in array entries - - LPSTORE dpStoreArray; // [LPSTORE] address of first item in store array, from start of file - LPGROUP dpGroupArray; // [LPGROUP] address of first item in group array, from start of file - - DWORD StartGroup[2]; // index of starting groups [2 of them] - // Ansi=0, Unicode=1 - - DWORD dwFlags; // Flags for the keyboard file - - DWORD dwHotKey; // standard windows hotkey (hiword=shift/ctrl/alt stuff, loword=vkey) - - //PWSTR dpName; // offset of name - //PWSTR dpLanguageName; // offset of language name; - //PWSTR dpCopyright; // offset of copyright - //PWSTR dpMessage; // offset of message in Keyboard About box - - DWORD dpBitmapOffset; // 0038 offset of the bitmaps in the file - DWORD dwBitmapSize; // 003C size in bytes of the bitmaps - //HBITMAP hBitmap; // handle to the bitmap in the file; -} KEYBOARD, *LPKEYBOARD; - -BOOL LoadKeyboard(LPWSTR fileName, LPKEYBOARD *lpKeyboard); - -#endif diff --git a/windows/src/engine/mcompile/mcompile.h b/windows/src/engine/mcompile/mcompile.h index 68f1da807cc..be9c39a8ce7 100644 --- a/windows/src/engine/mcompile/mcompile.h +++ b/windows/src/engine/mcompile/mcompile.h @@ -1,20 +1,19 @@ /* Name: mcompile Copyright: Copyright (C) 2003-2017 SIL International. - Documentation: - Description: + Documentation: + Description: Create Date: 3 Aug 2014 Modified Date: 3 Aug 2014 Authors: mcdurdin - Related Files: - Dependencies: + Related Files: + Dependencies: - Bugs: - Todo: - Notes: + Bugs: + Todo: + Notes: History: 03 Aug 2014 - mcdurdin - I4353 - V9.0 - mnemonic layout recompiler mixes up deadkey rules - */ #include @@ -28,3 +27,5 @@ struct DeadkeyMapping { // I4353 }; extern std::vector FDeadkeys; // I4353 + +BOOL LoadKeyboard(LPWSTR fileName, LPKEYBOARD *lpKeyboard); diff --git a/windows/src/engine/mcompile/pch.h b/windows/src/engine/mcompile/pch.h index 2de972b9f97..eb7a71f7296 100644 --- a/windows/src/engine/mcompile/pch.h +++ b/windows/src/engine/mcompile/pch.h @@ -8,10 +8,10 @@ #include "targetver.h" #include +#include "../../../../common/windows/cpp/include/legacy_kmx_memory.h" #include "mcompile.h" -#include "mc_kmxfile.h" #include "mc_syskbd.h" #include "../../../../common/windows/cpp/include/crc32.h" -#include "../../../../common/windows/cpp/include/legacy_kmx_file.h" +#include "../../../../common/include/kmx_file.h" #include "../../../../common/windows/cpp/include/xstring.h" #include "../../../../common/windows/cpp/include/keymansentry.h"