feat(developer): LDML keyboard XML Loader and Compiler

Implements basic LDML keyboard xml loader and compiler.
This commit is contained in:
Marc Durdin 2022-08-26 08:51:15 +10:00
parent 3865870114
commit 3c120ebae7
7 changed files with 404 additions and 47 deletions

View file

@ -0,0 +1,4 @@
export default interface CompilerCallbacks {
loadFile(baseFilename: string, filename: string): Buffer;
reportMessage(severity: number, message: string): void;
};

View file

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

View file

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

View file

@ -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 {
/**
* <keyboard> -- 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;
};

View file

@ -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 ]
];

View file

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

View file

@ -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: