diff --git a/common/schemas/kvk/kvk.ksy b/common/schemas/kvk/kvk.ksy
new file mode 100644
index 00000000000..11d8f2faa77
--- /dev/null
+++ b/common/schemas/kvk/kvk.ksy
@@ -0,0 +1,122 @@
+meta:
+ id: kvk
+ title: Keyman Visual Keyboard
+ file-extension: kvk
+ license: MIT
+ ks-version: 0.9
+ endian: le
+ bit-endian: le
+doc: |
+ KVK is the binary file format for Keyman Visual Keyboard
+ files. KVKS is the equivalent XML source file format
+doc-ref:
+ - https://github.com/keymanapp/keyman/
+seq:
+ - id: header
+ type: header
+ - id: keys
+ type: keys
+types:
+ header:
+ seq:
+ - id: identifier
+ contents: 'KVKF'
+ doc: Magic file identifier, always KVKF
+ - id: version
+ contents: [0, 6, 0, 0]
+ doc: Version number of KVK file format, always 0x00000600
+ - id: flag
+ type: header_flags
+ - id: associated_keyboard
+ type: string
+ - id: ansi_font
+ type: font
+ - id: unicode_font
+ type: font
+
+ header_flags:
+ seq:
+ - id: display_102
+ type: b1
+ doc: kvkh102, Keyboard should display 102nd key
+ - id: display_underlying
+ type: b1
+ doc: kvkhDisplayUnderlying, Keyboard should display underlying characters
+ - id: use_underlying
+ type: b1
+ doc: kvkhUseUnderlying,
+ - id: altgr
+ type: b1
+ doc: kvkhAltGr, Keyboard should treat left/right Ctrl and Alt separately
+
+ keys:
+ seq:
+ - id: count
+ type: u4
+ - id: key
+ type: key
+ repeat: expr
+ repeat-expr: count
+ key:
+ seq:
+ - id: flags
+ type: key_flags
+ - id: modifiers
+ type: key_modifiers
+ - id: vkey
+ type: u2
+ - id: text
+ type: string
+ - id: bitmap
+ type: u4
+
+ key_modifiers:
+ seq:
+ - id: shift
+ type: b1
+ - id: ctrl
+ type: b1
+ - id: alt
+ type: b1
+ - id: lctrl
+ type: b1
+ - id: rctrl
+ type: b1
+ - id: lalt
+ type: b1
+ - id: ralt
+ type: b1
+ - id: padding
+ type: b1
+ doc: reserved,
+ - id: zeropad
+ contents: [0]
+
+ key_flags:
+ seq:
+ - id: bitmap
+ type: b1
+ - id: unicode
+ type: b1
+ - id: padding
+ type: b6
+
+ font:
+ seq:
+ - id: name
+ type: string
+ - id: size
+ type: u4
+ - id: color
+ type: u4
+
+ string:
+ seq:
+ - id: len
+ type: u2
+ - id: str
+ type: str
+ size: len*2 - 2
+ encoding: utf-16
+ - id: zero_terminator
+ contents: [0,0]
diff --git a/common/schemas/kvks/README.md b/common/schemas/kvks/README.md
new file mode 100644
index 00000000000..fb4f2a8a687
--- /dev/null
+++ b/common/schemas/kvks/README.md
@@ -0,0 +1,14 @@
+# .kvks schema
+
+This schema validates .kvks files, according to the reference implementation
+from VisualKeyboardLoaderXML.pas.
+
+## Notes on conversion from xsd to json-schema
+
+Converted using xsd2json. Following structural changes:
+
+* kvk-version base type from km-version to string, copy km-version pattern in
+* remove xs:all bracketing
+* remove format:double from fontsize, change type to string
+* encoding property changed type to array
+* kvk-key added _ property for base text value
\ No newline at end of file
diff --git a/common/schemas/kvks/kvks.schema.json b/common/schemas/kvks/kvks.schema.json
new file mode 100644
index 00000000000..fd58f56e380
--- /dev/null
+++ b/common/schemas/kvks/kvks.schema.json
@@ -0,0 +1,155 @@
+{
+ "title": "kvks.xsd",
+ "$schema": "http://json-schema.org/schema#",
+ "type": "object",
+ "properties": {
+ "visualkeyboard": {
+ "properties": {
+ "header": {
+ "$ref": "#/definitions/kvk-header"
+ },
+ "encoding": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/kvk-encoding"
+ }
+ }
+ },
+ "required": [
+ "header"
+ ],
+ "additionalProperties": false,
+ "type": "object"
+ }
+ },
+ "required": [
+ "visualkeyboard"
+ ],
+ "additionalProperties": false,
+ "definitions": {
+ "kvk-header": {
+ "type": "object",
+ "properties": {
+ "version": {
+ "$ref": "#/definitions/kvk-version"
+ },
+ "kbdname": {
+ "type": "string"
+ },
+ "flags": {
+ "$ref": "#/definitions/kvk-header-flags"
+ },
+ "layout": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "version"
+ ],
+ "additionalProperties": false
+ },
+ "kvk-header-flags": {
+ "type": "object",
+ "properties": {
+ "key102": {
+ "$ref": "#/definitions/km-empty"
+ },
+ "displayunderlying": {
+ "$ref": "#/definitions/km-empty"
+ },
+ "usealtgr": {
+ "$ref": "#/definitions/km-empty"
+ },
+ "useunderlying": {
+ "$ref": "#/definitions/km-empty"
+ }
+ },
+ "additionalProperties": false
+ },
+ "kvk-encoding": {
+ "type": "object",
+ "properties": {
+ "layer": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/kvk-layer"
+ }
+ },
+ "name": {
+ "$ref": "#/definitions/kvk-encoding-name"
+ },
+ "fontname": {
+ "type": "string"
+ },
+ "fontsize": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "additionalProperties": false
+ },
+ "kvk-layer": {
+ "type": "object",
+ "properties": {
+ "key": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/kvk-key"
+ }
+ },
+ "shift": {
+ "$ref": "#/definitions/kvk-layer-shift"
+ }
+ },
+ "required": [
+ "shift"
+ ],
+ "additionalProperties": false
+ },
+ "kvk-key": {
+ "type": "object",
+ "properties": {
+ "bitmap": {
+ "type": "string"
+ },
+ "vkey": {
+ "type": "string"
+ },
+ "_": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "vkey"
+ ],
+ "additionalProperties": false
+ },
+ "km-empty": {
+ "type": "string"
+ },
+ "kvk-encoding-name": {
+ "type": "string",
+ "enum": [
+ "ansi",
+ "unicode"
+ ]
+ },
+ "kvk-layer-shift": {
+ "type": "string",
+ "pattern": "S?(C|LC|RC)?(A|LA|RA)?"
+ },
+ "kvk-version": {
+ "type": "string",
+ "pattern": "(\\d+\\.)+(\\d+)",
+ "enum": [
+ "10.0"
+ ]
+ },
+ "km-version": {
+ "type": "string",
+ "pattern": "(\\d+\\.)+(\\d+)"
+ }
+ }
+}
\ No newline at end of file
diff --git a/common/schemas/kvks/kvks.xsd b/common/schemas/kvks/kvks.xsd
new file mode 100644
index 00000000000..17056a5a57b
--- /dev/null
+++ b/common/schemas/kvks/kvks.xsd
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/developer/src/kmc-keyboard/build.sh b/developer/src/kmc-keyboard/build.sh
index 27aeccf27ee..938e0e1ecaa 100755
--- a/developer/src/kmc-keyboard/build.sh
+++ b/developer/src/kmc-keyboard/build.sh
@@ -36,6 +36,7 @@ else
# We need the schema file at runtime and bundled, so always copy it for all actions except `clean`
mkdir -p "$THIS_SCRIPT_PATH/build/src/"
cp "$KEYMAN_ROOT/resources/standards-data/ldml-keyboards/techpreview/ldml-keyboard.schema.json" "$THIS_SCRIPT_PATH/build/src/"
+ cp "$KEYMAN_ROOT/common/schemas/kvks/kvks.schema.json" "$THIS_SCRIPT_PATH/build/src/"
fi
#-------------------------------------------------------------------------------------------------------------------
diff --git a/developer/src/kmc-keyboard/package.json b/developer/src/kmc-keyboard/package.json
index 32884d49c8c..864f6967c86 100644
--- a/developer/src/kmc-keyboard/package.json
+++ b/developer/src/kmc-keyboard/package.json
@@ -24,9 +24,9 @@
"dependencies": {
"@keymanapp/keyman-version": "*",
"ajv": "^8.11.0",
- "restructure": "^3.0.0",
+ "restructure": "git+https://github.com/keymanapp/dependency-restructure.git#49d129cf0916d082a7278bb09296fb89cecfcc50",
"semver": "^7.3.7",
- "xml2js": "^0.4.19"
+ "xml2js": "git+https://github.com/keymanapp/dependency-node-xml2js#535fe732dc408d697e0f847c944cc45f0baf0829"
},
"devDependencies": {
"@types/chai": "^4.1.7",
diff --git a/developer/src/kmc-keyboard/src/compiler/callbacks.ts b/developer/src/kmc-keyboard/src/compiler/callbacks.ts
index 8d32f5e8c4a..f6cd673b562 100644
--- a/developer/src/kmc-keyboard/src/compiler/callbacks.ts
+++ b/developer/src/kmc-keyboard/src/compiler/callbacks.ts
@@ -6,5 +6,6 @@ export interface CompilerEvent {
export default interface CompilerCallbacks {
loadFile(baseFilename: string, filename: string): Buffer;
loadLdmlKeyboardSchema(): Buffer;
+ loadKvksJsonSchema(): Buffer;
reportMessage(event: CompilerEvent): void;
};
diff --git a/developer/src/kmc-keyboard/src/compiler/messages.ts b/developer/src/kmc-keyboard/src/compiler/messages.ts
index 24c4370767a..409b37acab9 100644
--- a/developer/src/kmc-keyboard/src/compiler/messages.ts
+++ b/developer/src/kmc-keyboard/src/compiler/messages.ts
@@ -20,7 +20,7 @@ export class CompilerMessages {
static Error_InvalidNormalization = (o:{form: string}) => m(this.ERROR_InvalidNormalization, `Invalid normalization form '${o.form}`);
static ERROR_InvalidNormalization = SevError | 0x0001;
- static Error_InvalidLocale = (o:{tag: string}) => m(this.ERROR_InvalidLocale, `Invalid BCP 47 locale form '${o.tag}`);
+ static Error_InvalidLocale = (o:{tag: string}) => m(this.ERROR_InvalidLocale, `Invalid BCP 47 locale form '${o.tag}'`);
static ERROR_InvalidLocale = SevError | 0x0002;
static Error_HardwareLayerHasTooManyRows = () => m(this.ERROR_HardwareLayerHasTooManyRows, `'hardware' layer has too many rows`);
diff --git a/developer/src/kmc-keyboard/src/kmx/kmx.ts b/developer/src/kmc-keyboard/src/kmx/kmx.ts
index 9583d9a889e..b5cf2e1df2d 100644
--- a/developer/src/kmc-keyboard/src/kmx/kmx.ts
+++ b/developer/src/kmc-keyboard/src/kmx/kmx.ts
@@ -2,6 +2,8 @@ import * as r from 'restructure';
/* Definitions from kmx_file.h. Must be kept in sync */
+// TODO: split kmx-file from kmx in-memory, similar to what I've done for kvk (keep restructure decl + BUILDER_ interfaces together)
+
// In memory representations of KMX structures
// kmx-builder will transform these to the corresponding COMP_xxxx
diff --git a/developer/src/kmc-keyboard/src/kvk/kvk-file-reader.ts b/developer/src/kmc-keyboard/src/kvk/kvk-file-reader.ts
new file mode 100644
index 00000000000..c3ee8924506
--- /dev/null
+++ b/developer/src/kmc-keyboard/src/kvk/kvk-file-reader.ts
@@ -0,0 +1,34 @@
+import KVKFile, { BUILDER_KVK_FILE, BUILDER_KVK_HEADER_IDENTIFIER, BUILDER_KVK_HEADER_VERSION } from "./kvk-file.js";
+import { VisualKeyboard, VisualKeyboardKey } from "./visual-keyboard.js";
+
+export default class KvkFileReader {
+ public read(source: Uint8Array): VisualKeyboard {
+ let binary: BUILDER_KVK_FILE;
+ let kvk = new KVKFile();
+ binary = kvk.KVK_FILE.fromBuffer(source);
+ if(binary.header.identifier != BUILDER_KVK_HEADER_IDENTIFIER ||
+ binary.header.version != BUILDER_KVK_HEADER_VERSION) {
+ return null;
+ }
+ let result = new VisualKeyboard();
+ result.header.version = binary.header.version;
+ result.header.flags = binary.header.flags;
+ result.header.associatedKeyboard = binary.header.associatedKeyboard.str;
+ result.header.ansiFont.color = binary.header.ansiFont.color;
+ result.header.ansiFont.name = binary.header.ansiFont.name.str;
+ result.header.ansiFont.size = binary.header.ansiFont.size;
+ result.header.unicodeFont.color = binary.header.unicodeFont.color;
+ result.header.unicodeFont.name = binary.header.unicodeFont.name.str;
+ result.header.unicodeFont.size = binary.header.unicodeFont.size;
+ for(let binaryKey of binary.keys) {
+ let key: VisualKeyboardKey = {
+ flags: binaryKey.flags,
+ vkey: binaryKey.vkey,
+ shift: binaryKey.shift,
+ text: binaryKey.text.str
+ };
+ result.keys.push(key);
+ }
+ return result;
+ }
+};
\ No newline at end of file
diff --git a/developer/src/kmc-keyboard/src/kvk/kvk-file-writer.ts b/developer/src/kmc-keyboard/src/kvk/kvk-file-writer.ts
new file mode 100644
index 00000000000..78a484284bd
--- /dev/null
+++ b/developer/src/kmc-keyboard/src/kvk/kvk-file-writer.ts
@@ -0,0 +1,72 @@
+import KVKFile, { BUILDER_KVK_FILE, BUILDER_KVK_HEADER_IDENTIFIER, BUILDER_KVK_HEADER_VERSION, BUILDER_KVK_KEY, BUILDER_KVK_STRING } from "./kvk-file.js";
+import { VisualKeyboard } from "./visual-keyboard.js";
+
+export default class KvkFileWriter {
+ /**
+ * Writes the visual keyboard to a binary .kvk format byte array.
+ * @param source VisualKeyboard
+ * @returns Uint8Array, the .kvk file
+ */
+ write(source: VisualKeyboard): Uint8Array {
+ const binary = this.build(source);
+ const kvk = new KVKFile();
+ const file: Uint8Array = new Uint8Array(kvk.KVK_FILE.size(binary));
+ const data = kvk.KVK_FILE.toBuffer(binary);
+ file.set(data, 0);
+ return file;
+ }
+
+ private build(source: VisualKeyboard) {
+ const binary: BUILDER_KVK_FILE = {
+ header: {
+ identifier: BUILDER_KVK_HEADER_IDENTIFIER,
+ version: BUILDER_KVK_HEADER_VERSION,
+ associatedKeyboard: {len:0,str:''},
+ flags: source.header.flags,
+ ansiFont:{
+ color: source.header.ansiFont.color,
+ size: source.header.ansiFont.size,
+ name: {len:0,str:''}
+ },
+ unicodeFont:{
+ color: source.header.unicodeFont.color,
+ size: source.header.unicodeFont.size,
+ name: {len:0,str:''}
+ },
+ },
+ keyCount: source.keys.length,
+ keys:[]
+ };
+
+ this.setString(binary.header.associatedKeyboard, source.header.associatedKeyboard);
+ this.setString(binary.header.ansiFont.name, source.header.ansiFont.name);
+ this.setString(binary.header.unicodeFont.name, source.header.unicodeFont.name);
+
+ for(let sourceKey of source.keys) {
+ const binaryKey: BUILDER_KVK_KEY = {
+ flags: sourceKey.flags,
+ vkey: sourceKey.vkey,
+ shift: sourceKey.shift,
+ text: { len: 0, str: '' },
+ bitmap: 0
+ };
+ this.setString(binaryKey.text, sourceKey.text);
+ binary.keys.push(binaryKey);
+ }
+
+ return binary;
+ }
+
+ /**
+ * Fills a kvk string from a source string. Note that the format includes both
+ * a length word and zero termination.
+ *
+ * @param str
+ * @param value
+ * @returns number
+ */
+ private setString(str: BUILDER_KVK_STRING, value: string): void {
+ str.len = value.length + 1;
+ str.str = value;
+ }
+};
\ No newline at end of file
diff --git a/developer/src/kmc-keyboard/src/kvk/kvk-file.ts b/developer/src/kmc-keyboard/src/kvk/kvk-file.ts
new file mode 100644
index 00000000000..b33117e0f0f
--- /dev/null
+++ b/developer/src/kmc-keyboard/src/kvk/kvk-file.ts
@@ -0,0 +1,122 @@
+import * as r from 'restructure';
+//
+// Binary backing structures for .kvk format
+// matching VisualKeyboardSaverBinary.pas
+//
+
+export const BUILDER_KVK_HEADER_IDENTIFIER = 0x464B564B; // 'KVKF', little-endian
+export const BUILDER_KVK_HEADER_VERSION = 0x0600; // Keyman 6.0
+
+export interface BUILDER_KVK_STRING {
+ len: number;
+ str: string;
+};
+
+export interface BUILDER_KVK_FONT {
+ name: BUILDER_KVK_STRING; // 0000, variable length
+ size: number; // 4 bytes
+ color: number; // 4 bytes
+};
+
+export const BUILDER_KVK_FONT_Size = 8; // size of fixed elements of BUILDER_KVK_FONT
+
+export const enum BUILDER_KVK_KEY_FLAGS {
+ kvkkBitmap = 0x01,
+ kvkkUnicode = 0x02
+};
+
+export const enum BUILDER_KVK_SHIFT_STATE {
+ KVKS_NORMAL = 0,
+ KVKS_SHIFT = 1,
+ KVKS_CTRL = 2,
+ KVKS_ALT = 4,
+ KVKS_LCTRL = 8,
+ KVKS_RCTRL = 16,
+ KVKS_LALT = 32,
+ KVKS_RALT = 64
+};
+
+export interface BUILDER_KVK_KEY {
+ flags: BUILDER_KVK_KEY_FLAGS; // 0000, 1 byte
+ shift: number; // 0001, 2 bytes
+ vkey: number; // 0003, 2 bytes
+ text: BUILDER_KVK_STRING; // 0005, variable length
+ bitmap: number; // always 0 // 4 bytes
+};
+
+export const BUILDER_KVK_KEY_Size = 9; // size of fixed elements of BUILDER_KVK_KEY
+
+export const enum BUILDER_KVK_HEADER_FLAGS {
+ kvkh102 = 0x01,
+ kvkhDisplayUnderlying = 0x02,
+ kvkhUseUnderlying = 0x04,
+ kvkhAltGr = 0x08
+};
+
+export interface BUILDER_KVK_HEADER {
+ identifier: number; // 0000, 4 bytes
+ version: number; // 0004, 4 bytes
+ flags: BUILDER_KVK_HEADER_FLAGS; // 0008, 1 byte
+ associatedKeyboard: BUILDER_KVK_STRING; // 0009, variable length
+ ansiFont: BUILDER_KVK_FONT; // variable length
+ unicodeFont: BUILDER_KVK_FONT; // variable length
+};
+
+export const BUILDER_KVK_HEADER_Size = 9 + BUILDER_KVK_FONT_Size + BUILDER_KVK_FONT_Size; // size of fixed elements of BUILDER_KVK_HEADER
+
+export interface BUILDER_KVK_FILE {
+ header: BUILDER_KVK_HEADER; // variable length
+ keyCount: number; // 4 bytes
+ keys: BUILDER_KVK_KEY[]; // variable length
+};
+
+export default class KVKFile {
+ public KVK_HEADER: any;
+ public KVK_KEY: any;
+ public KVK_KEYS: any;
+ public KVK_FONT: any;
+ public KVK_FILE: any;
+ public KVK_STRING: any;
+
+ constructor() {
+ //
+ // Binary restructure definitions matching VisualKeyboardSaverBinary.pas
+ // TODO: move binaries to separate kvk-file.ts
+ //
+
+ this.KVK_STRING = new r.Struct({
+ len: r.int16le,
+ str: new r.String(null, 'utf16le')
+ });
+
+ this.KVK_FONT = new r.Struct({
+ name: this.KVK_STRING,
+ size: r.int32le,
+ color: r.uint32le
+ });
+
+ this.KVK_KEY = new r.Struct({
+ flags: r.uint8,
+ shift: r.uint16le,
+ vkey: r.uint16le,
+ text: this.KVK_STRING,
+ bitmap: r.uint32le // always 00 00 00 00 for now
+ });
+
+ this.KVK_HEADER = new r.Struct({
+ identifier: r.uint32le, // KVKF
+ version: r.uint32le, // 0x0600
+ flags: r.uint8,
+ associatedKeyboard: this.KVK_STRING,
+ ansiFont: this.KVK_FONT,
+ unicodeFont: this.KVK_FONT
+ });
+
+ this.KVK_FILE = new r.Struct({
+ header: this.KVK_HEADER,
+ keyCount: r.uint32le,
+ keys: new r.Array(this.KVK_KEY, 'keyCount')
+ });
+
+ }
+};
\ No newline at end of file
diff --git a/developer/src/kmc-keyboard/src/kvk/kvks-file-reader.ts b/developer/src/kmc-keyboard/src/kvk/kvks-file-reader.ts
new file mode 100644
index 00000000000..3490c709b59
--- /dev/null
+++ b/developer/src/kmc-keyboard/src/kvk/kvks-file-reader.ts
@@ -0,0 +1,167 @@
+import * as xml2js from 'xml2js';
+import KVKSourceFile from './kvks-file.js';
+import CompilerCallbacks from '../compiler/callbacks.js';
+import Ajv from 'ajv';
+import { CompilerMessages } from '../compiler/messages.js';
+import { boxXmlArray } from '../util/util.js';
+import { VisualKeyboard, VisualKeyboardHeaderFlags, VisualKeyboardKey, VisualKeyboardKeyFlags, VisualKeyboardLegalShiftStates, VisualKeyboardShiftState } from './visual-keyboard.js';
+import { USVirtualKeyCodes } from '../ldml-keyboard/virtual-key-constants.js';
+import { BUILDER_KVK_HEADER_VERSION } from './kvk-file.js';
+
+export default class KVKSFileReader {
+ private readonly callbacks: CompilerCallbacks;
+
+ constructor (callbacks: CompilerCallbacks) {
+ this.callbacks = callbacks;
+ }
+
+ public read(file: Uint8Array): VisualKeyboard {
+ let source = this.internalRead(file);
+ return this.transform(source);
+ }
+
+ public internalRead(file: Uint8Array): KVKSourceFile {
+ let source: KVKSourceFile;
+
+ const parser = new xml2js.Parser({
+ explicitArray: false,
+ mergeAttrs: true,
+ includeWhiteChars: true,
+ normalize: false,
+ emptyTag: {} as any
+ // Why "as any"? xml2js is broken:
+ // https://github.com/Leonidas-from-XIV/node-xml2js/issues/648 means
+ // that an old version of `emptyTag` is used which doesn't support
+ // functions, but DefinitelyTyped is requiring use of function or a
+ // string. See also notes at
+ // https://github.com/DefinitelyTyped/DefinitelyTyped/pull/59259#issuecomment-1254405470
+ // An alternative fix would be to pull xml2js directly from github
+ // rather than using the version tagged on npmjs.com.
+ });
+
+ parser.parseString(file, (e: unknown, r: unknown) => { source = r as KVKSourceFile });
+ source = this.boxArrays(source);
+ this.cleanupUnderscore('visualkeyboard', source.visualkeyboard);
+ return this.validate(source);
+ }
+
+ /**
+ * The only element that allows spaces is . Remove
+ * all other empty whitespace-only values.
+ * @param root
+ * @param source
+ */
+ private cleanupUnderscore(root: string, source: any) {
+ if(root != 'key') {
+ if(source?.['_']?.trim() === '') {
+ delete source['_'];
+ }
+ }
+
+ for(let key of Object.keys(source)) {
+ if(Array.isArray(source[key])) {
+ for(let item of source[key]) {
+ if(typeof(item) === 'object') {
+ this.cleanupUnderscore(key, item);
+ }
+ }
+ } else if(typeof source[key] === 'object') {
+ this.cleanupUnderscore(key, source[key]);
+ }
+ }
+ }
+
+ public validate(source: KVKSourceFile): KVKSourceFile {
+ const schema = JSON.parse(this.callbacks.loadKvksJsonSchema().toString('utf8'));
+ const ajv = new Ajv();
+ if(!ajv.validate(schema, source)) {
+ console.dir(source, {depth:8});
+ this.callbacks.reportMessage(CompilerMessages.Error_InvalidFile({errorText: ajv.errorsText()}));
+ return null;
+ }
+ return source;
+ }
+
+ public transform(source: KVKSourceFile): VisualKeyboard {
+ // NOTE: at this point, the xml should have been validated
+ // and matched the schema result so we can assume properties exist
+ let result: VisualKeyboard = {
+ header: {
+ version: BUILDER_KVK_HEADER_VERSION,
+ flags: 0,
+ ansiFont: { name: "Arial", size: -12, color: 0xFF000008 }, // TODO-LDML: consider defaults
+ unicodeFont: { name: "Arial", size: -12, color: 0xFF000008 }, // TODO-LDML: consider defaults
+ associatedKeyboard: source.visualkeyboard?.header?.kbdname
+ },
+ keys: []
+ };
+
+ if(source.visualkeyboard?.header?.flags?.displayunderlying !== undefined) {
+ result.header.flags |= VisualKeyboardHeaderFlags.kvkhDisplayUnderlying;
+ }
+ if(source.visualkeyboard?.header?.flags?.key102 !== undefined) {
+ result.header.flags |= VisualKeyboardHeaderFlags.kvkh102;
+ }
+ if(source.visualkeyboard?.header?.flags?.usealtgr !== undefined) {
+ result.header.flags |= VisualKeyboardHeaderFlags.kvkhAltGr;
+ }
+ if(source.visualkeyboard?.header?.flags?.useunderlying !== undefined) {
+ result.header.flags |= VisualKeyboardHeaderFlags.kvkhUseUnderlying;
+ }
+
+ for(let encoding of source.visualkeyboard.encoding) {
+ let isUnicode = (encoding.name == 'unicode'),
+ font = isUnicode ? result.header.unicodeFont : result.header.ansiFont;
+ font.name = encoding.fontname;
+ font.size = parseInt(encoding.fontsize,10);
+ for(let layer of encoding.layer) {
+ let shift = this.kvksShiftToKvkShift(layer.shift);
+ for(let sourceKey of layer.key) {
+ let vkey = (USVirtualKeyCodes as any)[sourceKey.vkey];
+ if(!vkey) {
+ this.callbacks.reportMessage(CompilerMessages.Error_VkeyIsNotValid({vkey: sourceKey.vkey}));
+ continue;
+ }
+ let key: VisualKeyboardKey = {
+ flags: isUnicode ? VisualKeyboardKeyFlags.kvkkUnicode : 0, // TODO-LDML: bitmap support
+ shift: shift,
+ text: sourceKey._ ?? '',
+ vkey: vkey
+ }
+ result.keys.push(key);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * xml2js will not place single-entry objects into arrays.
+ * Easiest way to fix this is to box them ourselves as needed
+ * @param source KVKSourceFile
+ */
+ private boxArrays(source: KVKSourceFile) {
+ boxXmlArray(source.visualkeyboard, 'encoding');
+ for(let encoding of source.visualkeyboard.encoding) {
+ boxXmlArray(encoding, 'layer');
+ for(let layer of encoding.layer) {
+ boxXmlArray(layer, 'key');
+ }
+ }
+ return source;
+ }
+
+
+ public kvksShiftToKvkShift(shift: string): VisualKeyboardShiftState {
+ shift = shift.toUpperCase();
+
+ // TODO-LDML(lowpri): make a map of this?
+ for(let state of VisualKeyboardLegalShiftStates) {
+ if(state.name == shift) {
+ return state.shift;
+ }
+ }
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/developer/src/kmc-keyboard/src/kvk/kvks-file.ts b/developer/src/kmc-keyboard/src/kvk/kvks-file.ts
new file mode 100644
index 00000000000..46b860c59bf
--- /dev/null
+++ b/developer/src/kmc-keyboard/src/kvk/kvks-file.ts
@@ -0,0 +1,48 @@
+// .kvks xml format
+// See VisualKeyboardLoaderXML.pas, not the same as VisualKeyboardImportXML.pas!
+
+export default interface KVKSourceFile {
+ /**
+ * -- the root element.
+ */
+ visualkeyboard: KVKSVisualKeyboard;
+}
+
+export interface KVKSVisualKeyboard {
+ header?: KVKSHeader;
+ encoding?: KVKSEncoding[];
+};
+
+export interface KVKSHeader {
+ version?: string;
+ kbdname?: string;
+ flags?: KVKSFlags;
+ keybitmap?: string;
+ layout?: string;
+};
+
+export interface KVKSFlags {
+ key102?: string;
+ displayunderlying?: string;
+ useunderlying?: string;
+ usealtgr?: string;
+};
+
+export interface KVKSEncoding {
+ name?: string;
+ fontname?: string;
+ fontsize?: string;
+ layer?: KVKSLayer[];
+};
+
+export interface KVKSLayer {
+ shift?: string;
+ key?: KVKSKey[];
+};
+
+export interface KVKSKey {
+ vkey?: string;
+ bitmap?: string;
+ _?: string;
+};
+
diff --git a/developer/src/kmc-keyboard/src/kvk/visual-keyboard.ts b/developer/src/kmc-keyboard/src/kvk/visual-keyboard.ts
new file mode 100644
index 00000000000..c50011ca46d
--- /dev/null
+++ b/developer/src/kmc-keyboard/src/kvk/visual-keyboard.ts
@@ -0,0 +1,79 @@
+//
+// Visual Keyboard in-memory data
+//
+// Corresponds to .kvk / .kvks file data
+//
+
+import { BUILDER_KVK_SHIFT_STATE, BUILDER_KVK_HEADER_FLAGS, BUILDER_KVK_KEY_FLAGS } from "./kvk-file";
+
+export class VisualKeyboard {
+ header: VisualKeyboardHeader = {flags: 0, ansiFont:{}, unicodeFont:{}};
+ keys: VisualKeyboardKey[] = [];
+};
+
+export { BUILDER_KVK_HEADER_FLAGS as VisualKeyboardHeaderFlags } from "./kvk-file";
+
+export class VisualKeyboardHeader {
+ version?: number; // 0x0600
+ flags: BUILDER_KVK_HEADER_FLAGS;
+ associatedKeyboard?: string;
+ ansiFont: VisualKeyboardFont; // generally unused
+ unicodeFont: VisualKeyboardFont;
+};
+
+export class VisualKeyboardFont {
+ name?: string;
+ size?: number;
+ color?: number; // unused
+};
+
+export { BUILDER_KVK_KEY_FLAGS as VisualKeyboardKeyFlags } from "./kvk-file.js";
+
+export { BUILDER_KVK_SHIFT_STATE as VisualKeyboardShiftState } from "./kvk-file.js";
+
+export class VisualKeyboardKey {
+ flags?: BUILDER_KVK_KEY_FLAGS;
+ shift?: BUILDER_KVK_SHIFT_STATE;
+ vkey?: number;
+ text?: string;
+ //bitmap: bitmap-image, unsupported in ldml keyboards; may include for round-tripabilty?
+};
+
+interface VisualKeyboardLegalShiftState {
+ desc: string;
+ name: string;
+ shift: number;
+ vkeys: number[];
+};
+
+import { BUILDER_KVK_SHIFT_STATE as ss } from "./kvk-file.js";
+import { USVirtualKeyCodes as vk } from "../ldml-keyboard/virtual-key-constants.js";
+
+export const VisualKeyboardLegalShiftStates: VisualKeyboardLegalShiftState[] = [
+ {desc: 'Unshifted', name: '', shift: ss.KVKS_NORMAL, vkeys: []}, //1
+
+ {desc: 'Shift', name: 'S', shift: ss.KVKS_SHIFT, vkeys: [vk.K_SHIFT]},
+ {desc: 'Ctrl', name: 'C', shift: ss.KVKS_CTRL, vkeys: [vk.K_CONTROL]},
+ {desc: 'Alt', name: 'A', shift: ss.KVKS_ALT, vkeys: [vk.K_ALT]},
+ {desc: 'Shift+Ctrl', name: 'SC', shift: ss.KVKS_SHIFT | ss.KVKS_CTRL, vkeys: [vk.K_SHIFT,vk.K_CONTROL]},
+ {desc: 'Shift+Alt', name: 'SA', shift: ss.KVKS_SHIFT | ss.KVKS_ALT, vkeys: [vk.K_SHIFT,vk.K_ALT]},
+ {desc: 'Ctrl+Alt', name: 'CA', shift: ss.KVKS_CTRL | ss.KVKS_ALT, vkeys: [vk.K_CONTROL,vk.K_ALT]},
+ {desc: 'Shift+Ctrl+Alt', name: 'SCA', shift: ss.KVKS_SHIFT | ss.KVKS_CTRL | ss.KVKS_ALT, vkeys: [vk.K_SHIFT,vk.K_CONTROL,vk.K_ALT]}, //7
+
+ {desc: 'Left Ctrl', name: 'LC', shift: ss.KVKS_LCTRL, vkeys: [vk.K_LCONTROL]},
+ {desc: 'Right Ctrl', name: 'RC', shift: ss.KVKS_RCTRL, vkeys: [vk.K_RCONTROL]},
+ {desc: 'Left Alt', name: 'LA', shift: ss.KVKS_LALT, vkeys: [vk.K_LALT]},
+ {desc: 'Right Alt', name: 'RA', shift: ss.KVKS_RALT, vkeys: [vk.K_RALT]},
+ {desc: 'Shift+Left Ctrl', name: 'SLC', shift: ss.KVKS_SHIFT | ss.KVKS_LCTRL, vkeys: [vk.K_SHIFT,vk.K_LCONTROL]},
+ {desc: 'Shift+Right Ctrl', name: 'SRC', shift: ss.KVKS_SHIFT | ss.KVKS_RCTRL, vkeys: [vk.K_SHIFT,vk.K_RCONTROL]},
+ {desc: 'Shift+Left Alt', name: 'SLA', shift: ss.KVKS_SHIFT | ss.KVKS_LALT, vkeys: [vk.K_SHIFT,vk.K_LALT]},
+ {desc: 'Shift+Right Alt', name: 'SRA', shift: ss.KVKS_SHIFT | ss.KVKS_RALT, vkeys: [vk.K_SHIFT,vk.K_RALT]},
+ {desc: 'Left Ctrl+Left Alt', name: 'LCLA', shift: ss.KVKS_LCTRL | ss.KVKS_LALT, vkeys: [vk.K_LCONTROL,vk.K_LALT]},
+ {desc: 'Left Ctrl+Right Alt', name: 'LCRA', shift: ss.KVKS_LCTRL | ss.KVKS_RALT, vkeys: [vk.K_LCONTROL,vk.K_RALT]},
+ {desc: 'Right Ctrl+Left Alt', name: 'RCLA', shift: ss.KVKS_RCTRL | ss.KVKS_LALT, vkeys: [vk.K_RCONTROL,vk.K_LALT]},
+ {desc: 'Right Ctrl+Right Alt', name: 'RCRA', shift: ss.KVKS_RCTRL | ss.KVKS_RALT, vkeys: [vk.K_RCONTROL,vk.K_RALT]},
+ {desc: 'Shift+Left Ctrl+Left Alt', name: 'SLCLA', shift: ss.KVKS_SHIFT | ss.KVKS_LCTRL | ss.KVKS_LALT, vkeys: [vk.K_SHIFT, vk.K_LCONTROL,vk.K_LALT]},
+ {desc: 'Shift+Left Ctrl+Right Alt', name: 'SLCRA', shift: ss.KVKS_SHIFT | ss.KVKS_LCTRL | ss.KVKS_RALT, vkeys: [vk.K_SHIFT, vk.K_LCONTROL,vk.K_RALT]},
+ {desc: 'Shift+Right Ctrl+Left Alt', name: 'SRCLA', shift: ss.KVKS_SHIFT | ss.KVKS_RCTRL | ss.KVKS_LALT, vkeys: [vk.K_SHIFT, vk.K_RCONTROL,vk.K_LALT]},
+ {desc: 'Shift+Right Ctrl+Right Alt', name: 'SRCRA', shift: ss.KVKS_SHIFT | ss.KVKS_RCTRL | ss.KVKS_RALT, vkeys: [vk.K_SHIFT, vk.K_RCONTROL,vk.K_RALT]}, //16
+];
diff --git a/developer/src/kmc-keyboard/src/ldml-keyboard/ldml-keyboard-xml-reader.ts b/developer/src/kmc-keyboard/src/ldml-keyboard/ldml-keyboard-xml-reader.ts
index 2014b8c58a4..f8202715670 100644
--- a/developer/src/kmc-keyboard/src/ldml-keyboard/ldml-keyboard-xml-reader.ts
+++ b/developer/src/kmc-keyboard/src/ldml-keyboard/ldml-keyboard-xml-reader.ts
@@ -3,6 +3,7 @@ import LDMLKeyboardXMLSourceFile from './ldml-keyboard-xml.js';
import CompilerCallbacks from '../compiler/callbacks.js';
import Ajv from 'ajv';
import { CompilerMessages } from '../compiler/messages.js';
+import { boxXmlArray } from '../util/util.js';
export default class LDMLKeyboardXMLSourceFileReader {
private readonly callbacks: CompilerCallbacks;
@@ -17,40 +18,29 @@ export default class LDMLKeyboardXMLSourceFileReader {
* @param source any
*/
private boxArrays(source: any) {
- let box = (o: any, x: string) => {
- if(typeof o == 'object' && !Array.isArray(o[x])) {
- if(o[x] === null || o[x] === undefined) {
- o[x] = [];
- }
- else {
- o[x] = [o[x]];
- }
- }
- }
-
- box(source?.keyboard, 'layerMaps');
- box(source?.keyboard?.names, 'name');
- box(source?.keyboard?.vkeyMaps, 'vkeyMap');
- box(source?.keyboard?.keys, 'key');
- box(source?.keyboard?.locales, 'locale');
- box(source?.keyboard, 'transforms');
+ boxXmlArray(source?.keyboard, 'layerMaps');
+ boxXmlArray(source?.keyboard?.names, 'name');
+ boxXmlArray(source?.keyboard?.vkeyMaps, 'vkeyMap');
+ boxXmlArray(source?.keyboard?.keys, 'key');
+ boxXmlArray(source?.keyboard?.locales, 'locale');
+ boxXmlArray(source?.keyboard, 'transforms');
if(source?.keyboard?.layerMaps) {
for(let layerMaps of source?.keyboard?.layerMaps) {
- box(layerMaps, 'layerMap');
+ boxXmlArray(layerMaps, 'layerMap');
if(layerMaps?.layerMap) {
for(let layerMap of layerMaps?.layerMap) {
- box(layerMap, 'row');
+ boxXmlArray(layerMap, 'row');
}
}
}
}
if(source?.keyboard?.transforms) {
for(let transform of source.keyboard.transforms) {
- box(transform, 'transform');
+ boxXmlArray(transform, 'transform');
}
}
- box(source?.keyboard?.reorders, 'reorder');
- box(source?.keyboard?.backspaces, 'backspace');
+ boxXmlArray(source?.keyboard?.reorders, 'reorder');
+ boxXmlArray(source?.keyboard?.backspaces, 'backspace');
return source;
}
@@ -75,6 +65,7 @@ export default class LDMLKeyboardXMLSourceFileReader {
let parser = new xml2js.Parser({
explicitArray: false,
mergeAttrs: true,
+ includeWhiteChars: false,
emptyTag: {} as any
// Why "as any"? xml2js is broken:
// https://github.com/Leonidas-from-XIV/node-xml2js/issues/648 means
diff --git a/developer/src/kmc-keyboard/src/util/util.ts b/developer/src/kmc-keyboard/src/util/util.ts
index d663fc490bb..520de398b2e 100644
--- a/developer/src/kmc-keyboard/src/util/util.ts
+++ b/developer/src/kmc-keyboard/src/util/util.ts
@@ -5,3 +5,21 @@
export function isValidEnumValue(enu: T, value: string) {
return (Object.values(enu) as string[]).includes(value);
}
+
+/**
+ * xml2js will not place single-entry objects into arrays. Easiest way to fix
+ * this is to box them ourselves as needed. Ensures that o.x is an array.
+ *
+ * @param o Object with property to box
+ * @param x Name of element to box
+ */
+export function boxXmlArray(o: any, x: string): void {
+ if(typeof o == 'object' && !Array.isArray(o[x])) {
+ if(o[x] === null || o[x] === undefined) {
+ o[x] = [];
+ }
+ else {
+ o[x] = [o[x]];
+ }
+ }
+}
diff --git a/developer/src/kmc-keyboard/test/fixtures/kvk/khmer_angkor.kvk b/developer/src/kmc-keyboard/test/fixtures/kvk/khmer_angkor.kvk
new file mode 100644
index 00000000000..c64b3a5a544
Binary files /dev/null and b/developer/src/kmc-keyboard/test/fixtures/kvk/khmer_angkor.kvk differ
diff --git a/developer/src/kmc-keyboard/test/fixtures/kvk/khmer_angkor.kvks b/developer/src/kmc-keyboard/test/fixtures/kvk/khmer_angkor.kvks
new file mode 100644
index 00000000000..e9c38a464dd
--- /dev/null
+++ b/developer/src/kmc-keyboard/test/fixtures/kvk/khmer_angkor.kvks
@@ -0,0 +1,206 @@
+
+
+
+ 10.0
+ khmer_angkor
+
+
+
+
+
+
+ ឞ
+ ឝ
+ ៈ
+ ឳ
+ ឨ
+ ឩ
+ ឰ
+ ឫ
+ ឦ
+ ឱ
+ ឯ
+
+
+ ៜ
+
+ ៖
+ ៙
+ ៚
+
+ ៘
+
+
+ ]
+ [
+ /
+ .
+ ‘
+ +
+ &
+ ’
+ *
+ @
+ \
+ }
+ {
+ -
+ ÷
+ :
+ ,
+ ≈
+ ;
+ <
+ #
+ >
+ ×
+ $
+ €
+
+
+
+ ៸
+ ៰
+ ៱
+ ៲
+ ៳
+ ៴
+ ៵
+ ៶
+ ៷
+ ៹
+ ᧿
+ ᧾
+ ᧪
+ ᧫
+ ᧶
+ ᧵
+ ᧬
+ ᧷
+ ᧥
+ ᧸
+ ᧡
+ ᧻
+ ᧹
+ ᧮
+ ᧢
+ ᧯
+ ᧰
+ ᧦
+ ᧱
+ ᧤
+ ᧭
+ ᧣
+ ᧧
+ ᧠
+ ᧺
+ ᧲
+ ᧳
+ ᧴
+ ᧩
+ ᧨
+ ᧼
+ ᧽
+
+
+
+
+
+ ឥ
+ ។
+
+ ០
+ ១
+ ២
+ ៣
+ ៤
+ ៥
+ ៦
+ ៧
+ ៨
+ ៩
+
+ ឲ
+
+ ឮ
+ ឪ
+ «
+
+ ប
+ ច
+ ដ
+
+ ថ
+ ង
+ ហ
+
+
+ ក
+ ល
+ ម
+ ន
+
+ ផ
+ ឆ
+ រ
+ ស
+ ត
+
+ វ
+
+ ខ
+ យ
+ ឋ
+
+
+
+ !
+
+ "
+ ៛
+ %
+
+ (
+ )
+
+ =
+
+ ៕
+ ?
+ ៗ
+
+ ព
+ ជ
+ ឌ
+
+ ធ
+ អ
+ ះ
+
+ ញ
+ គ
+ ឡ
+
+ ណ
+
+ ភ
+ ឈ
+ ឬ
+
+ ទ
+
+
+
+ ឃ
+
+ ឍ
+
+
+
+ ឭ
+ ឧ
+ »
+
+
+
+
diff --git a/developer/src/kmc-keyboard/test/helpers/index.ts b/developer/src/kmc-keyboard/test/helpers/index.ts
index 0b4e0dc740d..f7636fae185 100644
--- a/developer/src/kmc-keyboard/test/helpers/index.ts
+++ b/developer/src/kmc-keyboard/test/helpers/index.ts
@@ -24,20 +24,36 @@ export function makePathToFixture(...components: string[]): string {
return fileURLToPath(new URL(path.join('..', '..', '..', 'test', 'fixtures', ...components), import.meta.url));
}
-
-export class CompilerCallbacks {
+class CompilerCallbacks {
messages: CompilerEvent[] = [];
loadFile(baseFilename: string, filename:string): Buffer {
// TODO: translate filename based on the baseFilename
return fs.readFileSync(filename);
}
reportMessage(event: CompilerEvent): void {
+ // console.log(event.message);
this.messages.push(event);
}
loadLdmlKeyboardSchema(): Buffer {
return fs.readFileSync(new URL(path.join('..', '..', 'src', 'ldml-keyboard.schema.json'), import.meta.url));
}
-}
+ loadKvksJsonSchema(): Buffer {
+ return fs.readFileSync(new URL(path.join('..', '..', 'src', 'kvks.schema.json'), import.meta.url));
+ }
+};
+
+export const compilerTestCallbacks = new CompilerCallbacks();
+
+beforeEach(function() {
+ compilerTestCallbacks.messages = [];
+});
+
+afterEach(function() {
+ if (this.currentTest.state !== 'passed') {
+ compilerTestCallbacks.messages.forEach(message => console.log(message.message));
+ }
+});
+
export function loadSectionFixture(compilerClass: typeof SectionCompiler, filename: string, callbacks: CompilerCallbacks): Section {
callbacks.messages = [];
@@ -57,18 +73,18 @@ export function loadSectionFixture(compilerClass: typeof SectionCompiler, filena
return compiler.compile(globalSections);
}
-export function compileKeyboard(inputFilename: string, callbacks: CompilerCallbacks, options: CompilerOptions): KMXPlusFile {
- const k = new Compiler(callbacks, options);
+export function compileKeyboard(inputFilename: string, options: CompilerOptions): KMXPlusFile {
+ const k = new Compiler(compilerTestCallbacks, options);
const source = k.load(inputFilename);
- checkMessages(callbacks);
+ checkMessages();
assert.isNotNull(source, 'k.load should not have returned null');
const valid = k.validate(source);
- checkMessages(callbacks);
+ checkMessages();
assert.isTrue(valid, 'k.validate should not have failed');
const kmx = k.compile(source);
- checkMessages(callbacks);
+ checkMessages();
assert.isNotNull(kmx, 'k.compile should not have returned null');
// In order for the KMX file to be loaded by non-KMXPlus components, it is helpful
@@ -78,9 +94,9 @@ export function compileKeyboard(inputFilename: string, callbacks: CompilerCallba
return kmx;
}
-export function checkMessages(callbacks: CompilerCallbacks) {
- if(callbacks.messages.length > 0) {
- console.log(callbacks.messages);
+export function checkMessages() {
+ if(compilerTestCallbacks.messages.length > 0) {
+ console.log(compilerTestCallbacks.messages);
}
- assert.isEmpty(callbacks.messages);
+ assert.isEmpty(compilerTestCallbacks.messages);
}
diff --git a/developer/src/kmc-keyboard/test/kvk/test-kvk-file.ts b/developer/src/kmc-keyboard/test/kvk/test-kvk-file.ts
new file mode 100644
index 00000000000..7578169eea7
--- /dev/null
+++ b/developer/src/kmc-keyboard/test/kvk/test-kvk-file.ts
@@ -0,0 +1,15 @@
+import * as fs from 'fs';
+import 'mocha';
+import { makePathToFixture } from '../helpers/index.js';
+import KvkFileReader from "../../src/kvk/kvk-file-reader.js";
+import { verify_khmer_angkor } from './test-kvk-utils.js';
+
+describe('kvk-file-reader', function () {
+ it('kvk-file-reader should read a valid file', function() {
+ const path = makePathToFixture('kvk', 'khmer_angkor.kvk');
+ const input = fs.readFileSync(path);
+ const reader = new KvkFileReader();
+ const vk = reader.read(input);
+ verify_khmer_angkor(vk);
+ });
+});
diff --git a/developer/src/kmc-keyboard/test/kvk/test-kvk-round-trip.ts b/developer/src/kmc-keyboard/test/kvk/test-kvk-round-trip.ts
new file mode 100644
index 00000000000..a03b34848c6
--- /dev/null
+++ b/developer/src/kmc-keyboard/test/kvk/test-kvk-round-trip.ts
@@ -0,0 +1,33 @@
+import * as fs from 'fs';
+import 'mocha';
+import {assert} from 'chai';
+import { compilerTestCallbacks, makePathToFixture } from '../helpers/index.js';
+import KvksFileReader from "../../src/kvk/kvks-file-reader.js";
+import KvkFileReader from "../../src/kvk/kvk-file-reader.js";
+import KvkFileWriter from "../../src/kvk/kvk-file-writer.js";
+
+describe('kvk-file-reader', function () {
+ it('kvk-file-reader should round-trip with kvk-file-writer', function() {
+ const path = makePathToFixture('kvk', 'khmer_angkor.kvk');
+ const input = fs.readFileSync(path);
+ const reader = new KvkFileReader();
+ const vk = reader.read(input);
+ const writer = new KvkFileWriter();
+ const output = writer.write(vk);
+ assert.deepEqual(input, output);
+ });
+});
+
+describe('kvks-file-reader', function () {
+ it('kvks-file-reader should compile with kvk-file-writer', function() {
+ const inputPath = makePathToFixture('kvk', 'khmer_angkor.kvks');
+ const compiledPath = makePathToFixture('kvk', 'khmer_angkor.kvk');
+ const input = fs.readFileSync(inputPath);
+ const compiled = fs.readFileSync(compiledPath);
+ const reader = new KvksFileReader(compilerTestCallbacks);
+ const vk = reader.read(input);
+ const writer = new KvkFileWriter();
+ const output = writer.write(vk);
+ assert.deepEqual(output, compiled);
+ });
+});
diff --git a/developer/src/kmc-keyboard/test/kvk/test-kvk-utils.ts b/developer/src/kmc-keyboard/test/kvk/test-kvk-utils.ts
new file mode 100644
index 00000000000..99e5c05fc77
--- /dev/null
+++ b/developer/src/kmc-keyboard/test/kvk/test-kvk-utils.ts
@@ -0,0 +1,24 @@
+import 'mocha';
+import {assert} from 'chai';
+import { VisualKeyboard, VisualKeyboardHeaderFlags, VisualKeyboardKeyFlags, VisualKeyboardShiftState } from "./visual-keyboard.js";
+import { USVirtualKeyCodes } from '../../src/ldml-keyboard/virtual-key-constants.js';
+
+export function verify_khmer_angkor(vk: VisualKeyboard) {
+ assert.equal(vk.header.flags, VisualKeyboardHeaderFlags.kvkhAltGr);
+ assert.equal(vk.header.associatedKeyboard, 'khmer_angkor');
+ assert.equal(vk.header.ansiFont.name, 'Arial');
+ assert.equal(vk.header.ansiFont.size, -12);
+ assert.equal(vk.header.ansiFont.color, 0xFF000008);
+ assert.equal(vk.header.unicodeFont.name, 'Khmer Busra Kbd');
+ assert.equal(vk.header.unicodeFont.size, 16);
+ assert.equal(vk.header.unicodeFont.color, 0xFF000008);
+ assert.equal(vk.keys.length, 186);
+ assert.equal(vk.keys[0].flags, VisualKeyboardKeyFlags.kvkkUnicode);
+ assert.equal(vk.keys[0].vkey, USVirtualKeyCodes.K_B);
+ assert.equal(vk.keys[0].shift, VisualKeyboardShiftState.KVKS_RALT);
+ assert.equal(vk.keys[0].text, 'ឞ');
+ assert.equal(vk.keys[185].flags, VisualKeyboardKeyFlags.kvkkUnicode);
+ assert.equal(vk.keys[185].vkey, USVirtualKeyCodes.K_COMMA);
+ assert.equal(vk.keys[185].shift, VisualKeyboardShiftState.KVKS_SHIFT);
+ assert.equal(vk.keys[185].text, '');
+}
diff --git a/developer/src/kmc-keyboard/test/kvk/test-kvks-file.ts b/developer/src/kmc-keyboard/test/kvk/test-kvks-file.ts
new file mode 100644
index 00000000000..15f79f6e15d
--- /dev/null
+++ b/developer/src/kmc-keyboard/test/kvk/test-kvks-file.ts
@@ -0,0 +1,15 @@
+import * as fs from 'fs';
+import 'mocha';
+import { compilerTestCallbacks, makePathToFixture } from '../helpers/index.js';
+import KvksFileReader from "../../src/kvk/kvks-file-reader.js";
+import { verify_khmer_angkor } from './test-kvk-utils.js';
+
+describe('kvks-file-reader', function() {
+ it('kvks-file-reader should read a valid file', function() {
+ const path = makePathToFixture('kvk', 'khmer_angkor.kvks');
+ const input = fs.readFileSync(path);
+ const reader = new KvksFileReader(compilerTestCallbacks);
+ const vk = reader.read(input);
+ verify_khmer_angkor(vk);
+ });
+});
diff --git a/developer/src/kmc-keyboard/test/ldml-keyboard/test-ldml-keyboard-xml-reader.ts b/developer/src/kmc-keyboard/test/ldml-keyboard/test-ldml-keyboard-xml-reader.ts
index ecd04762baf..2a287273458 100644
--- a/developer/src/kmc-keyboard/test/ldml-keyboard/test-ldml-keyboard-xml-reader.ts
+++ b/developer/src/kmc-keyboard/test/ldml-keyboard/test-ldml-keyboard-xml-reader.ts
@@ -1,6 +1,6 @@
import 'mocha';
import {assert} from 'chai';
-import {CompilerCallbacks, makePathToFixture} from '../helpers/index.js';
+import {compilerTestCallbacks, makePathToFixture} from '../helpers/index.js';
import LDMLKeyboardXMLSourceFileReader from '../../src/ldml-keyboard/ldml-keyboard-xml-reader.js';
import { CompilerMessages } from '../../src/compiler/messages.js';
@@ -9,22 +9,20 @@ describe('ldml keyboard xml reader tests', function() {
it("should fail to load files that don't conform to DTD", function() {
const inputFilename = makePathToFixture('invalid-structure-per-dtd.xml');
- const callbacks = new CompilerCallbacks();
- let reader = new LDMLKeyboardXMLSourceFileReader(callbacks);
+ let reader = new LDMLKeyboardXMLSourceFileReader(compilerTestCallbacks);
const source = reader.loadFile(inputFilename);
assert.isNull(source);
- assert.equal(callbacks.messages.length, 1);
- assert.deepEqual(callbacks.messages[0], CompilerMessages.Error_InvalidFile({errorText: "data/keyboard must have required property 'names'"}));
+ assert.equal(compilerTestCallbacks.messages.length, 1);
+ assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_InvalidFile({errorText: "data/keyboard must have required property 'names'"}));
});
it("should fail to load files with an invalid conformsTo", function() {
const inputFilename = makePathToFixture('invalid-conforms-to.xml');
- const callbacks = new CompilerCallbacks();
- let reader = new LDMLKeyboardXMLSourceFileReader(callbacks);
+ let reader = new LDMLKeyboardXMLSourceFileReader(compilerTestCallbacks);
const source = reader.loadFile(inputFilename);
assert.isNull(source);
- assert.equal(callbacks.messages.length, 1);
- assert.deepEqual(callbacks.messages[0], CompilerMessages.Error_InvalidFile({errorText: "data/keyboard/conformsTo must be equal to one of the allowed values"}));
+ assert.equal(compilerTestCallbacks.messages.length, 1);
+ assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_InvalidFile({errorText: "data/keyboard/conformsTo must be equal to one of the allowed values"}));
});
});
\ No newline at end of file
diff --git a/developer/src/kmc-keyboard/test/test-bksp.ts b/developer/src/kmc-keyboard/test/test-bksp.ts
index d6b234f3a2f..05a16b7a0e1 100644
--- a/developer/src/kmc-keyboard/test/test-bksp.ts
+++ b/developer/src/kmc-keyboard/test/test-bksp.ts
@@ -1,7 +1,7 @@
import 'mocha';
import { assert } from 'chai';
import { BkspCompiler } from '../src/compiler/bksp.js';
-import { CompilerCallbacks, loadSectionFixture } from './helpers/index.js';
+import { compilerTestCallbacks, loadSectionFixture } from './helpers/index.js';
import { Bksp, BkspItemFlags } from '../src/kmx/kmx-plus.js';
//import { CompilerMessages } from './keyman/compiler/messages';
@@ -9,9 +9,8 @@ describe('bksp', function () {
this.slow(500); // 0.5 sec -- json schema validation takes a while
it('should compile minimal bksp data', function() {
- const callbacks = new CompilerCallbacks();
- let bksp = loadSectionFixture(BkspCompiler, 'sections/bksp/minimal.xml', callbacks) as Bksp;
- assert.lengthOf(callbacks.messages, 0);
+ let bksp = loadSectionFixture(BkspCompiler, 'sections/bksp/minimal.xml', compilerTestCallbacks) as Bksp;
+ assert.lengthOf(compilerTestCallbacks.messages, 0);
assert.lengthOf(bksp.items, 1);
assert.lengthOf(bksp.items[0].from, 2);
diff --git a/developer/src/kmc-keyboard/test/test-compiler-e2e.ts b/developer/src/kmc-keyboard/test/test-compiler-e2e.ts
index 91508ee986d..c3babba4c61 100644
--- a/developer/src/kmc-keyboard/test/test-compiler-e2e.ts
+++ b/developer/src/kmc-keyboard/test/test-compiler-e2e.ts
@@ -2,7 +2,7 @@ import 'mocha';
import {assert} from 'chai';
import x_hextobin from '@keymanapp/hextobin';
import KMXBuilder from '../src/kmx/kmx-builder.js';
-import {checkMessages, compileKeyboard, CompilerCallbacks, makePathToFixture} from './helpers/index.js';
+import {checkMessages, compileKeyboard, makePathToFixture} from './helpers/index.js';
// const __filename = fileURLToPath(import.meta.url);
const hextobin = (x_hextobin as any).default;
@@ -18,14 +18,13 @@ describe('compiler-tests', function() {
const binaryFilename = makePathToFixture('basic.txt');
// Compile the keyboard
- const callbacks = new CompilerCallbacks();
- const kmx = compileKeyboard(inputFilename, callbacks, {debug: true, addCompilerVersion: false});
+ const kmx = compileKeyboard(inputFilename, {debug: true, addCompilerVersion: false});
assert.isNotNull(kmx);
// Use the builder to generate the binary output file
const builder = new KMXBuilder(kmx, true);
const code = builder.compile();
- checkMessages(callbacks);
+ checkMessages();
assert.isNotNull(code);
// Compare output
diff --git a/developer/src/kmc-keyboard/test/test-finl.ts b/developer/src/kmc-keyboard/test/test-finl.ts
index ad4749e5746..fb63a28100c 100644
--- a/developer/src/kmc-keyboard/test/test-finl.ts
+++ b/developer/src/kmc-keyboard/test/test-finl.ts
@@ -1,7 +1,7 @@
import 'mocha';
import { assert } from 'chai';
import { FinlCompiler } from '../src/compiler/tran.js';
-import { CompilerCallbacks, loadSectionFixture } from './helpers/index.js';
+import { compilerTestCallbacks, loadSectionFixture } from './helpers/index.js';
import { Finl, FinlItemFlags } from '../src/kmx/kmx-plus.js';
//import { CompilerMessages } from './keyman/compiler/messages';
@@ -9,9 +9,8 @@ describe('finl', function () {
this.slow(500); // 0.5 sec -- json schema validation takes a while
it('should compile minimal finl data', function() {
- const callbacks = new CompilerCallbacks();
- let finl = loadSectionFixture(FinlCompiler, 'sections/finl/minimal.xml', callbacks) as Finl;
- assert.lengthOf(callbacks.messages, 0);
+ let finl = loadSectionFixture(FinlCompiler, 'sections/finl/minimal.xml', compilerTestCallbacks) as Finl;
+ assert.lengthOf(compilerTestCallbacks.messages, 0);
assert.lengthOf(finl.items, 1);
assert.lengthOf(finl.items[0].from, 2);
diff --git a/developer/src/kmc-keyboard/test/test-keys.ts b/developer/src/kmc-keyboard/test/test-keys.ts
index 90d8c06a414..ae40eb051b9 100644
--- a/developer/src/kmc-keyboard/test/test-keys.ts
+++ b/developer/src/kmc-keyboard/test/test-keys.ts
@@ -1,7 +1,7 @@
import 'mocha';
import { assert } from 'chai';
import { KeysCompiler } from '../src/compiler/keys.js';
-import { CompilerCallbacks, loadSectionFixture } from './helpers/index.js';
+import { compilerTestCallbacks, loadSectionFixture } from './helpers/index.js';
import { Keys } from '../src/kmx/kmx-plus.js';
import { CompilerMessages } from '../src/compiler/messages.js';
@@ -9,54 +9,48 @@ describe('keys', function () {
this.slow(500); // 0.5 sec -- json schema validation takes a while
it('should compile minimal keys data', function() {
- const callbacks = new CompilerCallbacks();
- let keys = loadSectionFixture(KeysCompiler, 'sections/keys/minimal.xml', callbacks) as Keys;
+ let keys = loadSectionFixture(KeysCompiler, 'sections/keys/minimal.xml', compilerTestCallbacks) as Keys;
assert.isNotNull(keys);
- assert.equal(callbacks.messages.length, 0);
+ assert.equal(compilerTestCallbacks.messages.length, 0);
assert.equal(keys.keys.length, 1);
});
it('should compile a hardware layer', function() {
- const callbacks = new CompilerCallbacks();
- let keys = loadSectionFixture(KeysCompiler, 'sections/keys/hardware.xml', callbacks) as Keys;
+ let keys = loadSectionFixture(KeysCompiler, 'sections/keys/hardware.xml', compilerTestCallbacks) as Keys;
assert.isNotNull(keys);
- assert.equal(callbacks.messages.length, 0);
+ assert.equal(compilerTestCallbacks.messages.length, 0);
assert.equal(keys.keys.length, 2);
});
it('should reject structurally invalid layers', function() {
- const callbacks = new CompilerCallbacks();
- let keys = loadSectionFixture(KeysCompiler, 'sections/keys/invalid-missing-layer.xml', callbacks) as Keys;
+ let keys = loadSectionFixture(KeysCompiler, 'sections/keys/invalid-missing-layer.xml', compilerTestCallbacks) as Keys;
assert.isNull(keys);
- assert.equal(callbacks.messages.length, 1);
+ assert.equal(compilerTestCallbacks.messages.length, 1);
- assert.deepEqual(callbacks.messages[0], CompilerMessages.Error_MustBeAtLeastOneLayerElement());
+ assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_MustBeAtLeastOneLayerElement());
});
it('should reject layouts with too many hardware rows', function() {
- const callbacks = new CompilerCallbacks();
- let keys = loadSectionFixture(KeysCompiler, 'sections/keys/invalid-hardware-too-many-rows.xml', callbacks) as Keys;
+ let keys = loadSectionFixture(KeysCompiler, 'sections/keys/invalid-hardware-too-many-rows.xml', compilerTestCallbacks) as Keys;
assert.isNull(keys);
- assert.equal(callbacks.messages.length, 1);
+ assert.equal(compilerTestCallbacks.messages.length, 1);
- assert.deepEqual(callbacks.messages[0], CompilerMessages.Error_HardwareLayerHasTooManyRows());
+ assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_HardwareLayerHasTooManyRows());
});
it('should reject layouts with too many hardware keys', function() {
- const callbacks = new CompilerCallbacks();
- let keys = loadSectionFixture(KeysCompiler, 'sections/keys/invalid-hardware-too-many-keys.xml', callbacks) as Keys;
+ let keys = loadSectionFixture(KeysCompiler, 'sections/keys/invalid-hardware-too-many-keys.xml', compilerTestCallbacks) as Keys;
assert.isNull(keys);
- assert.equal(callbacks.messages.length, 1);
+ assert.equal(compilerTestCallbacks.messages.length, 1);
- assert.deepEqual(callbacks.messages[0], CompilerMessages.Error_RowOnHardwareLayerHasTooManyKeys({row: 1}));
+ assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_RowOnHardwareLayerHasTooManyKeys({row: 1}));
});
it('should reject layouts with undefined keys', function() {
- const callbacks = new CompilerCallbacks();
- let keys = loadSectionFixture(KeysCompiler, 'sections/keys/invalid-undefined-key.xml', callbacks) as Keys;
+ let keys = loadSectionFixture(KeysCompiler, 'sections/keys/invalid-undefined-key.xml', compilerTestCallbacks) as Keys;
assert.isNull(keys);
- assert.equal(callbacks.messages.length, 1);
+ assert.equal(compilerTestCallbacks.messages.length, 1);
- assert.deepEqual(callbacks.messages[0], CompilerMessages.Error_KeyNotFoundInKeyBag({col: 1, form: 'hardware', keyId: 'foo', layer: 'base', row: 1}));
+ assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_KeyNotFoundInKeyBag({col: 1, form: 'hardware', keyId: 'foo', layer: 'base', row: 1}));
});
});
diff --git a/developer/src/kmc-keyboard/test/test-loca.ts b/developer/src/kmc-keyboard/test/test-loca.ts
index 5baa3347189..d1f259427bd 100644
--- a/developer/src/kmc-keyboard/test/test-loca.ts
+++ b/developer/src/kmc-keyboard/test/test-loca.ts
@@ -1,7 +1,7 @@
import 'mocha';
import { assert } from 'chai';
import { LocaCompiler } from '../src/compiler/loca.js';
-import { CompilerCallbacks, loadSectionFixture } from './helpers/index.js';
+import { compilerTestCallbacks, loadSectionFixture } from './helpers/index.js';
import { Loca } from '../src/kmx/kmx-plus.js';
import { CompilerMessages } from '../src/compiler/messages.js';
@@ -9,27 +9,25 @@ describe('loca', function () {
this.slow(500); // 0.5 sec -- json schema validation takes a while
it('should compile minimal loca data', function() {
- const callbacks = new CompilerCallbacks();
- let loca = loadSectionFixture(LocaCompiler, 'sections/loca/minimal.xml', callbacks) as Loca;
+ let loca = loadSectionFixture(LocaCompiler, 'sections/loca/minimal.xml', compilerTestCallbacks) as Loca;
assert.isObject(loca);
- assert.equal(callbacks.messages.length, 0);
+ assert.equal(compilerTestCallbacks.messages.length, 0);
assert.equal(loca.locales.length, 1);
assert.equal(loca.locales[0].value.toLowerCase(), 'mt');
});
it('should compile multiple locales', function() {
- const callbacks = new CompilerCallbacks();
- let loca = loadSectionFixture(LocaCompiler, 'sections/loca/multiple.xml', callbacks) as Loca;
+ let loca = loadSectionFixture(LocaCompiler, 'sections/loca/multiple.xml', compilerTestCallbacks) as Loca;
assert.isObject(loca);
// Note: multiple.xml includes fr-FR twice, with differing case, which should be canonicalized
- assert.equal(callbacks.messages.length, 4);
- assert.deepEqual(callbacks.messages[0], CompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'fr-FR', locale: 'fr'}));
- assert.deepEqual(callbacks.messages[1], CompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'km-khmr-kh', locale: 'km'}));
- assert.deepEqual(callbacks.messages[2], CompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'fr-fr', locale: 'fr'}));
- assert.deepEqual(callbacks.messages[3], CompilerMessages.Hint_OneOrMoreRepeatedLocales());
+ assert.equal(compilerTestCallbacks.messages.length, 4);
+ assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'fr-FR', locale: 'fr'}));
+ assert.deepEqual(compilerTestCallbacks.messages[1], CompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'km-khmr-kh', locale: 'km'}));
+ assert.deepEqual(compilerTestCallbacks.messages[2], CompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'fr-fr', locale: 'fr'}));
+ assert.deepEqual(compilerTestCallbacks.messages[3], CompilerMessages.Hint_OneOrMoreRepeatedLocales());
// Original is 6 locales, now five minimized in the results
assert.equal(loca.locales.length, 5);
@@ -41,12 +39,11 @@ describe('loca', function () {
});
it('should reject structurally invalid locales', function() {
- const callbacks = new CompilerCallbacks();
- let loca = loadSectionFixture(LocaCompiler, 'sections/loca/invalid-locale.xml', callbacks) as Loca;
+ let loca = loadSectionFixture(LocaCompiler, 'sections/loca/invalid-locale.xml', compilerTestCallbacks) as Loca;
assert.isNull(loca);
- assert.equal(callbacks.messages.length, 1);
+ 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.
// Furthermore, we are testing BCP 47 structure, not the validity of each subtag -- we must assume the author knows of new subtags!
- assert.deepEqual(callbacks.messages[0], CompilerMessages.Error_InvalidLocale({tag:'en-*'}));
+ assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_InvalidLocale({tag:'en-*'}));
})
});
diff --git a/developer/src/kmc-keyboard/test/test-meta.ts b/developer/src/kmc-keyboard/test/test-meta.ts
index ffbc0c1dab7..d2417a0458f 100644
--- a/developer/src/kmc-keyboard/test/test-meta.ts
+++ b/developer/src/kmc-keyboard/test/test-meta.ts
@@ -1,7 +1,7 @@
import 'mocha';
import {assert} from 'chai';
import { MetaCompiler } from '../src/compiler/meta.js';
-import { CompilerCallbacks, loadSectionFixture } from './helpers/index.js';
+import { compilerTestCallbacks, loadSectionFixture } from './helpers/index.js';
import { KeyboardSettings, Meta } from '../src/kmx/kmx-plus.js';
import { CompilerMessages } from '../src/compiler/messages.js';
@@ -9,9 +9,8 @@ describe('meta', function () {
this.slow(500); // 0.5 sec -- json schema validation takes a while
it('should compile minimal metadata', function() {
- const callbacks = new CompilerCallbacks();
- let meta = loadSectionFixture(MetaCompiler, 'sections/meta/minimal.xml', callbacks) as Meta;
- assert.equal(callbacks.messages.length, 0);
+ let meta = loadSectionFixture(MetaCompiler, 'sections/meta/minimal.xml', compilerTestCallbacks) as Meta;
+ assert.equal(compilerTestCallbacks.messages.length, 0);
assert.isEmpty(meta.author.value); // TODO-LDML: default author string "unknown"?
assert.equal(meta.conform.value, 'techpreview');
@@ -22,9 +21,8 @@ describe('meta', function () {
});
it('should compile maximal metadata', function() {
- const callbacks = new CompilerCallbacks();
- let meta = loadSectionFixture(MetaCompiler, 'sections/meta/maximal.xml', callbacks) as Meta;
- assert.equal(callbacks.messages.length, 0);
+ let meta = loadSectionFixture(MetaCompiler, 'sections/meta/maximal.xml', compilerTestCallbacks) as Meta;
+ assert.equal(compilerTestCallbacks.messages.length, 0);
assert.equal(meta.author.value, 'The Keyman Team');
assert.equal(meta.conform.value, 'techpreview');
@@ -36,24 +34,22 @@ describe('meta', function () {
});
it('should reject invalid normalization', function() {
- const callbacks = new CompilerCallbacks();
- let meta = loadSectionFixture(MetaCompiler, 'sections/meta/invalid-normalization.xml', callbacks) as Meta;
+ let meta = loadSectionFixture(MetaCompiler, 'sections/meta/invalid-normalization.xml', compilerTestCallbacks) as Meta;
assert.isNull(meta);
- assert.equal(callbacks.messages.length, 1);
- assert.deepEqual(callbacks.messages[0], CompilerMessages.Error_InvalidNormalization({form:'NFQ'}));
+ assert.equal(compilerTestCallbacks.messages.length, 1);
+ assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_InvalidNormalization({form:'NFQ'}));
});
it('should reject invalid version', function() {
- const callbacks = new CompilerCallbacks();
- let meta = loadSectionFixture(MetaCompiler, 'sections/meta/invalid-version-1.0.xml', callbacks) as Meta;
+ let meta = loadSectionFixture(MetaCompiler, 'sections/meta/invalid-version-1.0.xml', compilerTestCallbacks) as Meta;
assert.isNull(meta);
- assert.equal(callbacks.messages.length, 1);
- assert.deepEqual(callbacks.messages[0], CompilerMessages.Error_InvalidVersion({version:'1.0'}));
+ assert.equal(compilerTestCallbacks.messages.length, 1);
+ assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_InvalidVersion({version:'1.0'}));
- meta = loadSectionFixture(MetaCompiler, 'sections/meta/invalid-version-v1.0.3.xml', callbacks) as Meta;
+ meta = loadSectionFixture(MetaCompiler, 'sections/meta/invalid-version-v1.0.3.xml', compilerTestCallbacks) as Meta;
assert.isNull(meta);
- assert.equal(callbacks.messages.length, 1);
- assert.deepEqual(callbacks.messages[0], CompilerMessages.Error_InvalidVersion({version:'v1.0.3'}));
+ assert.equal(compilerTestCallbacks.messages.length, 1);
+ assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_InvalidVersion({version:'v1.0.3'}));
});
});
diff --git a/developer/src/kmc-keyboard/test/test-metadata-compiler.ts b/developer/src/kmc-keyboard/test/test-metadata-compiler.ts
index 5cfb9678e38..55a3df83344 100644
--- a/developer/src/kmc-keyboard/test/test-metadata-compiler.ts
+++ b/developer/src/kmc-keyboard/test/test-metadata-compiler.ts
@@ -1,6 +1,6 @@
import 'mocha';
import {assert} from 'chai';
-import { checkMessages, compileKeyboard, CompilerCallbacks, makePathToFixture } from './helpers/index.js';
+import { checkMessages, compileKeyboard, makePathToFixture } from './helpers/index.js';
import KMXFile from '../src/kmx/kmx.js';
import KEYMAN_VERSION from '@keymanapp/keyman-version/keyman-version.mjs';
@@ -8,13 +8,11 @@ describe('kmx metadata compiler', function () {
this.slow(500); // 0.5 sec -- json schema validation takes a while
it('should compile metadata with debug and compiler version', function() {
- debugger
- const callbacks = new CompilerCallbacks();
const inputFilename = makePathToFixture('basic.xml');
// Compile the keyboard
- const kmx = compileKeyboard(inputFilename, callbacks, {debug:true, addCompilerVersion:true});
- checkMessages(callbacks);
+ const kmx = compileKeyboard(inputFilename, {debug:true, addCompilerVersion:true});
+ checkMessages();
assert.isNotNull(kmx);
// Order of stores is not significant in kmx spec, but kmxplus compiler will
@@ -45,12 +43,11 @@ describe('kmx metadata compiler', function () {
});
it('should compile metadata with no compiler version', function() {
- const callbacks = new CompilerCallbacks();
const inputFilename = makePathToFixture('basic.xml');
// Compile the keyboard
- const kmx = compileKeyboard(inputFilename, callbacks, {debug:true, addCompilerVersion:false});
- checkMessages(callbacks);
+ const kmx = compileKeyboard(inputFilename, {debug:true, addCompilerVersion:false});
+ checkMessages();
assert.isNotNull(kmx);
// TSS_NAME = 7
diff --git a/developer/src/kmc-keyboard/test/test-name.ts b/developer/src/kmc-keyboard/test/test-name.ts
index e3c80b0022a..f1c1a780a45 100644
--- a/developer/src/kmc-keyboard/test/test-name.ts
+++ b/developer/src/kmc-keyboard/test/test-name.ts
@@ -1,7 +1,7 @@
import 'mocha';
import { assert } from 'chai';
import { NameCompiler } from '../src/compiler/name.js';
-import { CompilerCallbacks, loadSectionFixture } from './helpers/index.js';
+import { compilerTestCallbacks, loadSectionFixture } from './helpers/index.js';
import { Name } from '../src/kmx/kmx-plus.js';
//import { CompilerMessages } from './keyman/compiler/messages';
@@ -9,18 +9,16 @@ describe('name', function () {
this.slow(500); // 0.5 sec -- json schema validation takes a while
it('should compile minimal name data', function() {
- const callbacks = new CompilerCallbacks();
- let name = loadSectionFixture(NameCompiler, 'sections/name/minimal.xml', callbacks) as Name;
- assert.equal(callbacks.messages.length, 0);
+ let name = loadSectionFixture(NameCompiler, 'sections/name/minimal.xml', compilerTestCallbacks) as Name;
+ assert.equal(compilerTestCallbacks.messages.length, 0);
assert.equal(name.names.length, 1);
assert.equal(name.names[0].value, 'My First Keyboard');
});
it('should compile multiple names', function() {
- const callbacks = new CompilerCallbacks();
- let name = loadSectionFixture(NameCompiler, 'sections/name/multiple.xml', callbacks) as Name;
- assert.equal(callbacks.messages.length, 0);
+ let name = loadSectionFixture(NameCompiler, 'sections/name/multiple.xml', compilerTestCallbacks) as Name;
+ assert.equal(compilerTestCallbacks.messages.length, 0);
assert.equal(name.names.length, 5);
assert.equal(name.names[0].value, 'My Second Keyboard');
diff --git a/developer/src/kmc-keyboard/test/test-ordr.ts b/developer/src/kmc-keyboard/test/test-ordr.ts
index 9e54d06e4bd..ff7bd34d402 100644
--- a/developer/src/kmc-keyboard/test/test-ordr.ts
+++ b/developer/src/kmc-keyboard/test/test-ordr.ts
@@ -1,7 +1,7 @@
import 'mocha';
import { assert } from 'chai';
import { OrdrCompiler } from '../src/compiler/ordr.js';
-import { CompilerCallbacks, loadSectionFixture } from './helpers/index.js';
+import { compilerTestCallbacks, loadSectionFixture } from './helpers/index.js';
import { Ordr } from '../src/kmx/kmx-plus.js';
//import { CompilerMessages } from './keyman/compiler/messages';
@@ -9,9 +9,8 @@ describe('ordr', function () {
this.slow(500); // 0.5 sec -- json schema validation takes a while
it('should compile minimal ordr data', function() {
- const callbacks = new CompilerCallbacks();
- let ordr = loadSectionFixture(OrdrCompiler, 'sections/ordr/minimal.xml', callbacks) as Ordr;
- assert.lengthOf(callbacks.messages, 0);
+ let ordr = loadSectionFixture(OrdrCompiler, 'sections/ordr/minimal.xml', compilerTestCallbacks) as Ordr;
+ assert.lengthOf(compilerTestCallbacks.messages, 0);
assert.lengthOf(ordr.items, 1);
assert.lengthOf(ordr.items[0].elements, 4);
diff --git a/developer/src/kmc-keyboard/test/test-tran.ts b/developer/src/kmc-keyboard/test/test-tran.ts
index 5004d5e876c..7e617711058 100644
--- a/developer/src/kmc-keyboard/test/test-tran.ts
+++ b/developer/src/kmc-keyboard/test/test-tran.ts
@@ -1,7 +1,7 @@
import 'mocha';
import { assert } from 'chai';
import { TranCompiler } from '../src/compiler/tran.js';
-import { CompilerCallbacks, loadSectionFixture } from './helpers/index.js';
+import { compilerTestCallbacks, loadSectionFixture } from './helpers/index.js';
import { Tran, TranItemFlags } from '../src/kmx/kmx-plus.js';
//import { CompilerMessages } from './keyman/compiler/messages';
@@ -9,9 +9,8 @@ describe('tran', function () {
this.slow(500); // 0.5 sec -- json schema validation takes a while
it('should compile minimal tran data', function() {
- const callbacks = new CompilerCallbacks();
- let tran = loadSectionFixture(TranCompiler, 'sections/tran/minimal.xml', callbacks) as Tran;
- assert.lengthOf(callbacks.messages, 0);
+ let tran = loadSectionFixture(TranCompiler, 'sections/tran/minimal.xml', compilerTestCallbacks) as Tran;
+ assert.lengthOf(compilerTestCallbacks.messages, 0);
assert.lengthOf(tran.items, 1);
assert.lengthOf(tran.items[0].from, 2);
diff --git a/developer/src/kmc-keyboard/test/test-vkey.ts b/developer/src/kmc-keyboard/test/test-vkey.ts
index 9c8dd49ffa2..0812b49648c 100644
--- a/developer/src/kmc-keyboard/test/test-vkey.ts
+++ b/developer/src/kmc-keyboard/test/test-vkey.ts
@@ -1,7 +1,7 @@
import 'mocha';
import { assert } from 'chai';
import { VkeyCompiler } from '../src/compiler/vkey.js';
-import { CompilerCallbacks, loadSectionFixture } from './helpers/index.js';
+import { compilerTestCallbacks, loadSectionFixture } from './helpers/index.js';
import { Vkey } from '../src/kmx/kmx-plus.js';
import { CompilerMessages } from '../src/compiler/messages.js';
import { USVirtualKeyCodes } from '../src/ldml-keyboard/virtual-key-constants.js';
@@ -10,9 +10,8 @@ describe('vkey compiler', function () {
this.slow(500); // 0.5 sec -- json schema validation takes a while
it('should compile minimal vkey data', function() {
- const callbacks = new CompilerCallbacks();
- let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/minimal.xml', callbacks) as Vkey;
- assert.equal(callbacks.messages.length, 0);
+ let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/minimal.xml', compilerTestCallbacks) as Vkey;
+ assert.equal(compilerTestCallbacks.messages.length, 0);
assert.equal(vkey.vkeys.length, 4);
// Note, final order is sorted by `vkey` member
@@ -23,44 +22,39 @@ describe('vkey compiler', function () {
});
it('should hint on redundant data', function() {
- const callbacks = new CompilerCallbacks();
- let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/redundant.xml', callbacks) as Vkey;
+ let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/redundant.xml', compilerTestCallbacks) as Vkey;
assert.isNotNull(vkey);
- assert.equal(callbacks.messages.length, 1);
- assert.deepEqual(callbacks.messages[0], CompilerMessages.Hint_VkeyMapIsRedundant({vkey: 'A'}));
+ assert.equal(compilerTestCallbacks.messages.length, 1);
+ assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Hint_VkeyMapIsRedundant({vkey: 'A'}));
});
it('should report an info message if same target found', function() {
- const callbacks = new CompilerCallbacks();
- let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/same-target.xml', callbacks) as Vkey;
+ let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/same-target.xml', compilerTestCallbacks) as Vkey;
assert.isNotNull(vkey);
- assert.equal(callbacks.messages.length, 1);
- assert.deepEqual(callbacks.messages[0], CompilerMessages.Info_MultipleVkeyMapsHaveSameTarget({vkey: 'Q'}));
+ assert.equal(compilerTestCallbacks.messages.length, 1);
+ assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Info_MultipleVkeyMapsHaveSameTarget({vkey: 'Q'}));
});
it('should error on invalid "from" vkey', function() {
- const callbacks = new CompilerCallbacks();
- let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/invalid-from-vkey.xml', callbacks) as Vkey;
+ let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/invalid-from-vkey.xml', compilerTestCallbacks) as Vkey;
assert.isNull(vkey);
- assert.equal(callbacks.messages.length, 2);
- assert.deepEqual(callbacks.messages[0], CompilerMessages.Error_VkeyIsNotValid({vkey: 'q'}));
- assert.deepEqual(callbacks.messages[1], CompilerMessages.Error_VkeyIsNotValid({vkey: 'HYFEN'}));
+ assert.equal(compilerTestCallbacks.messages.length, 2);
+ assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_VkeyIsNotValid({vkey: 'q'}));
+ assert.deepEqual(compilerTestCallbacks.messages[1], CompilerMessages.Error_VkeyIsNotValid({vkey: 'HYFEN'}));
});
it('should error on invalid "to" vkey', function() {
- const callbacks = new CompilerCallbacks();
- let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/invalid-to-vkey.xml', callbacks) as Vkey;
+ let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/invalid-to-vkey.xml', compilerTestCallbacks) as Vkey;
assert.isNull(vkey);
- assert.equal(callbacks.messages.length, 1);
- assert.deepEqual(callbacks.messages[0], CompilerMessages.Error_VkeyIsNotValid({vkey: 'A-ACUTE'}));
+ assert.equal(compilerTestCallbacks.messages.length, 1);
+ assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_VkeyIsNotValid({vkey: 'A-ACUTE'}));
});
it('should error on repeated vkeys', function() {
- const callbacks = new CompilerCallbacks();
- let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/invalid-repeated-vkey.xml', callbacks) as Vkey;
+ let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/invalid-repeated-vkey.xml', compilerTestCallbacks) as Vkey;
assert.isNull(vkey);
- assert.equal(callbacks.messages.length, 1);
- assert.deepEqual(callbacks.messages[0], CompilerMessages.Error_VkeyMapIsRepeated({vkey: 'A'}));
+ assert.equal(compilerTestCallbacks.messages.length, 1);
+ assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_VkeyMapIsRepeated({vkey: 'A'}));
});
});
diff --git a/developer/src/kmc/src/kmc.ts b/developer/src/kmc/src/kmc.ts
index 9c2e8a8cf37..9606e15b829 100644
--- a/developer/src/kmc/src/kmc.ts
+++ b/developer/src/kmc/src/kmc.ts
@@ -49,6 +49,10 @@ class CompilerCallbacks {
let schemaPath = new URL('ldml-keyboard.schema.json', import.meta.url);
return fs.readFileSync(schemaPath);
}
+ loadKvksJsonSchema(): Buffer {
+ let schemaPath = new URL('kvks.schema.json', import.meta.url);
+ return fs.readFileSync(schemaPath);
+ }
}
function compileKeyboard(inputFilename: string, options: kmc.CompilerOptions): Uint8Array {
diff --git a/package-lock.json b/package-lock.json
index d4a546c677e..8de1536f4d7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -338,9 +338,9 @@
"dependencies": {
"@keymanapp/keyman-version": "*",
"ajv": "^8.11.0",
- "restructure": "^3.0.0",
+ "restructure": "git+https://github.com/keymanapp/dependency-restructure.git#49d129cf0916d082a7278bb09296fb89cecfcc50",
"semver": "^7.3.7",
- "xml2js": "^0.4.19"
+ "xml2js": "git+https://github.com/keymanapp/dependency-node-xml2js#535fe732dc408d697e0f847c944cc45f0baf0829"
},
"devDependencies": {
"@types/chai": "^4.1.7",
@@ -6631,8 +6631,9 @@
},
"node_modules/restructure": {
"version": "3.0.0",
- "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.0.tgz",
- "integrity": "sha512-Xj8/MEIhhfj9X2rmD9iJ4Gga9EFqVlpMj3vfLnV2r/Mh5jRMryNV+6lWh9GdJtDBcBSPIqzRdfBQ3wDtNFv/uw=="
+ "resolved": "git+ssh://git@github.com/keymanapp/dependency-restructure.git#49d129cf0916d082a7278bb09296fb89cecfcc50",
+ "integrity": "sha512-2vBFOLc0+dkUb3Pnpl87o+B4SIc3MAm3P8PXT8YFkP51URFGsgTgm3gAnwTX2jXSEd/5eT9n7C733loG6OaVgw==",
+ "license": "MIT"
},
"node_modules/retry": {
"version": "0.12.0",
@@ -7718,8 +7719,9 @@
},
"node_modules/xml2js": {
"version": "0.4.23",
- "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
- "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
+ "resolved": "git+ssh://git@github.com/keymanapp/dependency-node-xml2js.git#535fe732dc408d697e0f847c944cc45f0baf0829",
+ "integrity": "sha512-5CS+yWxp0qg8zO7ng/iXrBZm2FXgpiJ+RJ3E0LzRxDXChESqzazRwzl8sXYe8/7je/NfwG4EXcClaRKUkcIvtQ==",
+ "license": "MIT",
"dependencies": {
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
@@ -8687,11 +8689,11 @@
"chai": "^4.3.4",
"chalk": "^2.4.2",
"mocha": "^8.4.0",
- "restructure": "^3.0.0",
+ "restructure": "git+https://github.com/keymanapp/dependency-restructure.git#49d129cf0916d082a7278bb09296fb89cecfcc50",
"semver": "^7.3.7",
"ts-node": "^9.1.1",
"typescript": "^4.5.4",
- "xml2js": "^0.4.19"
+ "xml2js": "git+https://github.com/keymanapp/dependency-node-xml2js#535fe732dc408d697e0f847c944cc45f0baf0829"
},
"dependencies": {
"@types/mocha": {
@@ -13419,9 +13421,9 @@
}
},
"restructure": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.0.tgz",
- "integrity": "sha512-Xj8/MEIhhfj9X2rmD9iJ4Gga9EFqVlpMj3vfLnV2r/Mh5jRMryNV+6lWh9GdJtDBcBSPIqzRdfBQ3wDtNFv/uw=="
+ "version": "git+ssh://git@github.com/keymanapp/dependency-restructure.git#49d129cf0916d082a7278bb09296fb89cecfcc50",
+ "integrity": "sha512-2vBFOLc0+dkUb3Pnpl87o+B4SIc3MAm3P8PXT8YFkP51URFGsgTgm3gAnwTX2jXSEd/5eT9n7C733loG6OaVgw==",
+ "from": "restructure@git+https://github.com/keymanapp/dependency-restructure.git#49d129cf0916d082a7278bb09296fb89cecfcc50"
},
"retry": {
"version": "0.12.0",
@@ -14241,9 +14243,9 @@
"requires": {}
},
"xml2js": {
- "version": "0.4.23",
- "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
- "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
+ "version": "git+ssh://git@github.com/keymanapp/dependency-node-xml2js.git#535fe732dc408d697e0f847c944cc45f0baf0829",
+ "integrity": "sha512-5CS+yWxp0qg8zO7ng/iXrBZm2FXgpiJ+RJ3E0LzRxDXChESqzazRwzl8sXYe8/7je/NfwG4EXcClaRKUkcIvtQ==",
+ "from": "xml2js@git+https://github.com/keymanapp/dependency-node-xml2js#535fe732dc408d697e0f847c944cc45f0baf0829",
"requires": {
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"