mirror of
https://github.com/keymanapp/keyman.git
synced 2026-10-07 22:47:29 +00:00
feat(developer): LDML keyboard XML Loader and Compiler
Implements basic LDML keyboard xml loader and compiler.
This commit is contained in:
parent
3865870114
commit
3c120ebae7
7 changed files with 404 additions and 47 deletions
4
developer/src/kmldmlc/src/keyman/compiler/callbacks.ts
Normal file
4
developer/src/kmldmlc/src/keyman/compiler/callbacks.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export default interface CompilerCallbacks {
|
||||
loadFile(baseFilename: string, filename: string): Buffer;
|
||||
reportMessage(severity: number, message: string): void;
|
||||
};
|
||||
138
developer/src/kmldmlc/src/keyman/compiler/compiler.ts
Normal file
138
developer/src/kmldmlc/src/keyman/compiler/compiler.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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 ]
|
||||
];
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue