From 3c120ebae7036bc7cafe933166bf44e3decd5bca Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Fri, 26 Aug 2022 08:51:15 +1000 Subject: [PATCH] feat(developer): LDML keyboard XML Loader and Compiler Implements basic LDML keyboard xml loader and compiler. --- .../kmldmlc/src/keyman/compiler/callbacks.ts | 4 + .../kmldmlc/src/keyman/compiler/compiler.ts | 138 +++++++++++++++++ .../ldml-keyboard/ldml-keyboard-xml-reader.ts | 47 ++++++ .../keyman/ldml-keyboard/ldml-keyboard-xml.ts | 63 ++++++++ .../keyman/ldml-keyboard/us-virtual-keys.ts | 139 ++++++++++++++++++ .../src/keyman/ldmlkeyboard/compiler.ts | 45 ------ developer/src/kmldmlc/src/kmldmlc.ts | 15 +- 7 files changed, 404 insertions(+), 47 deletions(-) create mode 100644 developer/src/kmldmlc/src/keyman/compiler/callbacks.ts create mode 100644 developer/src/kmldmlc/src/keyman/compiler/compiler.ts create mode 100644 developer/src/kmldmlc/src/keyman/ldml-keyboard/ldml-keyboard-xml-reader.ts create mode 100644 developer/src/kmldmlc/src/keyman/ldml-keyboard/ldml-keyboard-xml.ts create mode 100644 developer/src/kmldmlc/src/keyman/ldml-keyboard/us-virtual-keys.ts delete mode 100644 developer/src/kmldmlc/src/keyman/ldmlkeyboard/compiler.ts diff --git a/developer/src/kmldmlc/src/keyman/compiler/callbacks.ts b/developer/src/kmldmlc/src/keyman/compiler/callbacks.ts new file mode 100644 index 0000000000..7191da8c8a --- /dev/null +++ b/developer/src/kmldmlc/src/keyman/compiler/callbacks.ts @@ -0,0 +1,4 @@ +export default interface CompilerCallbacks { + loadFile(baseFilename: string, filename: string): Buffer; + reportMessage(severity: number, message: string): void; +}; diff --git a/developer/src/kmldmlc/src/keyman/compiler/compiler.ts b/developer/src/kmldmlc/src/keyman/compiler/compiler.ts new file mode 100644 index 0000000000..3c9002eceb --- /dev/null +++ b/developer/src/kmldmlc/src/keyman/compiler/compiler.ts @@ -0,0 +1,138 @@ +import KMXBuilder from '../kmx/kmx-builder'; +import KMXPlusFile from '../kmx/kmx-plus'; +import LDMLKeyboardXMLSourceFile, * as LDMLKeyboard from '../ldml-keyboard/ldml-keyboard-xml'; +import LDMLKeyboardXMLSourceFileReader from '../ldml-keyboard/ldml-keyboard-xml-reader'; +import { USVirtualKeyMap } from '../ldml-keyboard/us-virtual-keys'; +import CompilerCallbacks from './callbacks'; + +export default class Compiler { + private callbacks: CompilerCallbacks; + constructor (callbacks: CompilerCallbacks) { + this.callbacks = callbacks; + + //TEMP + this.callbacks.reportMessage(0, "Instantiated compiler successfully"); + } + + private translateLayerIdToModifier(id: string) { + if(id == 'base') { + return 0; + } + // TODO: other modifiers + return 0; + } + + private compileHardwareLayer( + keyboard: LDMLKeyboard.LKKeyboard, + layer: LDMLKeyboard.LKLayerMap, + kmx: KMXPlusFile + ) { + const mod = this.translateLayerIdToModifier(layer.id); + + let y = -1; + for(let row of layer.row) { + y++; + if(y > USVirtualKeyMap.length) { + this.callbacks.reportMessage(0, `'hardware' layer has too many rows`); + break; + } + + const keys = row.keys.split(' '); + let x = -1; + for(let key of keys) { + x++; + if(x > USVirtualKeyMap[y].length) { + this.callbacks.reportMessage(0, `Row #${y+1} on 'hardware' layer has too many keys`); + break; + } + + let keydef = keyboard.keys?.key?.find(x => x.id == key); + if(!keydef) { + this.callbacks.reportMessage(0, + `Key ${key} in position #${x+1} on row #${y+1} of layer ${layer.id}, form 'hardware' not found in key bag`); + continue; + } + + kmx.kmxplus.keys.keys.push({ + vkey: USVirtualKeyMap[y][x], + mod: mod, + to: keydef.to, + flags: 0 // Note: 'expand' is never set here, only by the .kmx builder + }); + } + } + } + + /** + * Loads a LDML Keyboard xml file and compiles into in-memory kmxplus + * structures. + * @param filename input filename, will use callback to load from disk + * @returns + */ + public load(filename: string): LDMLKeyboardXMLSourceFile { + let buf = this.callbacks.loadFile(filename, filename); + let reader = new LDMLKeyboardXMLSourceFileReader(this.callbacks); + let source = reader.load(buf); + if(!source) { + return null; + } + + // console.dir(source, {depth:15}); + + return source; + } + + /** + * Validates that the LDML keyboard source file and lints. If this returns true, + * it is safe to pass `source` to .compile. + * @param source + * @returns + */ + public validate(source: LDMLKeyboardXMLSourceFile): boolean { + /* + let validator: LDMLKeyboardValidator = new LDMLKeyboardValidator(this.callbacks); + if(!validator.validate(xml)) { + return null; + }*/ + return true; + } + + /** + * Transforms in-memory LDML keyboard xml file to an intermediate representation + * of a .kmx file. Input source variable should have already passed validation. + * @param source in-memory representation of LDML keyboard xml file + * @returns KMXPlusFile intermediate file + */ + public compile(source: LDMLKeyboardXMLSourceFile): KMXPlusFile { + let kmx = new KMXPlusFile(); + + // Transform source xml structures to kmxplus + + kmx.kmxplus.meta.name = source.keyboard.names.name[0].value; + kmx.kmxplus.meta.author = source.keyboard.info.author; + kmx.kmxplus.meta.conform = source.keyboard.conformsTo; + kmx.kmxplus.meta.layout = source.keyboard.info.layout; + kmx.kmxplus.meta.normalization = source.keyboard.info.normalization; + kmx.kmxplus.meta.indicator = source.keyboard.info.indicator; + + kmx.kmxplus.loca.locales.push(source.keyboard.locale); + + // Use LayerMap + keys to generate compiled keys for hardware + + if(source.keyboard.layerMaps[0].form == 'hardware') { + for(let layer of source.keyboard.layerMaps[0].layerMap) { + this.compileHardwareLayer(source.keyboard, layer, kmx); + } + } + + // TODO: generate vkey mapping for touch-only keys + + return kmx; + } + + public write(kmx: KMXPlusFile): Uint8Array { + // Use the builder to generate the binary output file + let builder = new KMXBuilder(kmx, true); + return builder.compile(); + } +} diff --git a/developer/src/kmldmlc/src/keyman/ldml-keyboard/ldml-keyboard-xml-reader.ts b/developer/src/kmldmlc/src/keyman/ldml-keyboard/ldml-keyboard-xml-reader.ts new file mode 100644 index 0000000000..931cb18df1 --- /dev/null +++ b/developer/src/kmldmlc/src/keyman/ldml-keyboard/ldml-keyboard-xml-reader.ts @@ -0,0 +1,47 @@ +import * as xml2js from 'xml2js'; +import LDMLKeyboardXMLSourceFile from './ldml-keyboard-xml'; +import CompilerCallbacks from '../compiler/callbacks'; + +export default class LDMLKeyboardXMLSourceFileReader { + + //private callbacks: CompilerCallbacks; + constructor (callbacks: CompilerCallbacks) { + //this.callbacks = callbacks; + } + + /** + * xml2js will not place single-entry objects into arrays. + * Easiest way to fix this is to box them ourselves as needed + * @param source any + */ + private boxArrays(source: any) { + let box = (o: any, x: string) => { + if(typeof o == 'object' && !Array.isArray(o[x])) o[x] = [o[x]]; + } + + box(source?.keyboard, 'layerMaps'); + box(source?.keyboard?.names, 'name'); + box(source?.keyboard?.keys, 'key'); + for(let layerMaps of source?.keyboard?.layerMaps) { + box(layerMaps, 'layerMap'); + for(let layerMap of layerMaps?.layerMap) { + box(layerMap, 'row'); + } + } + return source; + } + + public load(file: Uint8Array): LDMLKeyboardXMLSourceFile { + let source = (() => { + let a: LDMLKeyboardXMLSourceFile; + let parser = new xml2js.Parser({ + explicitArray: false, + mergeAttrs: true, + }); + parser.parseString(file, (e: unknown, r: unknown) => { a = r as LDMLKeyboardXMLSourceFile }); + return a; + })(); + + return this.boxArrays(source); + } +} \ No newline at end of file diff --git a/developer/src/kmldmlc/src/keyman/ldml-keyboard/ldml-keyboard-xml.ts b/developer/src/kmldmlc/src/keyman/ldml-keyboard/ldml-keyboard-xml.ts new file mode 100644 index 0000000000..501bedcb77 --- /dev/null +++ b/developer/src/kmldmlc/src/keyman/ldml-keyboard/ldml-keyboard-xml.ts @@ -0,0 +1,63 @@ +// +// Conforms to techpreview +// +// The interfaces in this file are designed with reference to the mapped +// structures produced by xml2js when passed a LDML keyboard .xml file. +// +// Using prefix LK for LDML Keyboard +// + +export default interface LDMLKeyboardXMLSourceFile { + /** + * -- the root element. + */ + keyboard: LKKeyboard; +} + +export interface LKKeyboard { + locale?: string; + conformsTo?: string; + + info?: LKInfo; + names?: LKNames; + keys?: LKKeys; + layerMaps?: LKLayerMaps[]; +}; + +export interface LKInfo { + author?: string; + indicator?: string; + layout?: string; + normalization?: string; +}; + +export interface LKNames { + name: LKName[]; +}; + +export interface LKName { + value?: string; +}; + +export interface LKKeys { + key: LKKey[]; +}; + +export interface LKKey { + id?: string; + to?: string; +}; + +export interface LKLayerMaps { + form?: string; + layerMap?: LKLayerMap[]; +}; + +export interface LKLayerMap { + id?: string; + row?: LKRow[]; +}; + +export interface LKRow { + keys?: string; +}; diff --git a/developer/src/kmldmlc/src/keyman/ldml-keyboard/us-virtual-keys.ts b/developer/src/kmldmlc/src/keyman/ldml-keyboard/us-virtual-keys.ts new file mode 100644 index 0000000000..6dd58d5914 --- /dev/null +++ b/developer/src/kmldmlc/src/keyman/ldml-keyboard/us-virtual-keys.ts @@ -0,0 +1,139 @@ + +// Define standard keycode numbers (exposed for use by other modules) +// TODO: merge with common\web\keyboard-processor\src\text\codes.ts +export const USVirtualKeyCodes = { + K_BKSP:8, + K_TAB:9, + K_ENTER:13, + K_SHIFT:16, + K_CONTROL:17, + K_ALT:18, + K_PAUSE:19, + K_CAPS:20, + K_ESC:27, + K_SPACE:32, + K_PGUP:33, + K_PGDN:34, + K_END:35, + K_HOME:36, + K_LEFT:37, + K_UP:38, + K_RIGHT:39, + K_DOWN:40, + K_SEL:41, + K_PRINT:42, + K_EXEC:43, + K_INS:45, + K_DEL:46, + K_HELP:47, + K_0:48, + K_1:49, + K_2:50, + K_3:51, + K_4:52, + K_5:53, + K_6:54, + K_7:55, + K_8:56, + K_9:57, + K_A:65, + K_B:66, + K_C:67, + K_D:68, + K_E:69, + K_F:70, + K_G:71, + K_H:72, + K_I:73, + K_J:74, + K_K:75, + K_L:76, + K_M:77, + K_N:78, + K_O:79, + K_P:80, + K_Q:81, + K_R:82, + K_S:83, + K_T:84, + K_U:85, + K_V:86, + K_W:87, + K_X:88, + K_Y:89, + K_Z:90, + K_NP0:96, + K_NP1:97, + K_NP2:98, + K_NP3:99, + K_NP4:100, + K_NP5:101, + K_NP6:102, + K_NP7:103, + K_NP8:104, + K_NP9:105, + K_NPSTAR:106, + K_NPPLUS:107, + K_SEPARATOR:108, + K_NPMINUS:109, + K_NPDOT:110, + K_NPSLASH:111, + K_F1:112, + K_F2:113, + K_F3:114, + K_F4:115, + K_F5:116, + K_F6:117, + K_F7:118, + K_F8:119, + K_F9:120, + K_F10:121, + K_F11:122, + K_F12:123, + K_NUMLOCK:144, + K_SCROLL:145, + K_LSHIFT:160, + K_RSHIFT:161, + K_LCONTROL:162, + K_RCONTROL:163, + K_LALT:164, + K_RALT:165, + K_COLON:186, + K_EQUAL:187, + K_COMMA:188, + K_HYPHEN:189, + K_PERIOD:190, + K_SLASH:191, + K_BKQUOTE:192, + K_LBRKT:219, + K_BKSLASH:220, + K_RBRKT:221, + K_QUOTE:222, + K_oE2:226, + K_OE2:226, + /*K_LOPT:50001, + K_ROPT:50002, + K_NUMERALS:50003, + K_SYMBOLS:50004, + K_CURRENCIES:50005, + K_UPPER:50006, + K_LOWER:50007, + K_ALPHA:50008, + K_SHIFTED:50009, + K_ALTGR:50010, + K_TABBACK:50011, + K_TABFWD:50012*/ +}; + +const k = USVirtualKeyCodes; + +export const USVirtualKeyMap: number[][] = [ + // ` 1 2 3 4 5 6 7 8 9 0 - = [bksp] + [ k.K_BKQUOTE, k.K_1, k.K_2, k.K_3, k.K_4, k.K_5, k.K_6, k.K_7, k.K_8, k.K_9, k.K_0, k.K_HYPHEN, k.K_EQUAL ], + // [tab] Q W E R T Y U I O P [ ] \ + [ k.K_Q, k.K_W, k.K_E, k.K_R, k.K_T, k.K_Y, k.K_U, k.K_I, k.K_O, k.K_P, k.K_LBRKT, k.K_RBRKT, k.K_BKSLASH ], + // [caps] A S D F G H J K L ; ' [enter] + [ k.K_A, k.K_S, k.K_D, k.K_F, k.K_G, k.K_H, k.K_J, k.K_K, k.K_L, k.K_COLON, k.K_QUOTE ], + // [shift] * Z X C V B N M , . / [shift] *=oE2 + [ k.K_oE2, k.K_Z, k.K_X, k.K_C, k.K_V, k.K_B, k.K_N, k.K_M, k.K_COMMA, k.K_PERIOD, k.K_SLASH ] +]; diff --git a/developer/src/kmldmlc/src/keyman/ldmlkeyboard/compiler.ts b/developer/src/kmldmlc/src/keyman/ldmlkeyboard/compiler.ts deleted file mode 100644 index 57d28b9b18..0000000000 --- a/developer/src/kmldmlc/src/keyman/ldmlkeyboard/compiler.ts +++ /dev/null @@ -1,45 +0,0 @@ -import KMXBuilder from '../kmx/kmx-builder'; -import KMXPlusFile from '../kmx/kmx-plus'; - -interface CompilerCallbacks { - loadFile(baseFilename:string, filename: string): Buffer; - reportMessage(severity: number, message: string): void; -}; - -export default class Compiler { - private callbacks: CompilerCallbacks; - constructor (callbacks: CompilerCallbacks) { - this.callbacks = callbacks; - - //TEMP - this.callbacks.reportMessage(0, "Instantiated compiler successfully"); - } - - public compile(filename: string): Uint8Array { - let kmx = new KMXPlusFile(); - - // Load the source XML file into an array - - /*let buf =*/ this.callbacks.loadFile(filename, filename); - - // TODO: create a LDMLXMLSourceFile from buf - - /* - kmx.kmxplus.meta.name = 'Sample Keyboard'; - kmx.kmxplus.meta.author = 'Marc Durdin'; - kmx.kmxplus.loca.locales.push('en'); - kmx.kmxplus.keys.keys.push({ - vkey: 65, - mod: 0, - to: "Hello!", - flags: 0 - }); - */ - - // TODO: transform LDMLXMLSourceFile to the kmx data - - // Use the builder to generate the binary output file - let builder = new KMXBuilder(kmx, true); - return builder.compile(); - } -} diff --git a/developer/src/kmldmlc/src/kmldmlc.ts b/developer/src/kmldmlc/src/kmldmlc.ts index 1518f90d7e..dbc9a5a929 100644 --- a/developer/src/kmldmlc/src/kmldmlc.ts +++ b/developer/src/kmldmlc/src/kmldmlc.ts @@ -7,7 +7,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as program from 'commander'; -import Compiler from './keyman/ldmlkeyboard/compiler'; +import Compiler from './keyman/compiler/compiler'; let inputFilename: string; @@ -48,7 +48,18 @@ class CompilerCallbacks { function compileKeyboard(inputFilename: string): Uint8Array { const c = new CompilerCallbacks(); const k = new Compiler(c); - return k.compile(inputFilename); + let source = k.load(inputFilename); + if(!source) { + return null; + } + if(!k.validate(source)) { + return null; + } + let kmx = k.compile(source); + if(!kmx) { + return null; + } + return k.write(kmx); } // Compile: