diff --git a/common/schemas/kvks/kvks.schema.json b/common/schemas/kvks/kvks.schema.json index fd58f56e38..357b28b602 100644 --- a/common/schemas/kvks/kvks.schema.json +++ b/common/schemas/kvks/kvks.schema.json @@ -75,18 +75,27 @@ "$ref": "#/definitions/kvk-layer" } }, - "name": { - "$ref": "#/definitions/kvk-encoding-name" - }, - "fontname": { - "type": "string" - }, - "fontsize": { - "type": "string" + "$": { + "type": "object", + "properties": { + "name": { + "$ref": "#/definitions/kvk-encoding-name" + }, + "fontname": { + "type": "string" + }, + "fontsize": { + "type": "string" + } + }, + "required": [ + "name" + ], + "additionalProperties": false } }, "required": [ - "name" + "$" ], "additionalProperties": false }, @@ -99,12 +108,21 @@ "$ref": "#/definitions/kvk-key" } }, - "shift": { - "$ref": "#/definitions/kvk-layer-shift" + "$": { + "type": "object", + "properties": { + "shift": { + "$ref": "#/definitions/kvk-layer-shift" + } + }, + "required": [ + "shift" + ], + "additionalProperties": false } }, "required": [ - "shift" + "$" ], "additionalProperties": false }, @@ -114,15 +132,24 @@ "bitmap": { "type": "string" }, - "vkey": { - "type": "string" + "$": { + "type": "object", + "properties": { + "vkey": { + "type": "string" + } + }, + "required": [ + "vkey" + ], + "additionalProperties": false }, "_": { "type": "string" } }, "required": [ - "vkey" + "$" ], "additionalProperties": false }, diff --git a/common/web/types/src/kvk/kvks-file-reader.ts b/common/web/types/src/kvk/kvks-file-reader.ts index 8a4810e6af..c6a696cf30 100644 --- a/common/web/types/src/kvk/kvks-file-reader.ts +++ b/common/web/types/src/kvk/kvks-file-reader.ts @@ -18,7 +18,7 @@ export default class KVKSFileReader { const parser = new xml2js.Parser({ explicitArray: false, - mergeAttrs: true, + mergeAttrs: false, includeWhiteChars: true, normalize: false, emptyTag: {} as any @@ -81,7 +81,8 @@ export default class KVKSFileReader { 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 + associatedKeyboard: source.visualkeyboard?.header?.kbdname, + underlyingLayout: source.visualkeyboard?.header?.layout, }, keys: [] }; @@ -100,19 +101,19 @@ export default class KVKSFileReader { } for(let encoding of source.visualkeyboard.encoding) { - let isUnicode = (encoding.name == 'unicode'), + let isUnicode = (encoding.$?.name == 'unicode'), font = isUnicode ? result.header.unicodeFont : result.header.ansiFont; - font.name = encoding.fontname; - font.size = parseInt(encoding.fontsize,10); + font.name = encoding.$?.fontname; + font.size = parseInt(encoding.$?.fontsize,10); for(let layer of encoding.layer) { - let shift = this.kvksShiftToKvkShift(layer.shift); + let shift = this.kvksShiftToKvkShift(layer.$?.shift); for(let sourceKey of layer.key) { - let vkey = (USVirtualKeyCodes as any)[sourceKey.vkey]; + let vkey = (USVirtualKeyCodes as any)[sourceKey.$?.vkey]; if(!vkey) { if(errors) { let e = new KVKSParseError(); e.type = KVKSParseErrorType.invalidVkey; - e.vkey = sourceKey.vkey; + e.vkey = sourceKey.$?.vkey; errors.push(e); } continue; diff --git a/common/web/types/src/kvk/kvks-file-writer.ts b/common/web/types/src/kvk/kvks-file-writer.ts new file mode 100644 index 0000000000..613c6caa51 --- /dev/null +++ b/common/web/types/src/kvk/kvks-file-writer.ts @@ -0,0 +1,113 @@ +import * as xml2js from 'xml2js'; +import KVKSourceFile, { KVKSEncoding, KVKSFlags, KVKSKey, KVKSLayer } from './kvks-file.js'; +import { VisualKeyboard, VisualKeyboardHeaderFlags, VisualKeyboardKeyFlags, VisualKeyboardLegalShiftStates, VisualKeyboardShiftState } from './visual-keyboard.js'; +import { USVirtualKeyCodes } from '../consts/virtual-key-constants.js'; + +export default class KVKSFileWriter { + public write(vk: VisualKeyboard): string { + + const builder = new xml2js.Builder({ + allowSurrogateChars: true, + attrkey: '$', + charkey: '_', + xmldec: { + version: '1.0', + encoding: 'UTF-8', + standalone: true + } + }) + + let flags: KVKSFlags = {}; + if(vk.header.flags & VisualKeyboardHeaderFlags.kvkhDisplayUnderlying) { + flags.displayunderlying = ''; + } + if(vk.header.flags & VisualKeyboardHeaderFlags.kvkh102) { + flags.key102 = ''; + } + if(vk.header.flags & VisualKeyboardHeaderFlags.kvkhAltGr) { + flags.usealtgr = ''; + } + if(vk.header.flags & VisualKeyboardHeaderFlags.kvkhUseUnderlying) { + flags.useunderlying = ''; + } + + + + let kvks: KVKSourceFile = { + visualkeyboard: { + header: { + version: '10.0', + kbdname: vk.header.associatedKeyboard, + flags: flags, + }, + encoding: [] + } + }; + + if(vk.header.underlyingLayout) kvks.visualkeyboard.header.layout = vk.header.underlyingLayout; + + let encodings: {ansi: {o: KVKSEncoding, l: {[name:string]:KVKSLayer}}, unicode: {o: KVKSEncoding, l: {[name:string]:KVKSLayer}}} = {ansi:null,unicode:null}; + + for(let key of vk.keys) { + const encoding = key.flags & VisualKeyboardKeyFlags.kvkkUnicode ? 'unicode' : 'ansi'; + const shift = this.kvkShiftToKvksShift(key.shift); + + if(!encodings[encoding]) { + encodings[encoding] = { + o: { + layer: [], + $: {name: encoding, + fontname: encoding == 'ansi' ? vk.header.ansiFont.name : vk.header.unicodeFont.name, + fontsize: (encoding == 'ansi' ? vk.header.ansiFont.size : vk.header.unicodeFont.size).toString(), + }, + }, + l: {} + }; + kvks.visualkeyboard.encoding.push(encodings[encoding].o); + } + let e = encodings[encoding]; + if(!e.l[shift]) { + e.l[shift] = { + key: [], + $: {shift: shift}, + }; + e.o.layer.push(e.l[shift]); + } + let l = e.l[shift]; + + // TODO-LDML: map + let vkeyName = ''; + for(let vkey of Object.keys(USVirtualKeyCodes)) { + if((USVirtualKeyCodes as any)[vkey] == key.vkey) { + vkeyName = vkey; + break; + } + } + + if(vkeyName == '') { + //TODO-LDML: warn + continue; + } + let k: KVKSKey = { + $: {vkey: vkeyName}, + _: key.text, + } + + l.key.push(k); + } + + // console.dir(kvks, {depth:8}); + let result = builder.buildObject(kvks); + return result; //Uint8Array.from(result); + } + + public kvkShiftToKvksShift(shift: VisualKeyboardShiftState): string { + // TODO-LDML(lowpri): make a map of this? + for(let state of VisualKeyboardLegalShiftStates) { + if(state.shift == shift) { + return state.name; + } + } + return ''; + } +} \ No newline at end of file diff --git a/common/web/types/src/kvk/kvks-file.ts b/common/web/types/src/kvk/kvks-file.ts index 46b860c59b..d3884d9a49 100644 --- a/common/web/types/src/kvk/kvks-file.ts +++ b/common/web/types/src/kvk/kvks-file.ts @@ -17,7 +17,6 @@ export interface KVKSHeader { version?: string; kbdname?: string; flags?: KVKSFlags; - keybitmap?: string; layout?: string; }; @@ -29,19 +28,25 @@ export interface KVKSFlags { }; export interface KVKSEncoding { - name?: string; - fontname?: string; - fontsize?: string; + $?: { + name?: string; + fontname?: string; + fontsize?: string; + }; layer?: KVKSLayer[]; }; export interface KVKSLayer { - shift?: string; + $?: { + shift?: string; + }; key?: KVKSKey[]; }; export interface KVKSKey { - vkey?: string; + $?: { + vkey?: string; + } bitmap?: string; _?: string; }; diff --git a/common/web/types/src/kvk/visual-keyboard.ts b/common/web/types/src/kvk/visual-keyboard.ts index e262d14366..44447f3e5c 100644 --- a/common/web/types/src/kvk/visual-keyboard.ts +++ b/common/web/types/src/kvk/visual-keyboard.ts @@ -19,6 +19,7 @@ export class VisualKeyboardHeader { associatedKeyboard?: string; ansiFont: VisualKeyboardFont; // generally unused unicodeFont: VisualKeyboardFont; + underlyingLayout?: string; }; export class VisualKeyboardFont { diff --git a/common/web/types/test/kvk/test-kvks-file.ts b/common/web/types/test/kvk/test-kvks-file.ts index e47b4378e6..e776a0a361 100644 --- a/common/web/types/test/kvk/test-kvks-file.ts +++ b/common/web/types/test/kvk/test-kvks-file.ts @@ -2,11 +2,12 @@ import * as fs from 'fs'; import 'mocha'; import { loadKvksJsonSchema, makePathToFixture } from '../helpers/index.js'; import KvksFileReader, { KVKSParseError } from "../../src/kvk/kvks-file-reader.js"; +import KvksFileWriter from "../../src/kvk/kvks-file-writer.js"; import { verify_khmer_angkor } from './test-kvk-utils.js'; import { assert } from 'chai'; describe('kvks-file-reader', function() { - it('kvks-file-reader should read a valid file', function() { + it('should read a valid file', function() { const path = makePathToFixture('kvk', 'khmer_angkor.kvks'); const input = fs.readFileSync(path); @@ -21,3 +22,24 @@ describe('kvks-file-reader', function() { verify_khmer_angkor(vk); }); }); + +describe('kvks-file-writer', function() { + it('should write a valid file', function() { + const path = makePathToFixture('kvk', 'khmer_angkor.kvks'); + const input = fs.readFileSync(path); + + const reader = new KvksFileReader(); + const kvksExpected = reader.read(input); + const errors: KVKSParseError[] = []; + const vk = reader.transform(kvksExpected, errors); + assert.isEmpty(errors); + + const writer = new KvksFileWriter(); + const output = writer.write(vk); + + // We compare the (re)loaded data, because there may be + // minor, irrelevant formatting differences in the emitted xml + const kvks = reader.read(Buffer.from(output, 'utf8')); + assert.deepEqual(kvks, kvksExpected); + }); +});