From 7e631c1a73530c98b3a6850c684953b0988cd054 Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Fri, 27 Sep 2024 14:09:42 -0500 Subject: [PATCH 1/8] feat(common): unified xml parser scaffolding Fixes: #12208 --- developer/src/common/web/utils/src/index.ts | 2 ++ developer/src/common/web/utils/src/xml-utils.ts | 11 +++++++++++ .../src/common/web/utils/test/test-xml-utils.ts | 13 +++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 developer/src/common/web/utils/src/xml-utils.ts create mode 100644 developer/src/common/web/utils/test/test-xml-utils.ts diff --git a/developer/src/common/web/utils/src/index.ts b/developer/src/common/web/utils/src/index.ts index 7ee5c507b80..4a748bc354a 100644 --- a/developer/src/common/web/utils/src/index.ts +++ b/developer/src/common/web/utils/src/index.ts @@ -45,3 +45,5 @@ export { defaultCompilerOptions, CompilerBaseOptions, CompilerCallbacks, Compile export { CommonTypesMessages } from './common-messages.js'; export * as xml2js from './deps/xml2js/xml2js.js'; + +export { KeymanXMLParser, KeymanXMLGenerator } from './xml-utils.js'; diff --git a/developer/src/common/web/utils/src/xml-utils.ts b/developer/src/common/web/utils/src/xml-utils.ts new file mode 100644 index 00000000000..fa71dcf9656 --- /dev/null +++ b/developer/src/common/web/utils/src/xml-utils.ts @@ -0,0 +1,11 @@ + +/** wrapper for XML parsing support */ +export class KeymanXMLParser { + +} + +/** wrapper for XML generation support */ +export class KeymanXMLGenerator { + +} + diff --git a/developer/src/common/web/utils/test/test-xml-utils.ts b/developer/src/common/web/utils/test/test-xml-utils.ts new file mode 100644 index 00000000000..756855e6b9f --- /dev/null +++ b/developer/src/common/web/utils/test/test-xml-utils.ts @@ -0,0 +1,13 @@ +import { assert } from 'chai'; +import 'mocha'; + +import { KeymanXMLParser, KeymanXMLGenerator } from '../src/xml-utils.js'; + +describe('XML Parser Test', () => { + it('null test', () => assert.ok(new KeymanXMLParser())); +}); + +describe('XML Generator Test', () => { + it('null test', () => assert.ok(new KeymanXMLGenerator())); +}); + From 609faf80475864aced1fbe4ee187082e0705b3aa Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Fri, 27 Sep 2024 16:52:21 -0500 Subject: [PATCH 2/8] feat(common): unified xml parser reader test Fixes: #12208 --- developer/src/common/web/utils/src/index.ts | 2 +- .../src/common/web/utils/src/xml-utils.ts | 94 +- .../utils/test/fixtures/xml/disp_maximal.xml | 16 + .../test/fixtures/xml/disp_maximal.xml.json | 35 + .../xml/error_invalid_package_file.kps | 32 + .../utils/test/fixtures/xml/k_020_fr-test.xml | 23 + .../test/fixtures/xml/k_020_fr-test.xml.json | 538 ++++++++ .../web/utils/test/fixtures/xml/k_020_fr.xml | 200 +++ .../utils/test/fixtures/xml/k_020_fr.xml.json | 509 +++++++ .../utils/test/fixtures/xml/khmer_angkor.kpj | 187 +++ .../test/fixtures/xml/khmer_angkor.kpj.json | 190 +++ .../utils/test/fixtures/xml/khmer_angkor.kvks | 206 +++ .../test/fixtures/xml/khmer_angkor.kvks.json | 1172 +++++++++++++++++ .../fixtures/xml/strs_invalid-illegal.xml | 58 + .../xml/strs_invalid-illegal.xml.json | 113 ++ .../utils/test/fixtures/xml/test_valid.kps | 46 + .../test/fixtures/xml/test_valid.kps.json | 64 + .../test/fixtures/xml/tran_fail-empty.xml | 19 + .../fixtures/xml/tran_fail-empty.xml.json | 28 + .../common/web/utils/test/test-xml-utils.ts | 109 +- 20 files changed, 3632 insertions(+), 9 deletions(-) create mode 100644 developer/src/common/web/utils/test/fixtures/xml/disp_maximal.xml create mode 100644 developer/src/common/web/utils/test/fixtures/xml/disp_maximal.xml.json create mode 100644 developer/src/common/web/utils/test/fixtures/xml/error_invalid_package_file.kps create mode 100644 developer/src/common/web/utils/test/fixtures/xml/k_020_fr-test.xml create mode 100644 developer/src/common/web/utils/test/fixtures/xml/k_020_fr-test.xml.json create mode 100644 developer/src/common/web/utils/test/fixtures/xml/k_020_fr.xml create mode 100644 developer/src/common/web/utils/test/fixtures/xml/k_020_fr.xml.json create mode 100644 developer/src/common/web/utils/test/fixtures/xml/khmer_angkor.kpj create mode 100644 developer/src/common/web/utils/test/fixtures/xml/khmer_angkor.kpj.json create mode 100644 developer/src/common/web/utils/test/fixtures/xml/khmer_angkor.kvks create mode 100644 developer/src/common/web/utils/test/fixtures/xml/khmer_angkor.kvks.json create mode 100644 developer/src/common/web/utils/test/fixtures/xml/strs_invalid-illegal.xml create mode 100644 developer/src/common/web/utils/test/fixtures/xml/strs_invalid-illegal.xml.json create mode 100644 developer/src/common/web/utils/test/fixtures/xml/test_valid.kps create mode 100644 developer/src/common/web/utils/test/fixtures/xml/test_valid.kps.json create mode 100644 developer/src/common/web/utils/test/fixtures/xml/tran_fail-empty.xml create mode 100644 developer/src/common/web/utils/test/fixtures/xml/tran_fail-empty.xml.json diff --git a/developer/src/common/web/utils/src/index.ts b/developer/src/common/web/utils/src/index.ts index 4a748bc354a..7b83f682dff 100644 --- a/developer/src/common/web/utils/src/index.ts +++ b/developer/src/common/web/utils/src/index.ts @@ -46,4 +46,4 @@ export { CommonTypesMessages } from './common-messages.js'; export * as xml2js from './deps/xml2js/xml2js.js'; -export { KeymanXMLParser, KeymanXMLGenerator } from './xml-utils.js'; +export { KeymanXMLOptions, KeymanXMLWriter, KeymanXMLReader } from './xml-utils.js'; diff --git a/developer/src/common/web/utils/src/xml-utils.ts b/developer/src/common/web/utils/src/xml-utils.ts index fa71dcf9656..319b607fe73 100644 --- a/developer/src/common/web/utils/src/xml-utils.ts +++ b/developer/src/common/web/utils/src/xml-utils.ts @@ -1,11 +1,99 @@ +import { xml2js } from "./index.js"; + +export class KeymanXMLOptions { + type: 'keyboard3' // LDML + | 'keyboard3-test' // LDML + | 'kps' // + | 'kvks' // + | 'kpj' // // + ; +} /** wrapper for XML parsing support */ -export class KeymanXMLParser { +export class KeymanXMLReader { + public constructor(public options: KeymanXMLOptions) { + } + public parse(data: string): Object { + const parser = this.parser(); + let a: any; + parser.parseString(data, (e: unknown, r: unknown) => { if (e) throw e; a = r; }); + return a; + } + + public parser() { + const { type } = this.options; + switch (type) { + case 'keyboard3': + return 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 + // 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. + }); + case 'keyboard3-test': + return new xml2js.Parser({ + // explicitArray: false, + preserveChildrenOrder: true, // needed for test data + explicitChildren: true, // needed for test data + // mergeAttrs: true, + // includeWhiteChars: 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. + }); + case 'kps': + return new xml2js.Parser({ + explicitArray: false + }); + case 'kpj': + return new xml2js.Parser({ + explicitArray: false, + mergeAttrs: false, + includeWhiteChars: false, + normalize: false, + emptyTag: '' + }); + case 'kvks': + return new xml2js.Parser({ + explicitArray: false, + mergeAttrs: false, + 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. + }); + default: + /* c8 ignore next 1 */ + throw Error(`Internal error: unhandled XML type ${type}`); + } + } } /** wrapper for XML generation support */ -export class KeymanXMLGenerator { - +export class KeymanXMLWriter { + constructor(public options: KeymanXMLOptions) { + } } diff --git a/developer/src/common/web/utils/test/fixtures/xml/disp_maximal.xml b/developer/src/common/web/utils/test/fixtures/xml/disp_maximal.xml new file mode 100644 index 00000000000..aeab3a71a5c --- /dev/null +++ b/developer/src/common/web/utils/test/fixtures/xml/disp_maximal.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/developer/src/common/web/utils/test/fixtures/xml/disp_maximal.xml.json b/developer/src/common/web/utils/test/fixtures/xml/disp_maximal.xml.json new file mode 100644 index 00000000000..55ea878c833 --- /dev/null +++ b/developer/src/common/web/utils/test/fixtures/xml/disp_maximal.xml.json @@ -0,0 +1,35 @@ +{ + "keyboard3": { + "xmlns": "https://schemas.unicode.org/cldr/45/keyboard3", + "locale": "mt", + "conformsTo": "45", + "info": { + "name": "disp-maximal" + }, + "displays": { + "display": [ + { + "keyId": "g", + "display": "(g)" + }, + { + "output": "f", + "display": "(f)" + }, + { + "output": "${eee}", + "display": "(${eee})" + } + ], + "displayOptions": { + "baseCharacter": "x" + } + }, + "variables": { + "string": { + "id": "eee", + "value": "e" + } + } + } +} \ No newline at end of file diff --git a/developer/src/common/web/utils/test/fixtures/xml/error_invalid_package_file.kps b/developer/src/common/web/utils/test/fixtures/xml/error_invalid_package_file.kps new file mode 100644 index 00000000000..e18eaa26a06 --- /dev/null +++ b/developer/src/common/web/utils/test/fixtures/xml/error_invalid_package_file.kps @@ -0,0 +1,32 @@ + + + + 15.0.266.0 + 7.0 + + + SENĆOŦEN (Saanich Dialect) Keyboard + + © 2019 National Research Council Canada & this test + Eddie Antonio Santos + 1.0 + + + + basic.kmx + Keyboard Basic + 0 + .kmx + + + + + Basic + basic + 1.0 + + Khmer + + + + diff --git a/developer/src/common/web/utils/test/fixtures/xml/k_020_fr-test.xml b/developer/src/common/web/utils/test/fixtures/xml/k_020_fr-test.xml new file mode 100644 index 00000000000..22fce785389 --- /dev/null +++ b/developer/src/common/web/utils/test/fixtures/xml/k_020_fr-test.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/common/web/utils/test/fixtures/xml/k_020_fr-test.xml.json b/developer/src/common/web/utils/test/fixtures/xml/k_020_fr-test.xml.json new file mode 100644 index 00000000000..ab530dd0b4a --- /dev/null +++ b/developer/src/common/web/utils/test/fixtures/xml/k_020_fr-test.xml.json @@ -0,0 +1,538 @@ +{ + "keyboardTest3": { + "$": { + "conformsTo": "techpreview" + }, + "#name": "keyboardTest3", + "$$": [ + { + "$": { + "keyboard": "k_020_fr.xml", + "author": "Team Keyboard", + "name": "fr-test-updated" + }, + "#name": "info" + }, + { + "$": { + "name": "simple-repertoire", + "chars": "[a b c d e \\u0022]", + "type": "simple" + }, + "#name": "repertoire" + }, + { + "$": { + "name": "chars-repertoire", + "chars": "[á é ó]", + "type": "gesture" + }, + "#name": "repertoire" + }, + { + "$": { + "name": "key-tests-updated" + }, + "#name": "tests", + "$$": [ + { + "$": { + "name": "key-test" + }, + "#name": "test", + "$$": [ + { + "$": { + "to": "abc\\u0022..." + }, + "#name": "startContext" + }, + { + "$": { + "key": "s" + }, + "#name": "keystroke" + }, + { + "$": { + "result": "abc\\u0022...s" + }, + "#name": "check" + }, + { + "$": { + "key": "t" + }, + "#name": "keystroke" + }, + { + "$": { + "result": "abc\\u0022...st" + }, + "#name": "check" + }, + { + "$": { + "key": "u" + }, + "#name": "keystroke" + }, + { + "$": { + "result": "abc\\u0022...stu" + }, + "#name": "check" + }, + { + "$": { + "to": "v" + }, + "#name": "emit" + }, + { + "$": { + "result": "abc\\u0022...stuv" + }, + "#name": "check" + } + ], + "startContext": [ + { + "$": { + "to": "abc\\u0022..." + } + } + ], + "keystroke": [ + { + "$": { + "key": "s" + } + }, + { + "$": { + "key": "t" + } + }, + { + "$": { + "key": "u" + } + } + ], + "check": [ + { + "$": { + "result": "abc\\u0022...s" + } + }, + { + "$": { + "result": "abc\\u0022...st" + } + }, + { + "$": { + "result": "abc\\u0022...stu" + } + }, + { + "$": { + "result": "abc\\u0022...stuv" + } + } + ], + "emit": [ + { + "$": { + "to": "v" + } + } + ] + } + ], + "test": [ + { + "$": { + "name": "key-test" + }, + "$$": [ + { + "$": { + "to": "abc\\u0022..." + }, + "#name": "startContext" + }, + { + "$": { + "key": "s" + }, + "#name": "keystroke" + }, + { + "$": { + "result": "abc\\u0022...s" + }, + "#name": "check" + }, + { + "$": { + "key": "t" + }, + "#name": "keystroke" + }, + { + "$": { + "result": "abc\\u0022...st" + }, + "#name": "check" + }, + { + "$": { + "key": "u" + }, + "#name": "keystroke" + }, + { + "$": { + "result": "abc\\u0022...stu" + }, + "#name": "check" + }, + { + "$": { + "to": "v" + }, + "#name": "emit" + }, + { + "$": { + "result": "abc\\u0022...stuv" + }, + "#name": "check" + } + ], + "startContext": [ + { + "$": { + "to": "abc\\u0022..." + } + } + ], + "keystroke": [ + { + "$": { + "key": "s" + } + }, + { + "$": { + "key": "t" + } + }, + { + "$": { + "key": "u" + } + } + ], + "check": [ + { + "$": { + "result": "abc\\u0022...s" + } + }, + { + "$": { + "result": "abc\\u0022...st" + } + }, + { + "$": { + "result": "abc\\u0022...stu" + } + }, + { + "$": { + "result": "abc\\u0022...stuv" + } + } + ], + "emit": [ + { + "$": { + "to": "v" + } + } + ] + } + ] + } + ], + "info": [ + { + "$": { + "keyboard": "k_020_fr.xml", + "author": "Team Keyboard", + "name": "fr-test-updated" + } + } + ], + "repertoire": [ + { + "$": { + "name": "simple-repertoire", + "chars": "[a b c d e \\u0022]", + "type": "simple" + } + }, + { + "$": { + "name": "chars-repertoire", + "chars": "[á é ó]", + "type": "gesture" + } + } + ], + "tests": [ + { + "$": { + "name": "key-tests-updated" + }, + "$$": [ + { + "$": { + "name": "key-test" + }, + "#name": "test", + "$$": [ + { + "$": { + "to": "abc\\u0022..." + }, + "#name": "startContext" + }, + { + "$": { + "key": "s" + }, + "#name": "keystroke" + }, + { + "$": { + "result": "abc\\u0022...s" + }, + "#name": "check" + }, + { + "$": { + "key": "t" + }, + "#name": "keystroke" + }, + { + "$": { + "result": "abc\\u0022...st" + }, + "#name": "check" + }, + { + "$": { + "key": "u" + }, + "#name": "keystroke" + }, + { + "$": { + "result": "abc\\u0022...stu" + }, + "#name": "check" + }, + { + "$": { + "to": "v" + }, + "#name": "emit" + }, + { + "$": { + "result": "abc\\u0022...stuv" + }, + "#name": "check" + } + ], + "startContext": [ + { + "$": { + "to": "abc\\u0022..." + } + } + ], + "keystroke": [ + { + "$": { + "key": "s" + } + }, + { + "$": { + "key": "t" + } + }, + { + "$": { + "key": "u" + } + } + ], + "check": [ + { + "$": { + "result": "abc\\u0022...s" + } + }, + { + "$": { + "result": "abc\\u0022...st" + } + }, + { + "$": { + "result": "abc\\u0022...stu" + } + }, + { + "$": { + "result": "abc\\u0022...stuv" + } + } + ], + "emit": [ + { + "$": { + "to": "v" + } + } + ] + } + ], + "test": [ + { + "$": { + "name": "key-test" + }, + "$$": [ + { + "$": { + "to": "abc\\u0022..." + }, + "#name": "startContext" + }, + { + "$": { + "key": "s" + }, + "#name": "keystroke" + }, + { + "$": { + "result": "abc\\u0022...s" + }, + "#name": "check" + }, + { + "$": { + "key": "t" + }, + "#name": "keystroke" + }, + { + "$": { + "result": "abc\\u0022...st" + }, + "#name": "check" + }, + { + "$": { + "key": "u" + }, + "#name": "keystroke" + }, + { + "$": { + "result": "abc\\u0022...stu" + }, + "#name": "check" + }, + { + "$": { + "to": "v" + }, + "#name": "emit" + }, + { + "$": { + "result": "abc\\u0022...stuv" + }, + "#name": "check" + } + ], + "startContext": [ + { + "$": { + "to": "abc\\u0022..." + } + } + ], + "keystroke": [ + { + "$": { + "key": "s" + } + }, + { + "$": { + "key": "t" + } + }, + { + "$": { + "key": "u" + } + } + ], + "check": [ + { + "$": { + "result": "abc\\u0022...s" + } + }, + { + "$": { + "result": "abc\\u0022...st" + } + }, + { + "$": { + "result": "abc\\u0022...stu" + } + }, + { + "$": { + "result": "abc\\u0022...stuv" + } + } + ], + "emit": [ + { + "$": { + "to": "v" + } + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/developer/src/common/web/utils/test/fixtures/xml/k_020_fr.xml b/developer/src/common/web/utils/test/fixtures/xml/k_020_fr.xml new file mode 100644 index 00000000000..671f1b8b9b6 --- /dev/null +++ b/developer/src/common/web/utils/test/fixtures/xml/k_020_fr.xml @@ -0,0 +1,200 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/common/web/utils/test/fixtures/xml/k_020_fr.xml.json b/developer/src/common/web/utils/test/fixtures/xml/k_020_fr.xml.json new file mode 100644 index 00000000000..1e23ec4bdbd --- /dev/null +++ b/developer/src/common/web/utils/test/fixtures/xml/k_020_fr.xml.json @@ -0,0 +1,509 @@ +{ + "keyboard3": { + "xmlns": "https://schemas.unicode.org/cldr/45/keyboard3", + "locale": "fr-t-k0-azerty", + "conformsTo": "45", + "locales": { + "locale": { + "id": "br" + } + }, + "version": { + "number": "1.0.1" + }, + "info": { + "author": "Team Keyboard", + "layout": "AZERTY", + "indicator": "FR", + "name": "French Test Updated" + }, + "displays": { + "display": { + "output": "\\u{0300}", + "display": "\\u{02CB}" + }, + "displayOptions": { + "baseCharacter": "x" + } + }, + "keys": { + "import": [ + { + "base": "cldr", + "path": "45/keys-Zyyy-punctuation.xml" + }, + { + "base": "cldr", + "path": "45/keys-Zyyy-currency.xml" + } + ], + "key": [ + { + "id": "shift", + "layerId": "shift" + }, + { + "id": "numeric", + "layerId": "numeric" + }, + { + "id": "symbol", + "layerId": "symbol" + }, + { + "id": "base", + "layerId": "base" + }, + { + "id": "bksp", + "gap": "true" + }, + { + "id": "extra", + "gap": "true" + }, + { + "id": "enter", + "output": "\\u{000A}" + }, + { + "id": "u-grave", + "output": "ü" + }, + { + "id": "e-grave", + "output": "é" + }, + { + "id": "e-acute", + "output": "è" + }, + { + "id": "c-cedilla", + "output": "ç" + }, + { + "id": "a-grave", + "output": "à" + }, + { + "id": "a-acute", + "output": "á" + }, + { + "id": "a-caret", + "output": "â" + }, + { + "id": "a-umlaut", + "output": "ä" + }, + { + "id": "a-tilde", + "output": "ã" + }, + { + "id": "a-ring", + "output": "å" + }, + { + "id": "a-caron", + "output": "ā" + }, + { + "id": "A-grave", + "output": "À" + }, + { + "id": "A-acute", + "output": "Á" + }, + { + "id": "A-caret", + "output": "Â" + }, + { + "id": "A-umlaut", + "output": "Ä" + }, + { + "id": "A-tilde", + "output": "Ã" + }, + { + "id": "A-ring", + "output": "Å" + }, + { + "id": "A-caron", + "output": "Ā" + }, + { + "id": "bullet", + "output": "•" + }, + { + "id": "umlaut", + "output": "¨" + }, + { + "id": "sub-2", + "output": "₂" + }, + { + "id": "super-2", + "output": "²", + "longPressKeyIds": "sub-2" + }, + { + "id": "a", + "flickId": "a", + "output": "a", + "longPressKeyIds": "a-grave a-caret a-acute a-umlaut a-tilde a-ring a-caron", + "longPressDefaultKeyId": "a-caret" + }, + { + "id": "A", + "flickId": "b", + "output": "A", + "longPressKeyIds": "A-grave A-caret A-acute A-umlaut a-tilde A-ring A-caron", + "longPressDefaultKeyId": "A-caret" + } + ] + }, + "flicks": { + "flick": [ + { + "id": "b", + "flickSegment": [ + { + "directions": "nw", + "keyId": "A-grave" + }, + { + "directions": "nw se", + "keyId": "A-acute" + }, + { + "directions": "e", + "keyId": "A-caron" + }, + { + "directions": "s", + "keyId": "numeric" + } + ] + }, + { + "id": "a", + "flickSegment": [ + { + "directions": "nw", + "keyId": "a-grave" + }, + { + "directions": "nw se", + "keyId": "a-acute" + }, + { + "directions": "e", + "keyId": "a-caron" + } + ] + } + ] + }, + "layers": [ + { + "formId": "iso", + "layer": [ + { + "modifiers": "none", + "row": [ + { + "keys": "super-2 amp e-grave double-quote apos open-paren hyphen e-acute underscore c-cedilla a-acute close-paren equal" + }, + { + "keys": "a z e r t y u i o p caret dollar" + }, + { + "keys": "q s d f g h j k l m u-grave asterisk" + }, + { + "keys": "open-angle w x c v b n comma semi-colon colon bang" + }, + { + "keys": "space" + } + ] + }, + { + "modifiers": "shift", + "row": [ + { + "keys": "1 2 3 4 5 6 7 8 9 0 degree plus" + }, + { + "keys": "A Z E R T Y U I O P umlaut pound" + }, + { + "keys": "Q S D F G H J K L M percent micro" + }, + { + "keys": "close-angle W X C V B N question period slash section" + }, + { + "keys": "space" + } + ] + } + ] + }, + { + "formId": "touch", + "minDeviceWidth": "150", + "layer": [ + { + "id": "base", + "row": [ + { + "keys": "a z e r t y u i o p" + }, + { + "keys": "q s d f g h j k l m" + }, + { + "keys": "shift gap w x c v b n gap" + }, + { + "keys": "numeric extra space enter" + } + ] + }, + { + "id": "shift", + "row": [ + { + "keys": "A Z E R T Y U I O P" + }, + { + "keys": "Q S D F G H J K L M" + }, + { + "keys": "base W X C V B N" + }, + { + "keys": "numeric extra space enter" + } + ] + }, + { + "id": "numeric", + "row": [ + { + "keys": "1 2 3 4 5 6 7 8 9 0" + }, + { + "keys": "hyphen slash colon semi-colon open-paren close-paren dollar amp at double-quote" + }, + { + "keys": "symbol period comma question bang double-quote" + }, + { + "keys": "base extra space enter" + } + ] + }, + { + "id": "symbol", + "row": [ + { + "keys": "open-square close-square open-curly close-curly hash percent caret asterisk plus equal" + }, + { + "keys": "underscore backslash pipe tilde open-angle close-angle euro pound yen bullet" + }, + { + "keys": "numeric period comma question bang double-quote" + }, + { + "keys": "base extra space enter" + } + ] + } + ] + } + ], + "transforms": { + "type": "simple", + "transformGroup": { + "transform": [ + { + "from": "` ", + "to": "`" + }, + { + "from": "`a", + "to": "à" + }, + { + "from": "`A", + "to": "À" + }, + { + "from": "`e", + "to": "è" + }, + { + "from": "`E", + "to": "È" + }, + { + "from": "`i", + "to": "ì" + }, + { + "from": "`I", + "to": "Ì" + }, + { + "from": "`o", + "to": "ò" + }, + { + "from": "`O", + "to": "Ò" + }, + { + "from": "`u", + "to": "ù" + }, + { + "from": "`U", + "to": "Ù" + }, + { + "from": "^ ", + "to": "^" + }, + { + "from": "^a", + "to": "â" + }, + { + "from": "^A", + "to": " " + }, + { + "from": "^e", + "to": "ê" + }, + { + "from": "^E", + "to": "Ê" + }, + { + "from": "^i", + "to": "î" + }, + { + "from": "^I", + "to": "Î" + }, + { + "from": "^o", + "to": "ô" + }, + { + "from": "^O", + "to": "Ô" + }, + { + "from": "^u", + "to": "û" + }, + { + "from": "^U", + "to": "Û" + }, + { + "from": "¨ ", + "to": "¨" + }, + { + "from": "¨a", + "to": "ä" + }, + { + "from": "¨A", + "to": "Ä" + }, + { + "from": "¨e", + "to": "ë" + }, + { + "from": "¨E", + "to": "Ë" + }, + { + "from": "¨i", + "to": "ï" + }, + { + "from": "¨I", + "to": "Ï" + }, + { + "from": "¨o", + "to": "ö" + }, + { + "from": "¨O", + "to": "Ö" + }, + { + "from": "¨u", + "to": "ü" + }, + { + "from": "¨U", + "to": "Ü" + }, + { + "from": "¨y", + "to": "ÿ" + }, + { + "from": "~ ", + "to": "~" + }, + { + "from": "~a", + "to": "ã" + }, + { + "from": "~A", + "to": "Ã" + }, + { + "from": "~n", + "to": "ñ" + }, + { + "from": "~N", + "to": "Ñ" + }, + { + "from": "~o", + "to": "õ" + }, + { + "from": "~O", + "to": "Õ" + } + ] + } + } + } +} \ No newline at end of file diff --git a/developer/src/common/web/utils/test/fixtures/xml/khmer_angkor.kpj b/developer/src/common/web/utils/test/fixtures/xml/khmer_angkor.kpj new file mode 100644 index 00000000000..4b239df20ce --- /dev/null +++ b/developer/src/common/web/utils/test/fixtures/xml/khmer_angkor.kpj @@ -0,0 +1,187 @@ + + + + $PROJECTPATH\build + True + True + False + keyboard + + + + id_f347675c33d2e6b1c705c787fad4941a + khmer_angkor.kmn + source\khmer_angkor.kmn + 1.3 + .kmn +
+ Khmer Angkor + © 2015-2022 SIL International + More than just a Khmer Unicode keyboard. +
+
+ + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + khmer_angkor.kps + source\khmer_angkor.kps + + .kps +
+ Khmer Angkor + © 2015-2022 SIL International +
+
+ + id_8a1efc7c4ab7cfece8aedd847679ca27 + khmer_angkor.ico + source\khmer_angkor.ico + + .ico + id_f347675c33d2e6b1c705c787fad4941a + + + id_8dc195db32d1fd0514de0ad51fff5df0 + khmer_angkor.js + source\..\build\khmer_angkor.js + + .js + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_10596632fcbf4138d24bcccf53e6ae01 + khmer_angkor.kvk + source\..\build\khmer_angkor.kvk + + .kvk + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_0a851f95ce553ecd62cbee6c32ced68f + khmer_angkor.kmx + source\..\build\khmer_angkor.kmx + + .kmx + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_d8b6eb05f4b7e2945c10e04c1f49e4c8 + keyboard_layout.png + source\welcome\keyboard_layout.png + + .png + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_724e5b4c63f10bc0abf7077f7c3172fc + welcome.htm + source\welcome\welcome.htm + + .htm + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_35857cb2b54f123612735ec948400082 + FONTLOG.txt + source\..\..\..\shared\fonts\khmer\mondulkiri\FONTLOG.txt + + .txt + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_7e3afe5bb59b888b08b48cd5817d8de4 + Mondulkiri-B.ttf + source\..\..\..\shared\fonts\khmer\mondulkiri\Mondulkiri-B.ttf + + .ttf + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_b9734e80f86c69ea5ae4dfa9f0083d09 + Mondulkiri-BI.ttf + source\..\..\..\shared\fonts\khmer\mondulkiri\Mondulkiri-BI.ttf + + .ttf + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_25abe4d2b0abc03a5be5b666a8de776e + Mondulkiri-I.ttf + source\..\..\..\shared\fonts\khmer\mondulkiri\Mondulkiri-I.ttf + + .ttf + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_b766568498108eee46ed1601ff69c47d + Mondulkiri-R.ttf + source\..\..\..\shared\fonts\khmer\mondulkiri\Mondulkiri-R.ttf + + .ttf + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_84544d04133cab3dbfc86b91ad1a4e17 + OFL.txt + source\..\..\..\shared\fonts\khmer\mondulkiri\OFL.txt + + .txt + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_0c33fbefd1c20f487b1bea2343b3bb2c + OFL-FAQ.txt + source\..\..\..\shared\fonts\khmer\mondulkiri\OFL-FAQ.txt + + .txt + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_a59d89fca36a310147645fa2604e521b + KAK_Documentation_EN.pdf + source\welcome\KAK_Documentation_EN.pdf + + .pdf + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_5643c4cd3933b3ada0b4af6579305ec4 + KAK_Documentation_KH.pdf + source\welcome\KAK_Documentation_KH.pdf + + .pdf + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_8da344c4cea6f467013357fe099006f5 + readme.htm + source\readme.htm + + .htm + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_acb0dd94c60e345d999670e999cbd159 + image002.png + source\welcome\image002.png + + .png + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_4edf70bc019f05b5ad39a2ea727ad547 + khmer_busra_kbd.ttf + source\..\..\..\shared\fonts\khmer\busrakbd\khmer_busra_kbd.ttf + + .ttf + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + + + id_bc823844e4399751e1867016801f7327 + splash.gif + source\splash.gif + + .gif + id_8d4eb765f80c9f2b0f769cf4e4aaa456 + +
+
diff --git a/developer/src/common/web/utils/test/fixtures/xml/khmer_angkor.kpj.json b/developer/src/common/web/utils/test/fixtures/xml/khmer_angkor.kpj.json new file mode 100644 index 00000000000..8f2310f821c --- /dev/null +++ b/developer/src/common/web/utils/test/fixtures/xml/khmer_angkor.kpj.json @@ -0,0 +1,190 @@ +{ + "KeymanDeveloperProject": { + "Options": { + "BuildPath": "$PROJECTPATH\\build", + "CompilerWarningsAsErrors": "True", + "WarnDeprecatedCode": "True", + "CheckFilenameConventions": "False", + "ProjectType": "keyboard" + }, + "Files": { + "File": [ + { + "ID": "id_f347675c33d2e6b1c705c787fad4941a", + "Filename": "khmer_angkor.kmn", + "Filepath": "source\\khmer_angkor.kmn", + "FileVersion": "1.3", + "FileType": ".kmn", + "Details": { + "Name": "Khmer Angkor", + "Copyright": "© 2015-2022 SIL International", + "Message": "More than just a Khmer Unicode keyboard." + } + }, + { + "ID": "id_8d4eb765f80c9f2b0f769cf4e4aaa456", + "Filename": "khmer_angkor.kps", + "Filepath": "source\\khmer_angkor.kps", + "FileVersion": "", + "FileType": ".kps", + "Details": { + "Name": "Khmer Angkor", + "Copyright": "© 2015-2022 SIL International" + } + }, + { + "ID": "id_8a1efc7c4ab7cfece8aedd847679ca27", + "Filename": "khmer_angkor.ico", + "Filepath": "source\\khmer_angkor.ico", + "FileVersion": "", + "FileType": ".ico", + "ParentFileID": "id_f347675c33d2e6b1c705c787fad4941a" + }, + { + "ID": "id_8dc195db32d1fd0514de0ad51fff5df0", + "Filename": "khmer_angkor.js", + "Filepath": "source\\..\\build\\khmer_angkor.js", + "FileVersion": "", + "FileType": ".js", + "ParentFileID": "id_8d4eb765f80c9f2b0f769cf4e4aaa456" + }, + { + "ID": "id_10596632fcbf4138d24bcccf53e6ae01", + "Filename": "khmer_angkor.kvk", + "Filepath": "source\\..\\build\\khmer_angkor.kvk", + "FileVersion": "", + "FileType": ".kvk", + "ParentFileID": "id_8d4eb765f80c9f2b0f769cf4e4aaa456" + }, + { + "ID": "id_0a851f95ce553ecd62cbee6c32ced68f", + "Filename": "khmer_angkor.kmx", + "Filepath": "source\\..\\build\\khmer_angkor.kmx", + "FileVersion": "", + "FileType": ".kmx", + "ParentFileID": "id_8d4eb765f80c9f2b0f769cf4e4aaa456" + }, + { + "ID": "id_d8b6eb05f4b7e2945c10e04c1f49e4c8", + "Filename": "keyboard_layout.png", + "Filepath": "source\\welcome\\keyboard_layout.png", + "FileVersion": "", + "FileType": ".png", + "ParentFileID": "id_8d4eb765f80c9f2b0f769cf4e4aaa456" + }, + { + "ID": "id_724e5b4c63f10bc0abf7077f7c3172fc", + "Filename": "welcome.htm", + "Filepath": "source\\welcome\\welcome.htm", + "FileVersion": "", + "FileType": ".htm", + "ParentFileID": "id_8d4eb765f80c9f2b0f769cf4e4aaa456" + }, + { + "ID": "id_35857cb2b54f123612735ec948400082", + "Filename": "FONTLOG.txt", + "Filepath": "source\\..\\..\\..\\shared\\fonts\\khmer\\mondulkiri\\FONTLOG.txt", + "FileVersion": "", + "FileType": ".txt", + "ParentFileID": "id_8d4eb765f80c9f2b0f769cf4e4aaa456" + }, + { + "ID": "id_7e3afe5bb59b888b08b48cd5817d8de4", + "Filename": "Mondulkiri-B.ttf", + "Filepath": "source\\..\\..\\..\\shared\\fonts\\khmer\\mondulkiri\\Mondulkiri-B.ttf", + "FileVersion": "", + "FileType": ".ttf", + "ParentFileID": "id_8d4eb765f80c9f2b0f769cf4e4aaa456" + }, + { + "ID": "id_b9734e80f86c69ea5ae4dfa9f0083d09", + "Filename": "Mondulkiri-BI.ttf", + "Filepath": "source\\..\\..\\..\\shared\\fonts\\khmer\\mondulkiri\\Mondulkiri-BI.ttf", + "FileVersion": "", + "FileType": ".ttf", + "ParentFileID": "id_8d4eb765f80c9f2b0f769cf4e4aaa456" + }, + { + "ID": "id_25abe4d2b0abc03a5be5b666a8de776e", + "Filename": "Mondulkiri-I.ttf", + "Filepath": "source\\..\\..\\..\\shared\\fonts\\khmer\\mondulkiri\\Mondulkiri-I.ttf", + "FileVersion": "", + "FileType": ".ttf", + "ParentFileID": "id_8d4eb765f80c9f2b0f769cf4e4aaa456" + }, + { + "ID": "id_b766568498108eee46ed1601ff69c47d", + "Filename": "Mondulkiri-R.ttf", + "Filepath": "source\\..\\..\\..\\shared\\fonts\\khmer\\mondulkiri\\Mondulkiri-R.ttf", + "FileVersion": "", + "FileType": ".ttf", + "ParentFileID": "id_8d4eb765f80c9f2b0f769cf4e4aaa456" + }, + { + "ID": "id_84544d04133cab3dbfc86b91ad1a4e17", + "Filename": "OFL.txt", + "Filepath": "source\\..\\..\\..\\shared\\fonts\\khmer\\mondulkiri\\OFL.txt", + "FileVersion": "", + "FileType": ".txt", + "ParentFileID": "id_8d4eb765f80c9f2b0f769cf4e4aaa456" + }, + { + "ID": "id_0c33fbefd1c20f487b1bea2343b3bb2c", + "Filename": "OFL-FAQ.txt", + "Filepath": "source\\..\\..\\..\\shared\\fonts\\khmer\\mondulkiri\\OFL-FAQ.txt", + "FileVersion": "", + "FileType": ".txt", + "ParentFileID": "id_8d4eb765f80c9f2b0f769cf4e4aaa456" + }, + { + "ID": "id_a59d89fca36a310147645fa2604e521b", + "Filename": "KAK_Documentation_EN.pdf", + "Filepath": "source\\welcome\\KAK_Documentation_EN.pdf", + "FileVersion": "", + "FileType": ".pdf", + "ParentFileID": "id_8d4eb765f80c9f2b0f769cf4e4aaa456" + }, + { + "ID": "id_5643c4cd3933b3ada0b4af6579305ec4", + "Filename": "KAK_Documentation_KH.pdf", + "Filepath": "source\\welcome\\KAK_Documentation_KH.pdf", + "FileVersion": "", + "FileType": ".pdf", + "ParentFileID": "id_8d4eb765f80c9f2b0f769cf4e4aaa456" + }, + { + "ID": "id_8da344c4cea6f467013357fe099006f5", + "Filename": "readme.htm", + "Filepath": "source\\readme.htm", + "FileVersion": "", + "FileType": ".htm", + "ParentFileID": "id_8d4eb765f80c9f2b0f769cf4e4aaa456" + }, + { + "ID": "id_acb0dd94c60e345d999670e999cbd159", + "Filename": "image002.png", + "Filepath": "source\\welcome\\image002.png", + "FileVersion": "", + "FileType": ".png", + "ParentFileID": "id_8d4eb765f80c9f2b0f769cf4e4aaa456" + }, + { + "ID": "id_4edf70bc019f05b5ad39a2ea727ad547", + "Filename": "khmer_busra_kbd.ttf", + "Filepath": "source\\..\\..\\..\\shared\\fonts\\khmer\\busrakbd\\khmer_busra_kbd.ttf", + "FileVersion": "", + "FileType": ".ttf", + "ParentFileID": "id_8d4eb765f80c9f2b0f769cf4e4aaa456" + }, + { + "ID": "id_bc823844e4399751e1867016801f7327", + "Filename": "splash.gif", + "Filepath": "source\\splash.gif", + "FileVersion": "", + "FileType": ".gif", + "ParentFileID": "id_8d4eb765f80c9f2b0f769cf4e4aaa456" + } + ] + } + } +} \ No newline at end of file diff --git a/developer/src/common/web/utils/test/fixtures/xml/khmer_angkor.kvks b/developer/src/common/web/utils/test/fixtures/xml/khmer_angkor.kvks new file mode 100644 index 00000000000..e9c38a464dd --- /dev/null +++ b/developer/src/common/web/utils/test/fixtures/xml/khmer_angkor.kvks @@ -0,0 +1,206 @@ + + +
+ 10.0 + khmer_angkor + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + ] + [ + / + . + + + + & + + * + @ + \ + } + { + - + ÷ + : + , + + ; + < + # + > + × + $ + +   + + + + + + + + + + + + + ᧿ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + « + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ! + + " + + % + + ( + ) + + = + + + ? + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +  + + + » + + + +
diff --git a/developer/src/common/web/utils/test/fixtures/xml/khmer_angkor.kvks.json b/developer/src/common/web/utils/test/fixtures/xml/khmer_angkor.kvks.json new file mode 100644 index 00000000000..6775beb1605 --- /dev/null +++ b/developer/src/common/web/utils/test/fixtures/xml/khmer_angkor.kvks.json @@ -0,0 +1,1172 @@ +{ + "visualkeyboard": { + "_": "\n \n \n", + "header": { + "_": "\n \n \n \n ", + "version": "10.0", + "kbdname": "khmer_angkor", + "flags": { + "_": "\n \n ", + "usealtgr": "" + } + }, + "encoding": { + "_": "\n \n \n \n \n ", + "$": { + "name": "unicode", + "fontname": "Khmer Busra Kbd", + "fontsize": "16" + }, + "layer": [ + { + "_": "\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ", + "$": { + "shift": "RA" + }, + "key": [ + { + "_": "ឞ", + "$": { + "vkey": "K_B" + } + }, + { + "_": "ឝ", + "$": { + "vkey": "K_K" + } + }, + { + "_": "ៈ", + "$": { + "vkey": "K_QUOTE" + } + }, + { + "_": "ឳ", + "$": { + "vkey": "K_RBRKT" + } + }, + { + "_": "ឨ", + "$": { + "vkey": "K_T" + } + }, + { + "_": "ឩ", + "$": { + "vkey": "K_LBRKT" + } + }, + { + "_": "ឰ", + "$": { + "vkey": "K_P" + } + }, + { + "_": "ឫ", + "$": { + "vkey": "K_R" + } + }, + { + "_": "ឦ", + "$": { + "vkey": "K_I" + } + }, + { + "_": "ឱ", + "$": { + "vkey": "K_O" + } + }, + { + "_": "ឯ", + "$": { + "vkey": "K_E" + } + }, + { + "_": "", + "$": { + "vkey": "K_3" + } + }, + { + "_": "", + "$": { + "vkey": "K_W" + } + }, + { + "_": "ៜ", + "$": { + "vkey": "K_Q" + } + }, + { + "_": "", + "$": { + "vkey": "K_EQUAL" + } + }, + { + "_": "៖", + "$": { + "vkey": "K_COLON" + } + }, + { + "_": "៙", + "$": { + "vkey": "K_6" + } + }, + { + "_": "៚", + "$": { + "vkey": "K_7" + } + }, + { + "_": "", + "$": { + "vkey": "K_M" + } + }, + { + "_": "៘", + "$": { + "vkey": "K_L" + } + }, + { + "_": "‌", + "$": { + "vkey": "K_1" + } + }, + { + "_": "‍", + "$": { + "vkey": "K_BKQUOTE" + } + }, + { + "_": "]", + "$": { + "vkey": "K_U" + } + }, + { + "_": "[", + "$": { + "vkey": "K_Y" + } + }, + { + "_": "/", + "$": { + "vkey": "K_SLASH" + } + }, + { + "_": ".", + "$": { + "vkey": "K_PERIOD" + } + }, + { + "_": "‘", + "$": { + "vkey": "K_H" + } + }, + { + "_": "+", + "$": { + "vkey": "K_A" + } + }, + { + "_": "&", + "$": { + "vkey": "K_V" + } + }, + { + "_": "’", + "$": { + "vkey": "K_J" + } + }, + { + "_": "*", + "$": { + "vkey": "K_8" + } + }, + { + "_": "@", + "$": { + "vkey": "K_2" + } + }, + { + "_": "\\", + "$": { + "vkey": "K_BKSLASH" + } + }, + { + "_": "}", + "$": { + "vkey": "K_0" + } + }, + { + "_": "{", + "$": { + "vkey": "K_9" + } + }, + { + "_": "-", + "$": { + "vkey": "K_S" + } + }, + { + "_": "÷", + "$": { + "vkey": "K_F" + } + }, + { + "_": ":", + "$": { + "vkey": "K_G" + } + }, + { + "_": ",", + "$": { + "vkey": "K_COMMA" + } + }, + { + "_": "≈", + "$": { + "vkey": "K_HYPHEN" + } + }, + { + "_": ";", + "$": { + "vkey": "K_N" + } + }, + { + "_": "<", + "$": { + "vkey": "K_Z" + } + }, + { + "_": "#", + "$": { + "vkey": "K_C" + } + }, + { + "_": ">", + "$": { + "vkey": "K_X" + } + }, + { + "_": "×", + "$": { + "vkey": "K_D" + } + }, + { + "_": "$", + "$": { + "vkey": "K_4" + } + }, + { + "_": "€", + "$": { + "vkey": "K_5" + } + }, + { + "_": " ", + "$": { + "vkey": "K_SPACE" + } + } + ] + }, + { + "_": "\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ", + "$": { + "shift": "SRA" + }, + "key": [ + { + "_": "៸", + "$": { + "vkey": "K_8" + } + }, + { + "_": "៰", + "$": { + "vkey": "K_0" + } + }, + { + "_": "៱", + "$": { + "vkey": "K_1" + } + }, + { + "_": "៲", + "$": { + "vkey": "K_2" + } + }, + { + "_": "៳", + "$": { + "vkey": "K_3" + } + }, + { + "_": "៴", + "$": { + "vkey": "K_4" + } + }, + { + "_": "៵", + "$": { + "vkey": "K_5" + } + }, + { + "_": "៶", + "$": { + "vkey": "K_6" + } + }, + { + "_": "៷", + "$": { + "vkey": "K_7" + } + }, + { + "_": "៹", + "$": { + "vkey": "K_9" + } + }, + { + "_": "᧿", + "$": { + "vkey": "K_PERIOD" + } + }, + { + "_": "᧾", + "$": { + "vkey": "K_COMMA" + } + }, + { + "_": "᧪", + "$": { + "vkey": "K_LBRKT" + } + }, + { + "_": "᧫", + "$": { + "vkey": "K_RBRKT" + } + }, + { + "_": "᧶", + "$": { + "vkey": "K_QUOTE" + } + }, + { + "_": "᧵", + "$": { + "vkey": "K_COLON" + } + }, + { + "_": "᧬", + "$": { + "vkey": "K_A" + } + }, + { + "_": "᧷", + "$": { + "vkey": "K_Z" + } + }, + { + "_": "᧥", + "$": { + "vkey": "K_Y" + } + }, + { + "_": "᧸", + "$": { + "vkey": "K_X" + } + }, + { + "_": "᧡", + "$": { + "vkey": "K_W" + } + }, + { + "_": "᧻", + "$": { + "vkey": "K_B" + } + }, + { + "_": "᧹", + "$": { + "vkey": "K_C" + } + }, + { + "_": "᧮", + "$": { + "vkey": "K_D" + } + }, + { + "_": "᧢", + "$": { + "vkey": "K_E" + } + }, + { + "_": "᧯", + "$": { + "vkey": "K_F" + } + }, + { + "_": "᧰", + "$": { + "vkey": "K_G" + } + }, + { + "_": "᧦", + "$": { + "vkey": "K_U" + } + }, + { + "_": "᧱", + "$": { + "vkey": "K_H" + } + }, + { + "_": "᧤", + "$": { + "vkey": "K_T" + } + }, + { + "_": "᧭", + "$": { + "vkey": "K_S" + } + }, + { + "_": "᧣", + "$": { + "vkey": "K_R" + } + }, + { + "_": "᧧", + "$": { + "vkey": "K_I" + } + }, + { + "_": "᧠", + "$": { + "vkey": "K_Q" + } + }, + { + "_": "᧺", + "$": { + "vkey": "K_V" + } + }, + { + "_": "᧲", + "$": { + "vkey": "K_J" + } + }, + { + "_": "᧳", + "$": { + "vkey": "K_K" + } + }, + { + "_": "᧴", + "$": { + "vkey": "K_L" + } + }, + { + "_": "᧩", + "$": { + "vkey": "K_P" + } + }, + { + "_": "᧨", + "$": { + "vkey": "K_O" + } + }, + { + "_": "᧼", + "$": { + "vkey": "K_N" + } + }, + { + "_": "᧽", + "$": { + "vkey": "K_M" + } + } + ] + }, + { + "_": "\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ", + "$": { + "shift": "" + }, + "key": [ + { + "_": "​", + "$": { + "vkey": "K_SPACE" + } + }, + { + "_": "", + "$": { + "vkey": "K_QUOTE" + } + }, + { + "_": "", + "$": { + "vkey": "K_COMMA" + } + }, + { + "_": "ឥ", + "$": { + "vkey": "K_HYPHEN" + } + }, + { + "_": "។", + "$": { + "vkey": "K_PERIOD" + } + }, + { + "_": "", + "$": { + "vkey": "K_SLASH" + } + }, + { + "_": "០", + "$": { + "vkey": "K_0" + } + }, + { + "_": "១", + "$": { + "vkey": "K_1" + } + }, + { + "_": "២", + "$": { + "vkey": "K_2" + } + }, + { + "_": "៣", + "$": { + "vkey": "K_3" + } + }, + { + "_": "៤", + "$": { + "vkey": "K_4" + } + }, + { + "_": "៥", + "$": { + "vkey": "K_5" + } + }, + { + "_": "៦", + "$": { + "vkey": "K_6" + } + }, + { + "_": "៧", + "$": { + "vkey": "K_7" + } + }, + { + "_": "៨", + "$": { + "vkey": "K_8" + } + }, + { + "_": "៩", + "$": { + "vkey": "K_9" + } + }, + { + "_": "", + "$": { + "vkey": "K_COLON" + } + }, + { + "_": "ឲ", + "$": { + "vkey": "K_EQUAL" + } + }, + { + "_": "", + "$": { + "vkey": "K_LBRKT" + } + }, + { + "_": "ឮ", + "$": { + "vkey": "K_BKSLASH" + } + }, + { + "_": "ឪ", + "$": { + "vkey": "K_RBRKT" + } + }, + { + "_": "«", + "$": { + "vkey": "K_BKQUOTE" + } + }, + { + "_": "", + "$": { + "vkey": "K_A" + } + }, + { + "_": "ប", + "$": { + "vkey": "K_B" + } + }, + { + "_": "ច", + "$": { + "vkey": "K_C" + } + }, + { + "_": "ដ", + "$": { + "vkey": "K_D" + } + }, + { + "_": "", + "$": { + "vkey": "K_E" + } + }, + { + "_": "ថ", + "$": { + "vkey": "K_F" + } + }, + { + "_": "ង", + "$": { + "vkey": "K_G" + } + }, + { + "_": "ហ", + "$": { + "vkey": "K_H" + } + }, + { + "_": "", + "$": { + "vkey": "K_I" + } + }, + { + "_": "", + "$": { + "vkey": "K_J" + } + }, + { + "_": "ក", + "$": { + "vkey": "K_K" + } + }, + { + "_": "ល", + "$": { + "vkey": "K_L" + } + }, + { + "_": "ម", + "$": { + "vkey": "K_M" + } + }, + { + "_": "ន", + "$": { + "vkey": "K_N" + } + }, + { + "_": "", + "$": { + "vkey": "K_O" + } + }, + { + "_": "ផ", + "$": { + "vkey": "K_P" + } + }, + { + "_": "ឆ", + "$": { + "vkey": "K_Q" + } + }, + { + "_": "រ", + "$": { + "vkey": "K_R" + } + }, + { + "_": "ស", + "$": { + "vkey": "K_S" + } + }, + { + "_": "ត", + "$": { + "vkey": "K_T" + } + }, + { + "_": "", + "$": { + "vkey": "K_U" + } + }, + { + "_": "វ", + "$": { + "vkey": "K_V" + } + }, + { + "_": "", + "$": { + "vkey": "K_W" + } + }, + { + "_": "ខ", + "$": { + "vkey": "K_X" + } + }, + { + "_": "យ", + "$": { + "vkey": "K_Y" + } + }, + { + "_": "ឋ", + "$": { + "vkey": "K_Z" + } + } + ] + }, + { + "_": "\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ", + "$": { + "shift": "S" + }, + "key": [ + { + "_": "", + "$": { + "vkey": "K_SPACE" + } + }, + { + "_": "!", + "$": { + "vkey": "K_1" + } + }, + { + "_": "", + "$": { + "vkey": "K_QUOTE" + } + }, + { + "_": "\"", + "$": { + "vkey": "K_3" + } + }, + { + "_": "៛", + "$": { + "vkey": "K_4" + } + }, + { + "_": "%", + "$": { + "vkey": "K_5" + } + }, + { + "_": "", + "$": { + "vkey": "K_7" + } + }, + { + "_": "(", + "$": { + "vkey": "K_9" + } + }, + { + "_": ")", + "$": { + "vkey": "K_0" + } + }, + { + "_": "", + "$": { + "vkey": "K_8" + } + }, + { + "_": "=", + "$": { + "vkey": "K_EQUAL" + } + }, + { + "_": "", + "$": { + "vkey": "K_COLON" + } + }, + { + "_": "៕", + "$": { + "vkey": "K_PERIOD" + } + }, + { + "_": "?", + "$": { + "vkey": "K_SLASH" + } + }, + { + "_": "ៗ", + "$": { + "vkey": "K_2" + } + }, + { + "_": "", + "$": { + "vkey": "K_A" + } + }, + { + "_": "ព", + "$": { + "vkey": "K_B" + } + }, + { + "_": "ជ", + "$": { + "vkey": "K_C" + } + }, + { + "_": "ឌ", + "$": { + "vkey": "K_D" + } + }, + { + "_": "", + "$": { + "vkey": "K_E" + } + }, + { + "_": "ធ", + "$": { + "vkey": "K_F" + } + }, + { + "_": "អ", + "$": { + "vkey": "K_G" + } + }, + { + "_": "ះ", + "$": { + "vkey": "K_H" + } + }, + { + "_": "", + "$": { + "vkey": "K_I" + } + }, + { + "_": "ញ", + "$": { + "vkey": "K_J" + } + }, + { + "_": "គ", + "$": { + "vkey": "K_K" + } + }, + { + "_": "ឡ", + "$": { + "vkey": "K_L" + } + }, + { + "_": "", + "$": { + "vkey": "K_M" + } + }, + { + "_": "ណ", + "$": { + "vkey": "K_N" + } + }, + { + "_": "", + "$": { + "vkey": "K_O" + } + }, + { + "_": "ភ", + "$": { + "vkey": "K_P" + } + }, + { + "_": "ឈ", + "$": { + "vkey": "K_Q" + } + }, + { + "_": "ឬ", + "$": { + "vkey": "K_R" + } + }, + { + "_": "", + "$": { + "vkey": "K_S" + } + }, + { + "_": "ទ", + "$": { + "vkey": "K_T" + } + }, + { + "_": "", + "$": { + "vkey": "K_U" + } + }, + { + "_": "", + "$": { + "vkey": "K_V" + } + }, + { + "_": "", + "$": { + "vkey": "K_W" + } + }, + { + "_": "ឃ", + "$": { + "vkey": "K_X" + } + }, + { + "_": "", + "$": { + "vkey": "K_Y" + } + }, + { + "_": "ឍ", + "$": { + "vkey": "K_Z" + } + }, + { + "_": "", + "$": { + "vkey": "K_6" + } + }, + { + "_": "", + "$": { + "vkey": "K_HYPHEN" + } + }, + { + "_": "", + "$": { + "vkey": "K_LBRKT" + } + }, + { + "_": "ឭ", + "$": { + "vkey": "K_BKSLASH" + } + }, + { + "_": "ឧ", + "$": { + "vkey": "K_RBRKT" + } + }, + { + "_": "»", + "$": { + "vkey": "K_BKQUOTE" + } + }, + { + "_": "", + "$": { + "vkey": "K_COMMA" + } + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/developer/src/common/web/utils/test/fixtures/xml/strs_invalid-illegal.xml b/developer/src/common/web/utils/test/fixtures/xml/strs_invalid-illegal.xml new file mode 100644 index 00000000000..3eddaffdeea --- /dev/null +++ b/developer/src/common/web/utils/test/fixtures/xml/strs_invalid-illegal.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/common/web/utils/test/fixtures/xml/strs_invalid-illegal.xml.json b/developer/src/common/web/utils/test/fixtures/xml/strs_invalid-illegal.xml.json new file mode 100644 index 00000000000..e26bf18696d --- /dev/null +++ b/developer/src/common/web/utils/test/fixtures/xml/strs_invalid-illegal.xml.json @@ -0,0 +1,113 @@ +{ + "keyboard3": { + "xmlns": "https://schemas.unicode.org/cldr/45/keyboard3", + "locale": "mt", + "conformsTo": "45", + "version": { + "number": "1.0.0" + }, + "info": { + "author": "srl295", + "indicator": "🙀", + "layout": "qwerty", + "name": "TestKbd" + }, + "displays": { + "display": [ + { + "output": "a", + "display": "^" + }, + { + "keyId": "e", + "display": "^e" + } + ], + "displayOptions": { + "baseCharacter": "e" + } + }, + "keys": { + "key": [ + { + "id": "hmaqtugha", + "output": "h\\u{FDD0}", + "longPressKeyIds": "a e" + }, + { + "id": "that", + "output": "￿" + } + ] + }, + "layers": { + "formId": "us", + "minDeviceWidth": "123", + "layer": { + "id": "￾", + "row": { + "keys": "hmaqtugha that" + } + } + }, + "variables": { + "string": [ + { + "id": "a", + "value": "\\m{a}" + }, + { + "id": "vst", + "value": "abc pua:\\u{E010}" + } + ], + "set": { + "id": "vse", + "value": "a b \\u{04FFFE} \\u{5FFFF}" + }, + "uset": { + "id": "vus", + "value": "[abc]" + } + }, + "transforms": [ + { + "type": "simple", + "transformGroup": [ + { + "transform": [ + { + "from": "^a", + "to": "\\u{FDD0}" + }, + { + "from": "a", + "to": "\\m{a}\\u{E020}" + } + ] + }, + { + "transform": { + "from": "\\m{a}" + } + }, + { + "reorder": { + "before": "\\u{1A6B}", + "from": "\\u{1A60}[\\u{1A75}-\\u{1A79}]\\u{1A45}", + "order": "10 55 10" + } + } + ] + }, + { + "type": "backspace", + "transformGroup": { + "transform": { + "from": "^e" + } + } + } + ] + } +} \ No newline at end of file diff --git a/developer/src/common/web/utils/test/fixtures/xml/test_valid.kps b/developer/src/common/web/utils/test/fixtures/xml/test_valid.kps new file mode 100644 index 00000000000..8877759a281 --- /dev/null +++ b/developer/src/common/web/utils/test/fixtures/xml/test_valid.kps @@ -0,0 +1,46 @@ + + + + 10.0.1024.0 + 7.0 + + + + + + + + + + + + 1.0 + Test Valid + + + + test_valid.kmx + Keyboard Test Valid + 0 + .kmx + + + test_valid.js + Keyboard Test Valid + 0 + .js + + + + + Test Valid + test_valid + 1.0 + True + + English + + + + + diff --git a/developer/src/common/web/utils/test/fixtures/xml/test_valid.kps.json b/developer/src/common/web/utils/test/fixtures/xml/test_valid.kps.json new file mode 100644 index 00000000000..276554d09c3 --- /dev/null +++ b/developer/src/common/web/utils/test/fixtures/xml/test_valid.kps.json @@ -0,0 +1,64 @@ +{ + "Package": { + "System": { + "KeymanDeveloperVersion": "10.0.1024.0", + "FileVersion": "7.0" + }, + "Options": { + "ExecuteProgram": "", + "MSIFileName": "", + "MSIOptions": "" + }, + "StartMenu": { + "Folder": "", + "Items": "" + }, + "Info": { + "Version": { + "_": "1.0", + "$": { + "URL": "" + } + }, + "Name": { + "_": "Test Valid", + "$": { + "URL": "" + } + } + }, + "Files": { + "File": [ + { + "Name": "test_valid.kmx", + "Description": "Keyboard Test Valid", + "CopyLocation": "0", + "FileType": ".kmx" + }, + { + "Name": "test_valid.js", + "Description": "Keyboard Test Valid", + "CopyLocation": "0", + "FileType": ".js" + } + ] + }, + "Keyboards": { + "Keyboard": { + "Name": "Test Valid", + "ID": "test_valid", + "Version": "1.0", + "RTL": "True", + "Languages": { + "Language": { + "_": "English", + "$": { + "ID": "en" + } + } + } + } + }, + "Strings": "" + } +} \ No newline at end of file diff --git a/developer/src/common/web/utils/test/fixtures/xml/tran_fail-empty.xml b/developer/src/common/web/utils/test/fixtures/xml/tran_fail-empty.xml new file mode 100644 index 00000000000..6c28d3f00d7 --- /dev/null +++ b/developer/src/common/web/utils/test/fixtures/xml/tran_fail-empty.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/common/web/utils/test/fixtures/xml/tran_fail-empty.xml.json b/developer/src/common/web/utils/test/fixtures/xml/tran_fail-empty.xml.json new file mode 100644 index 00000000000..3308970fdd8 --- /dev/null +++ b/developer/src/common/web/utils/test/fixtures/xml/tran_fail-empty.xml.json @@ -0,0 +1,28 @@ +{ + "keyboard3": { + "xmlns": "https://schemas.unicode.org/cldr/45/keyboard3", + "locale": "mt", + "conformsTo": "45", + "info": { + "name": "tran-mixed" + }, + "transforms": { + "type": "simple", + "transformGroup": [ + { + "transform": { + "from": "xx", + "to": "x" + } + }, + { + "reorder": { + "from": "ខែ្ម", + "order": "1 3 4 2" + } + }, + {} + ] + } + } +} \ No newline at end of file diff --git a/developer/src/common/web/utils/test/test-xml-utils.ts b/developer/src/common/web/utils/test/test-xml-utils.ts index 756855e6b9f..b06da2cc034 100644 --- a/developer/src/common/web/utils/test/test-xml-utils.ts +++ b/developer/src/common/web/utils/test/test-xml-utils.ts @@ -1,13 +1,112 @@ import { assert } from 'chai'; import 'mocha'; +import { env } from 'node:process'; +import { readFileSync, writeFileSync } from 'node:fs'; -import { KeymanXMLParser, KeymanXMLGenerator } from '../src/xml-utils.js'; -describe('XML Parser Test', () => { - it('null test', () => assert.ok(new KeymanXMLParser())); +import { KeymanXMLOptions, KeymanXMLReader, KeymanXMLWriter } from '../src/xml-utils.js'; +import { makePathToFixture } from './helpers/index.js'; + +// if true, attempt to WRITE the fixtures +const { GEN_XML_FIXTURES } = env; + +class Case { + options: KeymanXMLOptions; + paths: string[]; +}; + +const read_cases : Case[] = [ + { + options: { type: 'keyboard3' }, + paths: [ + // keyboards + 'disp_maximal.xml', + 'k_020_fr.xml', + 'strs_invalid-illegal.xml', + 'tran_fail-empty.xml', + ], + }, { + options: { type: 'keyboard3-test' }, + paths: [ + // keyboard test + 'k_020_fr-test.xml', + ], + }, { + options: { type: 'kvks' }, + paths: [ + // kvks + 'khmer_angkor.kvks', + ], + }, { + options: { type: 'kps' }, + paths: [ + // kps + 'test_valid.kps', + // 'error_invalid_package_file.kps', + ], + }, { + options: { type: 'kpj' }, + paths: [ + // kpj + 'khmer_angkor.kpj', + ], + }, +]; + +/** read data, or null */ +function readData(path: string) : string | null { + try { + return readFileSync(path, 'utf-8'); + } catch(e) { + if (e?.code !== 'ENOENT') console.error(`reading ${path}`, e); + return null; + } +} + +function readJson(path: string) : any | null { + const data = readData(path); + if(data === null) return null; + return JSON.parse(data); +} + +function writeJson(path: string, data: any) { + writeFileSync(path, JSON.stringify(data, null, ' ')); +} + +describe(`XML Reader Test ${GEN_XML_FIXTURES && '(update mode!)' || ''}`, () => { + for (const c of read_cases) { + const {options, paths} = c; + describe(`test reading ${JSON.stringify(options)}`, () => { + const reader = new KeymanXMLReader(options); + assert.ok(reader); + for (const path of paths) { + const xmlPath = makePathToFixture('xml', `${path}`); + const jsonPath = makePathToFixture('xml', `${path}.json`); + it(`read: ${xmlPath}`, () => { + // get the string data + const xml = readData(xmlPath); + assert.ok(xml, `Could not read ${xmlPath}`); + + // now, parse + const actual = reader.parse(xml); + assert.ok(actual, `Parser failed on ${xmlPath}`); + + // get the expected + const expect = readJson(jsonPath); + if (GEN_XML_FIXTURES) { + console.log(`GEN_XML_FIXTURES: writing ${jsonPath} from actual`); + writeJson(jsonPath, actual); + } else { + assert.ok(expect, `Could not read ${jsonPath} - run with env GEN_XML_FIXTURES=1 to update.`); + assert.deepEqual(actual, expect, `Mismatch of ${xmlPath} vs ${jsonPath}`); + } + }); + } + }); + } }); -describe('XML Generator Test', () => { - it('null test', () => assert.ok(new KeymanXMLGenerator())); +describe('XML Writer Test', () => { + it('null test', () => assert.ok(new KeymanXMLWriter({type: 'kpj'}))); }); From 7da7375ee513381b66e752aeb69a8f684fcf24d0 Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Fri, 27 Sep 2024 17:31:00 -0500 Subject: [PATCH 3/8] feat(common): xml: map \r\n to \n - help make the parsed form identical across platforms Fixes: #12208 --- developer/src/common/web/utils/test/test-xml-utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/developer/src/common/web/utils/test/test-xml-utils.ts b/developer/src/common/web/utils/test/test-xml-utils.ts index b06da2cc034..48461ee58b5 100644 --- a/developer/src/common/web/utils/test/test-xml-utils.ts +++ b/developer/src/common/web/utils/test/test-xml-utils.ts @@ -87,8 +87,8 @@ describe(`XML Reader Test ${GEN_XML_FIXTURES && '(update mode!)' || ''}`, () => const xml = readData(xmlPath); assert.ok(xml, `Could not read ${xmlPath}`); - // now, parse - const actual = reader.parse(xml); + // now, parse. subsitute endings for Win + const actual = reader.parse(xml.replace(/\r\n/g, '\n')); assert.ok(actual, `Parser failed on ${xmlPath}`); // get the expected From 3832ca1c122cd4828a8d39b8782733e068e0e95a Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Fri, 27 Sep 2024 17:49:32 -0500 Subject: [PATCH 4/8] feat(developer,common): use unified xml parser Subsystems changed: - ldml keyboard reader (main and test) - kpj - kvks - kmp compiler test: made the test-xml-utils less verbose about the pathnames Fixes: #12208 --- .../utils/src/types/kpj/kpj-file-reader.ts | 19 ++----- .../utils/src/types/kvks/kvks-file-reader.ts | 38 +++++--------- .../ldml-keyboard/ldml-keyboard-xml-reader.ts | 49 +++---------------- .../src/common/web/utils/src/xml-utils.ts | 2 +- .../common/web/utils/test/test-xml-utils.ts | 2 +- .../kmc-package/src/compiler/kmp-compiler.ts | 8 ++- 6 files changed, 26 insertions(+), 92 deletions(-) diff --git a/developer/src/common/web/utils/src/types/kpj/kpj-file-reader.ts b/developer/src/common/web/utils/src/types/kpj/kpj-file-reader.ts index d7768987531..4d60436d1c0 100644 --- a/developer/src/common/web/utils/src/types/kpj/kpj-file-reader.ts +++ b/developer/src/common/web/utils/src/types/kpj/kpj-file-reader.ts @@ -1,4 +1,4 @@ -import { xml2js } from '../../index.js'; +import { KeymanXMLReader } from '../../index.js'; import { KPJFile, KPJFileProject } from './kpj-file.js'; import { util } from '@keymanapp/common-types'; import { KeymanDeveloperProject, KeymanDeveloperProjectFile10, KeymanDeveloperProjectType } from './keyman-developer-project.js'; @@ -13,20 +13,9 @@ export class KPJFileReader { public read(file: Uint8Array): KPJFile { let data: KPJFile; - const parser = new xml2js.Parser({ - explicitArray: false, - mergeAttrs: false, - includeWhiteChars: false, - normalize: false, - emptyTag: '' - }); + data = new KeymanXMLReader({ type: 'kpj' }) + .parse(file.toString()); - parser.parseString(file, (e: unknown, r: unknown) => { - if(e) { - throw e; - } - data = r as KPJFile; - }); data = this.boxArrays(data); if(data.KeymanDeveloperProject?.Files?.File?.length) { for(const file of data.KeymanDeveloperProject?.Files?.File) { @@ -126,4 +115,4 @@ export class KPJFileReader { util.boxXmlArray(source.KeymanDeveloperProject.Files, 'File'); return source; } -} \ No newline at end of file +} diff --git a/developer/src/common/web/utils/src/types/kvks/kvks-file-reader.ts b/developer/src/common/web/utils/src/types/kvks/kvks-file-reader.ts index 6b2ad8e0a98..d0a43a6418d 100644 --- a/developer/src/common/web/utils/src/types/kvks/kvks-file-reader.ts +++ b/developer/src/common/web/utils/src/types/kvks/kvks-file-reader.ts @@ -1,5 +1,5 @@ import { SchemaValidators as SV, KvkFile, util, Constants } from '@keymanapp/common-types'; -import { xml2js } from '../../index.js' +import { KeymanXMLReader } from '../../index.js' import KVKSourceFile from './kvks-file.js'; const SchemaValidators = SV.default; import boxXmlArray = util.boxXmlArray; @@ -20,31 +20,15 @@ export default class KVKSFileReader { public read(file: Uint8Array): KVKSourceFile { let source: KVKSourceFile; - const parser = new xml2js.Parser({ - explicitArray: false, - mergeAttrs: false, - 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) => { - if(e) { - if(file.byteLength > 4 && file.subarray(0,3).every((v,i) => v == KVK_HEADER_IDENTIFIER_BYTES[i])) { - throw new Error('File appears to be a binary .kvk file', {cause: e}); - } - throw e; - }; - source = r as KVKSourceFile; - }); + try { + source = new KeymanXMLReader({ type: 'kvks' }) + .parse(file.toString()) as KVKSourceFile; + } catch(e) { + if(file.byteLength > 4 && file.subarray(0,3).every((v,i) => v == KVK_HEADER_IDENTIFIER_BYTES[i])) { + throw new Error('File appears to be a binary .kvk file', {cause: e}); + } + throw e; + } if(source) { source = this.boxArrays(source); this.cleanupFlags(source); @@ -197,4 +181,4 @@ export default class KVKSFileReader { } return 0; } -} \ No newline at end of file +} diff --git a/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts b/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts index d364c16d7dc..8e653054836 100644 --- a/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts +++ b/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts @@ -4,13 +4,12 @@ * Reads a LDML XML keyboard file into JS object tree and resolves imports */ import { SchemaValidators, util } from '@keymanapp/common-types'; -import { xml2js } from '../../index.js'; import { CommonTypesMessages } from '../../common-messages.js'; import { CompilerCallbacks } from '../../compiler-interfaces.js'; import { LDMLKeyboardXMLSourceFile, LKImport, ImportStatus } from './ldml-keyboard-xml.js'; import { constants } from '@keymanapp/ldml-keyboard-constants'; import { LDMLKeyboardTestDataXMLSourceFile, LKTTest, LKTTests } from './ldml-keyboard-testdata-xml.js'; - +import { KeymanXMLReader } from '@keymanapp/developer-utils'; import boxXmlArray = util.boxXmlArray; interface NameAndProps { @@ -262,26 +261,9 @@ export class LDMLKeyboardXMLSourceFileReader { } loadUnboxed(file: Uint8Array): LDMLKeyboardXMLSourceFile { - const source = (() => { - let a: LDMLKeyboardXMLSourceFile; - const 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 - // 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. - }); - const data = new TextDecoder().decode(file); - parser.parseString(data, (e: unknown, r: unknown) => { if(e) throw e; a = r as LDMLKeyboardXMLSourceFile }); // TODO-LDML: isn't 'e' the error? - return a; - })(); + const data = new TextDecoder().decode(file); + const source = new KeymanXMLReader({ type: 'keyboard3' }) + .parse(data) as LDMLKeyboardXMLSourceFile; return source; } @@ -311,27 +293,8 @@ export class LDMLKeyboardXMLSourceFileReader { } loadTestDataUnboxed(file: Uint8Array): any { - const source = (() => { - let a: any; - const parser = new xml2js.Parser({ - // explicitArray: false, - preserveChildrenOrder:true, // needed for test data - explicitChildren: true, // needed for test data - // mergeAttrs: true, - // includeWhiteChars: 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) => { a = r as any }); // TODO-LDML: isn't 'e' the error? - return a; // Why 'any'? Because we need to box up the $'s into proper properties. - })(); + const source = new KeymanXMLReader({ type: 'keyboard3-test' }) + .parse(file.toString()) as any; return source; } diff --git a/developer/src/common/web/utils/src/xml-utils.ts b/developer/src/common/web/utils/src/xml-utils.ts index 319b607fe73..a6a0849ca41 100644 --- a/developer/src/common/web/utils/src/xml-utils.ts +++ b/developer/src/common/web/utils/src/xml-utils.ts @@ -14,7 +14,7 @@ export class KeymanXMLReader { public constructor(public options: KeymanXMLOptions) { } - public parse(data: string): Object { + public parse(data: string): any { const parser = this.parser(); let a: any; parser.parseString(data, (e: unknown, r: unknown) => { if (e) throw e; a = r; }); diff --git a/developer/src/common/web/utils/test/test-xml-utils.ts b/developer/src/common/web/utils/test/test-xml-utils.ts index 48461ee58b5..c0a2855a4f6 100644 --- a/developer/src/common/web/utils/test/test-xml-utils.ts +++ b/developer/src/common/web/utils/test/test-xml-utils.ts @@ -82,7 +82,7 @@ describe(`XML Reader Test ${GEN_XML_FIXTURES && '(update mode!)' || ''}`, () => for (const path of paths) { const xmlPath = makePathToFixture('xml', `${path}`); const jsonPath = makePathToFixture('xml', `${path}.json`); - it(`read: ${xmlPath}`, () => { + it(`read: xml/${path}`, () => { // get the string data const xml = readData(xmlPath); assert.ok(xml, `Could not read ${xmlPath}`); diff --git a/developer/src/kmc-package/src/compiler/kmp-compiler.ts b/developer/src/kmc-package/src/compiler/kmp-compiler.ts index 04688d0e5a2..d187f4831e8 100644 --- a/developer/src/kmc-package/src/compiler/kmp-compiler.ts +++ b/developer/src/kmc-package/src/compiler/kmp-compiler.ts @@ -1,4 +1,4 @@ -import { xml2js } from '@keymanapp/developer-utils'; +import { KeymanXMLReader } from '@keymanapp/developer-utils'; import JSZip from 'jszip'; import KEYMAN_VERSION from "@keymanapp/keyman-version"; @@ -180,12 +180,10 @@ export class KmpCompiler implements KeymanCompiler { const kpsPackage = (() => { let a: KpsFile.KpsPackage; - let parser = new xml2js.Parser({ - explicitArray: false - }); try { - parser.parseString(data, (e: unknown, r: unknown) => { if(e) throw e; a = r as KpsFile.KpsPackage }); + a = new KeymanXMLReader({ type: 'kps' }) + .parse(data.toString()) as KpsFile.KpsPackage; } catch(e) { this.callbacks.reportMessage(PackageCompilerMessages.Error_InvalidPackageFile({e})); } From d540e4cda0f4cd9278415c9c89308dc11c7d9341 Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Fri, 27 Sep 2024 18:37:40 -0500 Subject: [PATCH 5/8] feat(common): unified xml writer test Fixes: #12208 --- .../src/common/web/utils/src/xml-utils.ts | 23 + .../test/fixtures/xml/khmer_angkor2.kvks | 206 +++ .../test/fixtures/xml/khmer_angkor2.kvks.json | 1172 +++++++++++++++++ .../common/web/utils/test/test-xml-utils.ts | 57 +- 4 files changed, 1449 insertions(+), 9 deletions(-) create mode 100644 developer/src/common/web/utils/test/fixtures/xml/khmer_angkor2.kvks create mode 100644 developer/src/common/web/utils/test/fixtures/xml/khmer_angkor2.kvks.json diff --git a/developer/src/common/web/utils/src/xml-utils.ts b/developer/src/common/web/utils/src/xml-utils.ts index a6a0849ca41..8b467b03422 100644 --- a/developer/src/common/web/utils/src/xml-utils.ts +++ b/developer/src/common/web/utils/src/xml-utils.ts @@ -93,7 +93,30 @@ export class KeymanXMLReader { /** wrapper for XML generation support */ export class KeymanXMLWriter { + write(data: any) : string { + const builder = this.builder(); + return builder.buildObject(data); + } constructor(public options: KeymanXMLOptions) { } + + public builder() { + switch(this.options.type) { + case 'kvks': + return new xml2js.Builder({ + allowSurrogateChars: true, + attrkey: '$', + charkey: '_', + xmldec: { + version: '1.0', + encoding: 'UTF-8', + standalone: true + } + }); + default: + /* c8 ignore next 1 */ + throw Error(`Internal error: unhandled XML type ${this.options.type}`); + } + } } diff --git a/developer/src/common/web/utils/test/fixtures/xml/khmer_angkor2.kvks b/developer/src/common/web/utils/test/fixtures/xml/khmer_angkor2.kvks new file mode 100644 index 00000000000..05eb838178f --- /dev/null +++ b/developer/src/common/web/utils/test/fixtures/xml/khmer_angkor2.kvks @@ -0,0 +1,206 @@ + + +
+ 10.0 + khmer_angkor + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + ] + [ + / + . + + + + & + + * + @ + \ + } + { + - + ÷ + : + , + + ; + < + # + > + × + $ + +   + + + + + + + + + + + + + ᧿ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + « + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ! + + " + + % + + ( + ) + + = + + + ? + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +  + + + » + + + +
\ No newline at end of file diff --git a/developer/src/common/web/utils/test/fixtures/xml/khmer_angkor2.kvks.json b/developer/src/common/web/utils/test/fixtures/xml/khmer_angkor2.kvks.json new file mode 100644 index 00000000000..beabd9e6737 --- /dev/null +++ b/developer/src/common/web/utils/test/fixtures/xml/khmer_angkor2.kvks.json @@ -0,0 +1,1172 @@ +{ + "visualkeyboard": { + + "header": { + + "version": "10.0", + "kbdname": "khmer_angkor", + "flags": { + + "usealtgr": "" + } + }, + "encoding": { + + "$": { + "name": "unicode", + "fontname": "Khmer Busra Kbd", + "fontsize": "16" + }, + "layer": [ + { + + "$": { + "shift": "RA" + }, + "key": [ + { + "_": "ឞ", + "$": { + "vkey": "K_B" + } + }, + { + "_": "ឝ", + "$": { + "vkey": "K_K" + } + }, + { + "_": "ៈ", + "$": { + "vkey": "K_QUOTE" + } + }, + { + "_": "ឳ", + "$": { + "vkey": "K_RBRKT" + } + }, + { + "_": "ឨ", + "$": { + "vkey": "K_T" + } + }, + { + "_": "ឩ", + "$": { + "vkey": "K_LBRKT" + } + }, + { + "_": "ឰ", + "$": { + "vkey": "K_P" + } + }, + { + "_": "ឫ", + "$": { + "vkey": "K_R" + } + }, + { + "_": "ឦ", + "$": { + "vkey": "K_I" + } + }, + { + "_": "ឱ", + "$": { + "vkey": "K_O" + } + }, + { + "_": "ឯ", + "$": { + "vkey": "K_E" + } + }, + { + "_": "", + "$": { + "vkey": "K_3" + } + }, + { + "_": "", + "$": { + "vkey": "K_W" + } + }, + { + "_": "ៜ", + "$": { + "vkey": "K_Q" + } + }, + { + "_": "", + "$": { + "vkey": "K_EQUAL" + } + }, + { + "_": "៖", + "$": { + "vkey": "K_COLON" + } + }, + { + "_": "៙", + "$": { + "vkey": "K_6" + } + }, + { + "_": "៚", + "$": { + "vkey": "K_7" + } + }, + { + "_": "", + "$": { + "vkey": "K_M" + } + }, + { + "_": "៘", + "$": { + "vkey": "K_L" + } + }, + { + "_": "‌", + "$": { + "vkey": "K_1" + } + }, + { + "_": "‍", + "$": { + "vkey": "K_BKQUOTE" + } + }, + { + "_": "]", + "$": { + "vkey": "K_U" + } + }, + { + "_": "[", + "$": { + "vkey": "K_Y" + } + }, + { + "_": "/", + "$": { + "vkey": "K_SLASH" + } + }, + { + "_": ".", + "$": { + "vkey": "K_PERIOD" + } + }, + { + "_": "‘", + "$": { + "vkey": "K_H" + } + }, + { + "_": "+", + "$": { + "vkey": "K_A" + } + }, + { + "_": "&", + "$": { + "vkey": "K_V" + } + }, + { + "_": "’", + "$": { + "vkey": "K_J" + } + }, + { + "_": "*", + "$": { + "vkey": "K_8" + } + }, + { + "_": "@", + "$": { + "vkey": "K_2" + } + }, + { + "_": "\\", + "$": { + "vkey": "K_BKSLASH" + } + }, + { + "_": "}", + "$": { + "vkey": "K_0" + } + }, + { + "_": "{", + "$": { + "vkey": "K_9" + } + }, + { + "_": "-", + "$": { + "vkey": "K_S" + } + }, + { + "_": "÷", + "$": { + "vkey": "K_F" + } + }, + { + "_": ":", + "$": { + "vkey": "K_G" + } + }, + { + "_": ",", + "$": { + "vkey": "K_COMMA" + } + }, + { + "_": "≈", + "$": { + "vkey": "K_HYPHEN" + } + }, + { + "_": ";", + "$": { + "vkey": "K_N" + } + }, + { + "_": "<", + "$": { + "vkey": "K_Z" + } + }, + { + "_": "#", + "$": { + "vkey": "K_C" + } + }, + { + "_": ">", + "$": { + "vkey": "K_X" + } + }, + { + "_": "×", + "$": { + "vkey": "K_D" + } + }, + { + "_": "$", + "$": { + "vkey": "K_4" + } + }, + { + "_": "€", + "$": { + "vkey": "K_5" + } + }, + { + "_": " ", + "$": { + "vkey": "K_SPACE" + } + } + ] + }, + { + + "$": { + "shift": "SRA" + }, + "key": [ + { + "_": "៸", + "$": { + "vkey": "K_8" + } + }, + { + "_": "៰", + "$": { + "vkey": "K_0" + } + }, + { + "_": "៱", + "$": { + "vkey": "K_1" + } + }, + { + "_": "៲", + "$": { + "vkey": "K_2" + } + }, + { + "_": "៳", + "$": { + "vkey": "K_3" + } + }, + { + "_": "៴", + "$": { + "vkey": "K_4" + } + }, + { + "_": "៵", + "$": { + "vkey": "K_5" + } + }, + { + "_": "៶", + "$": { + "vkey": "K_6" + } + }, + { + "_": "៷", + "$": { + "vkey": "K_7" + } + }, + { + "_": "៹", + "$": { + "vkey": "K_9" + } + }, + { + "_": "᧿", + "$": { + "vkey": "K_PERIOD" + } + }, + { + "_": "᧾", + "$": { + "vkey": "K_COMMA" + } + }, + { + "_": "᧪", + "$": { + "vkey": "K_LBRKT" + } + }, + { + "_": "᧫", + "$": { + "vkey": "K_RBRKT" + } + }, + { + "_": "᧶", + "$": { + "vkey": "K_QUOTE" + } + }, + { + "_": "᧵", + "$": { + "vkey": "K_COLON" + } + }, + { + "_": "᧬", + "$": { + "vkey": "K_A" + } + }, + { + "_": "᧷", + "$": { + "vkey": "K_Z" + } + }, + { + "_": "᧥", + "$": { + "vkey": "K_Y" + } + }, + { + "_": "᧸", + "$": { + "vkey": "K_X" + } + }, + { + "_": "᧡", + "$": { + "vkey": "K_W" + } + }, + { + "_": "᧻", + "$": { + "vkey": "K_B" + } + }, + { + "_": "᧹", + "$": { + "vkey": "K_C" + } + }, + { + "_": "᧮", + "$": { + "vkey": "K_D" + } + }, + { + "_": "᧢", + "$": { + "vkey": "K_E" + } + }, + { + "_": "᧯", + "$": { + "vkey": "K_F" + } + }, + { + "_": "᧰", + "$": { + "vkey": "K_G" + } + }, + { + "_": "᧦", + "$": { + "vkey": "K_U" + } + }, + { + "_": "᧱", + "$": { + "vkey": "K_H" + } + }, + { + "_": "᧤", + "$": { + "vkey": "K_T" + } + }, + { + "_": "᧭", + "$": { + "vkey": "K_S" + } + }, + { + "_": "᧣", + "$": { + "vkey": "K_R" + } + }, + { + "_": "᧧", + "$": { + "vkey": "K_I" + } + }, + { + "_": "᧠", + "$": { + "vkey": "K_Q" + } + }, + { + "_": "᧺", + "$": { + "vkey": "K_V" + } + }, + { + "_": "᧲", + "$": { + "vkey": "K_J" + } + }, + { + "_": "᧳", + "$": { + "vkey": "K_K" + } + }, + { + "_": "᧴", + "$": { + "vkey": "K_L" + } + }, + { + "_": "᧩", + "$": { + "vkey": "K_P" + } + }, + { + "_": "᧨", + "$": { + "vkey": "K_O" + } + }, + { + "_": "᧼", + "$": { + "vkey": "K_N" + } + }, + { + "_": "᧽", + "$": { + "vkey": "K_M" + } + } + ] + }, + { + + "$": { + "shift": "" + }, + "key": [ + { + "_": "​", + "$": { + "vkey": "K_SPACE" + } + }, + { + "_": "", + "$": { + "vkey": "K_QUOTE" + } + }, + { + "_": "", + "$": { + "vkey": "K_COMMA" + } + }, + { + "_": "ឥ", + "$": { + "vkey": "K_HYPHEN" + } + }, + { + "_": "។", + "$": { + "vkey": "K_PERIOD" + } + }, + { + "_": "", + "$": { + "vkey": "K_SLASH" + } + }, + { + "_": "០", + "$": { + "vkey": "K_0" + } + }, + { + "_": "១", + "$": { + "vkey": "K_1" + } + }, + { + "_": "២", + "$": { + "vkey": "K_2" + } + }, + { + "_": "៣", + "$": { + "vkey": "K_3" + } + }, + { + "_": "៤", + "$": { + "vkey": "K_4" + } + }, + { + "_": "៥", + "$": { + "vkey": "K_5" + } + }, + { + "_": "៦", + "$": { + "vkey": "K_6" + } + }, + { + "_": "៧", + "$": { + "vkey": "K_7" + } + }, + { + "_": "៨", + "$": { + "vkey": "K_8" + } + }, + { + "_": "៩", + "$": { + "vkey": "K_9" + } + }, + { + "_": "", + "$": { + "vkey": "K_COLON" + } + }, + { + "_": "ឲ", + "$": { + "vkey": "K_EQUAL" + } + }, + { + "_": "", + "$": { + "vkey": "K_LBRKT" + } + }, + { + "_": "ឮ", + "$": { + "vkey": "K_BKSLASH" + } + }, + { + "_": "ឪ", + "$": { + "vkey": "K_RBRKT" + } + }, + { + "_": "«", + "$": { + "vkey": "K_BKQUOTE" + } + }, + { + "_": "", + "$": { + "vkey": "K_A" + } + }, + { + "_": "ប", + "$": { + "vkey": "K_B" + } + }, + { + "_": "ច", + "$": { + "vkey": "K_C" + } + }, + { + "_": "ដ", + "$": { + "vkey": "K_D" + } + }, + { + "_": "", + "$": { + "vkey": "K_E" + } + }, + { + "_": "ថ", + "$": { + "vkey": "K_F" + } + }, + { + "_": "ង", + "$": { + "vkey": "K_G" + } + }, + { + "_": "ហ", + "$": { + "vkey": "K_H" + } + }, + { + "_": "", + "$": { + "vkey": "K_I" + } + }, + { + "_": "", + "$": { + "vkey": "K_J" + } + }, + { + "_": "ក", + "$": { + "vkey": "K_K" + } + }, + { + "_": "ល", + "$": { + "vkey": "K_L" + } + }, + { + "_": "ម", + "$": { + "vkey": "K_M" + } + }, + { + "_": "ន", + "$": { + "vkey": "K_N" + } + }, + { + "_": "", + "$": { + "vkey": "K_O" + } + }, + { + "_": "ផ", + "$": { + "vkey": "K_P" + } + }, + { + "_": "ឆ", + "$": { + "vkey": "K_Q" + } + }, + { + "_": "រ", + "$": { + "vkey": "K_R" + } + }, + { + "_": "ស", + "$": { + "vkey": "K_S" + } + }, + { + "_": "ត", + "$": { + "vkey": "K_T" + } + }, + { + "_": "", + "$": { + "vkey": "K_U" + } + }, + { + "_": "វ", + "$": { + "vkey": "K_V" + } + }, + { + "_": "", + "$": { + "vkey": "K_W" + } + }, + { + "_": "ខ", + "$": { + "vkey": "K_X" + } + }, + { + "_": "យ", + "$": { + "vkey": "K_Y" + } + }, + { + "_": "ឋ", + "$": { + "vkey": "K_Z" + } + } + ] + }, + { + + "$": { + "shift": "S" + }, + "key": [ + { + "_": "", + "$": { + "vkey": "K_SPACE" + } + }, + { + "_": "!", + "$": { + "vkey": "K_1" + } + }, + { + "_": "", + "$": { + "vkey": "K_QUOTE" + } + }, + { + "_": "\"", + "$": { + "vkey": "K_3" + } + }, + { + "_": "៛", + "$": { + "vkey": "K_4" + } + }, + { + "_": "%", + "$": { + "vkey": "K_5" + } + }, + { + "_": "", + "$": { + "vkey": "K_7" + } + }, + { + "_": "(", + "$": { + "vkey": "K_9" + } + }, + { + "_": ")", + "$": { + "vkey": "K_0" + } + }, + { + "_": "", + "$": { + "vkey": "K_8" + } + }, + { + "_": "=", + "$": { + "vkey": "K_EQUAL" + } + }, + { + "_": "", + "$": { + "vkey": "K_COLON" + } + }, + { + "_": "៕", + "$": { + "vkey": "K_PERIOD" + } + }, + { + "_": "?", + "$": { + "vkey": "K_SLASH" + } + }, + { + "_": "ៗ", + "$": { + "vkey": "K_2" + } + }, + { + "_": "", + "$": { + "vkey": "K_A" + } + }, + { + "_": "ព", + "$": { + "vkey": "K_B" + } + }, + { + "_": "ជ", + "$": { + "vkey": "K_C" + } + }, + { + "_": "ឌ", + "$": { + "vkey": "K_D" + } + }, + { + "_": "", + "$": { + "vkey": "K_E" + } + }, + { + "_": "ធ", + "$": { + "vkey": "K_F" + } + }, + { + "_": "អ", + "$": { + "vkey": "K_G" + } + }, + { + "_": "ះ", + "$": { + "vkey": "K_H" + } + }, + { + "_": "", + "$": { + "vkey": "K_I" + } + }, + { + "_": "ញ", + "$": { + "vkey": "K_J" + } + }, + { + "_": "គ", + "$": { + "vkey": "K_K" + } + }, + { + "_": "ឡ", + "$": { + "vkey": "K_L" + } + }, + { + "_": "", + "$": { + "vkey": "K_M" + } + }, + { + "_": "ណ", + "$": { + "vkey": "K_N" + } + }, + { + "_": "", + "$": { + "vkey": "K_O" + } + }, + { + "_": "ភ", + "$": { + "vkey": "K_P" + } + }, + { + "_": "ឈ", + "$": { + "vkey": "K_Q" + } + }, + { + "_": "ឬ", + "$": { + "vkey": "K_R" + } + }, + { + "_": "", + "$": { + "vkey": "K_S" + } + }, + { + "_": "ទ", + "$": { + "vkey": "K_T" + } + }, + { + "_": "", + "$": { + "vkey": "K_U" + } + }, + { + "_": "", + "$": { + "vkey": "K_V" + } + }, + { + "_": "", + "$": { + "vkey": "K_W" + } + }, + { + "_": "ឃ", + "$": { + "vkey": "K_X" + } + }, + { + "_": "", + "$": { + "vkey": "K_Y" + } + }, + { + "_": "ឍ", + "$": { + "vkey": "K_Z" + } + }, + { + "_": "", + "$": { + "vkey": "K_6" + } + }, + { + "_": "", + "$": { + "vkey": "K_HYPHEN" + } + }, + { + "_": "", + "$": { + "vkey": "K_LBRKT" + } + }, + { + "_": "ឭ", + "$": { + "vkey": "K_BKSLASH" + } + }, + { + "_": "ឧ", + "$": { + "vkey": "K_RBRKT" + } + }, + { + "_": "»", + "$": { + "vkey": "K_BKQUOTE" + } + }, + { + "_": "", + "$": { + "vkey": "K_COMMA" + } + } + ] + } + ] + } + } +} diff --git a/developer/src/common/web/utils/test/test-xml-utils.ts b/developer/src/common/web/utils/test/test-xml-utils.ts index c0a2855a4f6..8d32c7e1746 100644 --- a/developer/src/common/web/utils/test/test-xml-utils.ts +++ b/developer/src/common/web/utils/test/test-xml-utils.ts @@ -15,7 +15,7 @@ class Case { paths: string[]; }; -const read_cases : Case[] = [ +const read_cases: Case[] = [ { options: { type: 'keyboard3' }, paths: [ @@ -53,19 +53,29 @@ const read_cases : Case[] = [ }, ]; +const write_cases: Case[] = [ + { + options: { type: 'kvks' }, + paths: [ + // kvks + 'khmer_angkor2.kvks', // similar to the 'read case' with the similar name, except for whitespace differences and the prologue + ], + }, +]; + /** read data, or null */ -function readData(path: string) : string | null { +function readData(path: string): string | null { try { return readFileSync(path, 'utf-8'); - } catch(e) { + } catch (e) { if (e?.code !== 'ENOENT') console.error(`reading ${path}`, e); return null; } } -function readJson(path: string) : any | null { +function readJson(path: string): any | null { const data = readData(path); - if(data === null) return null; + if (data === null) return null; return JSON.parse(data); } @@ -75,7 +85,7 @@ function writeJson(path: string, data: any) { describe(`XML Reader Test ${GEN_XML_FIXTURES && '(update mode!)' || ''}`, () => { for (const c of read_cases) { - const {options, paths} = c; + const { options, paths } = c; describe(`test reading ${JSON.stringify(options)}`, () => { const reader = new KeymanXMLReader(options); assert.ok(reader); @@ -106,7 +116,36 @@ describe(`XML Reader Test ${GEN_XML_FIXTURES && '(update mode!)' || ''}`, () => } }); -describe('XML Writer Test', () => { - it('null test', () => assert.ok(new KeymanXMLWriter({type: 'kpj'}))); -}); +describe(`XML Writer Test ${GEN_XML_FIXTURES && '(update mode!)' || ''}`, () => { + for (const c of write_cases) { + const { options, paths } = c; + describe(`test writing ${JSON.stringify(options)}`, () => { + const writer = new KeymanXMLWriter(options); + assert.ok(writer); + for (const path of paths) { + const jsonPath = makePathToFixture('xml', `${path}.json`); + const xmlPath = makePathToFixture('xml', `${path}`); + it(`write: xml/${path}`, () => { + // get the object data + const data = readJson(jsonPath); + assert.ok(data, `Could not read input ${jsonPath}`); + + // now, write. + const actual = writer.write(data); + assert.ok(actual, `Writer failed on ${jsonPath}`); + + if (GEN_XML_FIXTURES) { + console.log(`GEN_XML_FIXTURES: writing ${xmlPath} from actual`); + writeFileSync(xmlPath, actual); + } else { + // get the expected data + const expect = readData(xmlPath).replace(/\r\n/g, '\n'); + assert.ok(expect, `Could not read expected output ${xmlPath} - run with env=GEN_XML_FIXTURES=1 to update`); + assert.deepEqual(actual.trim(), expect.trim(), `Mismatch of ${xmlPath} vs ${jsonPath}`); + } + }); + } + }); + } +}); From 1780b1ebf3e3d6b2da2cbc03e6e73df7b6b9d09d Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Fri, 27 Sep 2024 18:53:54 -0500 Subject: [PATCH 6/8] feat(developer,common): use unified xml writer Fixes: #12208 --- .../utils/src/types/kvks/kvks-file-writer.ts | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/developer/src/common/web/utils/src/types/kvks/kvks-file-writer.ts b/developer/src/common/web/utils/src/types/kvks/kvks-file-writer.ts index dd4962a8fad..6138403f6ef 100644 --- a/developer/src/common/web/utils/src/types/kvks/kvks-file-writer.ts +++ b/developer/src/common/web/utils/src/types/kvks/kvks-file-writer.ts @@ -1,6 +1,6 @@ import { VisualKeyboard as VK, Constants } from '@keymanapp/common-types'; import KVKSourceFile, { KVKSEncoding, KVKSFlags, KVKSKey, KVKSLayer } from './kvks-file.js'; -import { xml2js } from '../../index.js'; +import { KeymanXMLWriter } from '../../index.js'; import USVirtualKeyCodes = Constants.USVirtualKeyCodes; import VisualKeyboard = VK.VisualKeyboard; @@ -11,18 +11,6 @@ import VisualKeyboardShiftState = VK.VisualKeyboardShiftState; 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 - } - }) - const flags: KVKSFlags = {}; if(vk.header.flags & VisualKeyboardHeaderFlags.kvkhDisplayUnderlying) { flags.displayunderlying = ''; @@ -37,8 +25,6 @@ export default class KVKSFileWriter { flags.useunderlying = ''; } - - const kvks: KVKSourceFile = { visualkeyboard: { header: { @@ -105,7 +91,7 @@ export default class KVKSFileWriter { l.key.push(k); } - const result = builder.buildObject(kvks); + const result = new KeymanXMLWriter({type: 'kvks'}).write(kvks); return result; //Uint8Array.from(result); } @@ -124,4 +110,4 @@ export default class KVKSFileWriter { } return ''; } -} \ No newline at end of file +} From eba7079694a334edaaa3033832fafe9fb2c42452 Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Mon, 30 Sep 2024 10:00:46 -0500 Subject: [PATCH 7/8] chore(developer): update per style guide Fixes: #12208 --- .../src/common/web/utils/src/xml-utils.ts | 216 +++++++++--------- .../common/web/utils/test/test-xml-utils.ts | 8 + 2 files changed, 120 insertions(+), 104 deletions(-) diff --git a/developer/src/common/web/utils/src/xml-utils.ts b/developer/src/common/web/utils/src/xml-utils.ts index 8b467b03422..e902435cae3 100644 --- a/developer/src/common/web/utils/src/xml-utils.ts +++ b/developer/src/common/web/utils/src/xml-utils.ts @@ -1,122 +1,130 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by srl on 2024-09-27 + * + * Abstraction for XML reading and writing + */ + import { xml2js } from "./index.js"; export class KeymanXMLOptions { - type: 'keyboard3' // LDML - | 'keyboard3-test' // LDML - | 'kps' // - | 'kvks' // - | 'kpj' // // - ; + type: 'keyboard3' // LDML + | 'keyboard3-test' // LDML + | 'kps' // + | 'kvks' // + | 'kpj' // // + ; } /** wrapper for XML parsing support */ export class KeymanXMLReader { - public constructor(public options: KeymanXMLOptions) { - } + public constructor(public options: KeymanXMLOptions) { + } - public parse(data: string): any { - const parser = this.parser(); - let a: any; - parser.parseString(data, (e: unknown, r: unknown) => { if (e) throw e; a = r; }); - return a; - } + public parse(data: string): any { + const parser = this.parser(); + let a: any; + parser.parseString(data, (e: unknown, r: unknown) => { if (e) throw e; a = r; }); + return a; + } - public parser() { - const { type } = this.options; - switch (type) { - case 'keyboard3': - return 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 - // 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. - }); - case 'keyboard3-test': - return new xml2js.Parser({ - // explicitArray: false, - preserveChildrenOrder: true, // needed for test data - explicitChildren: true, // needed for test data - // mergeAttrs: true, - // includeWhiteChars: 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. - }); - case 'kps': - return new xml2js.Parser({ - explicitArray: false - }); - case 'kpj': - return new xml2js.Parser({ - explicitArray: false, - mergeAttrs: false, - includeWhiteChars: false, - normalize: false, - emptyTag: '' - }); - case 'kvks': - return new xml2js.Parser({ - explicitArray: false, - mergeAttrs: false, - 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. - }); - default: - /* c8 ignore next 1 */ - throw Error(`Internal error: unhandled XML type ${type}`); - } + public parser() { + const { type } = this.options; + switch (type) { + case 'keyboard3': + return 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 + // 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. + }); + case 'keyboard3-test': + return new xml2js.Parser({ + // explicitArray: false, + preserveChildrenOrder: true, // needed for test data + explicitChildren: true, // needed for test data + // mergeAttrs: true, + // includeWhiteChars: 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. + }); + case 'kps': + return new xml2js.Parser({ + explicitArray: false + }); + case 'kpj': + return new xml2js.Parser({ + explicitArray: false, + mergeAttrs: false, + includeWhiteChars: false, + normalize: false, + emptyTag: '' + }); + case 'kvks': + return new xml2js.Parser({ + explicitArray: false, + mergeAttrs: false, + 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. + }); + default: + /* c8 ignore next 1 */ + throw Error(`Internal error: unhandled XML type ${type}`); } + } } /** wrapper for XML generation support */ export class KeymanXMLWriter { - write(data: any) : string { - const builder = this.builder(); - return builder.buildObject(data); - } - constructor(public options: KeymanXMLOptions) { - } + write(data: any): string { + const builder = this.builder(); + return builder.buildObject(data); + } + constructor(public options: KeymanXMLOptions) { + } - public builder() { - switch(this.options.type) { - case 'kvks': - return new xml2js.Builder({ - allowSurrogateChars: true, - attrkey: '$', - charkey: '_', - xmldec: { - version: '1.0', - encoding: 'UTF-8', - standalone: true - } - }); - default: - /* c8 ignore next 1 */ - throw Error(`Internal error: unhandled XML type ${this.options.type}`); - } + public builder() { + switch (this.options.type) { + case 'kvks': + return new xml2js.Builder({ + allowSurrogateChars: true, + attrkey: '$', + charkey: '_', + xmldec: { + version: '1.0', + encoding: 'UTF-8', + standalone: true + } + }); + default: + /* c8 ignore next 1 */ + throw Error(`Internal error: unhandled XML type ${this.options.type}`); } + } } diff --git a/developer/src/common/web/utils/test/test-xml-utils.ts b/developer/src/common/web/utils/test/test-xml-utils.ts index 8d32c7e1746..d9e7f63ae3d 100644 --- a/developer/src/common/web/utils/test/test-xml-utils.ts +++ b/developer/src/common/web/utils/test/test-xml-utils.ts @@ -1,3 +1,11 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by srl on 2024-09-27 + * + * Test for abstraction for XML reading and writing + */ + import { assert } from 'chai'; import 'mocha'; import { env } from 'node:process'; From 158c4a7637c1b0a68bace21b3e94def5a3fcc7a6 Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Mon, 30 Sep 2024 13:25:38 -0500 Subject: [PATCH 8/8] feat(developer): update the unified xml parser per review - declarative syntax for options - workaround an issue where xml2js is mutating our options objects - improve tests Fixes: #12208 --- developer/src/common/web/utils/src/index.ts | 4 +- .../utils/src/types/kpj/kpj-file-reader.ts | 2 +- .../utils/src/types/kvks/kvks-file-reader.ts | 2 +- .../utils/src/types/kvks/kvks-file-writer.ts | 2 +- .../ldml-keyboard/ldml-keyboard-xml-reader.ts | 4 +- .../src/common/web/utils/src/xml-utils.ts | 180 +++++++++--------- .../common/web/utils/test/test-xml-utils.ts | 32 ++-- .../kmc-package/src/compiler/kmp-compiler.ts | 2 +- 8 files changed, 113 insertions(+), 115 deletions(-) diff --git a/developer/src/common/web/utils/src/index.ts b/developer/src/common/web/utils/src/index.ts index 7b83f682dff..21b59cc749f 100644 --- a/developer/src/common/web/utils/src/index.ts +++ b/developer/src/common/web/utils/src/index.ts @@ -44,6 +44,4 @@ export { defaultCompilerOptions, CompilerBaseOptions, CompilerCallbacks, Compile export { CommonTypesMessages } from './common-messages.js'; -export * as xml2js from './deps/xml2js/xml2js.js'; - -export { KeymanXMLOptions, KeymanXMLWriter, KeymanXMLReader } from './xml-utils.js'; +export { KeymanXMLType, KeymanXMLWriter, KeymanXMLReader } from './xml-utils.js'; diff --git a/developer/src/common/web/utils/src/types/kpj/kpj-file-reader.ts b/developer/src/common/web/utils/src/types/kpj/kpj-file-reader.ts index 4d60436d1c0..f86879b0068 100644 --- a/developer/src/common/web/utils/src/types/kpj/kpj-file-reader.ts +++ b/developer/src/common/web/utils/src/types/kpj/kpj-file-reader.ts @@ -13,7 +13,7 @@ export class KPJFileReader { public read(file: Uint8Array): KPJFile { let data: KPJFile; - data = new KeymanXMLReader({ type: 'kpj' }) + data = new KeymanXMLReader('kpj') .parse(file.toString()); data = this.boxArrays(data); diff --git a/developer/src/common/web/utils/src/types/kvks/kvks-file-reader.ts b/developer/src/common/web/utils/src/types/kvks/kvks-file-reader.ts index d0a43a6418d..9f2ca18f887 100644 --- a/developer/src/common/web/utils/src/types/kvks/kvks-file-reader.ts +++ b/developer/src/common/web/utils/src/types/kvks/kvks-file-reader.ts @@ -21,7 +21,7 @@ export default class KVKSFileReader { let source: KVKSourceFile; try { - source = new KeymanXMLReader({ type: 'kvks' }) + source = new KeymanXMLReader('kvks') .parse(file.toString()) as KVKSourceFile; } catch(e) { if(file.byteLength > 4 && file.subarray(0,3).every((v,i) => v == KVK_HEADER_IDENTIFIER_BYTES[i])) { diff --git a/developer/src/common/web/utils/src/types/kvks/kvks-file-writer.ts b/developer/src/common/web/utils/src/types/kvks/kvks-file-writer.ts index 6138403f6ef..70534ff4408 100644 --- a/developer/src/common/web/utils/src/types/kvks/kvks-file-writer.ts +++ b/developer/src/common/web/utils/src/types/kvks/kvks-file-writer.ts @@ -91,7 +91,7 @@ export default class KVKSFileWriter { l.key.push(k); } - const result = new KeymanXMLWriter({type: 'kvks'}).write(kvks); + const result = new KeymanXMLWriter('kvks').write(kvks); return result; //Uint8Array.from(result); } diff --git a/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts b/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts index 8e653054836..b0fe92be320 100644 --- a/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts +++ b/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts @@ -262,7 +262,7 @@ export class LDMLKeyboardXMLSourceFileReader { loadUnboxed(file: Uint8Array): LDMLKeyboardXMLSourceFile { const data = new TextDecoder().decode(file); - const source = new KeymanXMLReader({ type: 'keyboard3' }) + const source = new KeymanXMLReader('keyboard3') .parse(data) as LDMLKeyboardXMLSourceFile; return source; } @@ -293,7 +293,7 @@ export class LDMLKeyboardXMLSourceFileReader { } loadTestDataUnboxed(file: Uint8Array): any { - const source = new KeymanXMLReader({ type: 'keyboard3-test' }) + const source = new KeymanXMLReader('keyboardTest3') .parse(file.toString()) as any; return source; } diff --git a/developer/src/common/web/utils/src/xml-utils.ts b/developer/src/common/web/utils/src/xml-utils.ts index e902435cae3..6abcaa6d0f7 100644 --- a/developer/src/common/web/utils/src/xml-utils.ts +++ b/developer/src/common/web/utils/src/xml-utils.ts @@ -6,20 +6,84 @@ * Abstraction for XML reading and writing */ -import { xml2js } from "./index.js"; +import * as xml2js from "./deps/xml2js/xml2js.js"; -export class KeymanXMLOptions { - type: 'keyboard3' // LDML - | 'keyboard3-test' // LDML - | 'kps' // - | 'kvks' // - | 'kpj' // // - ; -} +export type KeymanXMLType = + 'keyboard3' // LDML + | 'keyboardTest3' // LDML + | 'kps' // + | 'kvks' // + | 'kpj' // + ; + +/** Bag of options, maximally one for each KeymanXMLType */ +type KemanXMLOptionsBag = { + [key in KeymanXMLType]?: any +}; + +/** map of options for the XML parser */ +const PARSER_OPTIONS: KemanXMLOptionsBag = { + 'keyboard3': { + 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 + // 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. + }, + 'keyboardTest3': { + preserveChildrenOrder: true, // needed for test data + explicitChildren: true, // needed for test data + }, + 'kps': { + explicitArray: false + }, + 'kpj': { + explicitArray: false, + mergeAttrs: false, + includeWhiteChars: false, + normalize: false, + emptyTag: '' + }, + 'kvks': { + explicitArray: false, + mergeAttrs: false, + 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. + }, +}; + +const GENERATOR_OPTIONS: KemanXMLOptionsBag = { + kvks: { + allowSurrogateChars: true, + attrkey: '$', + charkey: '_', + xmldec: { + version: '1.0', + encoding: 'UTF-8', + standalone: true + }, + }, +}; /** wrapper for XML parsing support */ export class KeymanXMLReader { - public constructor(public options: KeymanXMLOptions) { + public constructor(public type: KeymanXMLType) { } public parse(data: string): any { @@ -30,72 +94,16 @@ export class KeymanXMLReader { } public parser() { - const { type } = this.options; - switch (type) { - case 'keyboard3': - return 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 - // 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. - }); - case 'keyboard3-test': - return new xml2js.Parser({ - // explicitArray: false, - preserveChildrenOrder: true, // needed for test data - explicitChildren: true, // needed for test data - // mergeAttrs: true, - // includeWhiteChars: 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. - }); - case 'kps': - return new xml2js.Parser({ - explicitArray: false - }); - case 'kpj': - return new xml2js.Parser({ - explicitArray: false, - mergeAttrs: false, - includeWhiteChars: false, - normalize: false, - emptyTag: '' - }); - case 'kvks': - return new xml2js.Parser({ - explicitArray: false, - mergeAttrs: false, - 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. - }); - default: - /* c8 ignore next 1 */ - throw Error(`Internal error: unhandled XML type ${type}`); + let options = PARSER_OPTIONS[this.type]; + if (!options) { + /* c8 ignore next 1 */ + throw Error(`Internal error: unhandled XML type ${this.type}`); + } + options = Object.assign({}, options); // TODO: xml2js likes to mutate the options here. Shallow clone the object. + if (options.emptyTag) { + options.emptyTag = {}; // TODO: xml2js likes to mutate the options here. Reset it. } + return new xml2js.Parser(options); } } @@ -105,26 +113,16 @@ export class KeymanXMLWriter { const builder = this.builder(); return builder.buildObject(data); } - constructor(public options: KeymanXMLOptions) { + constructor(public type: KeymanXMLType) { } public builder() { - switch (this.options.type) { - case 'kvks': - return new xml2js.Builder({ - allowSurrogateChars: true, - attrkey: '$', - charkey: '_', - xmldec: { - version: '1.0', - encoding: 'UTF-8', - standalone: true - } - }); - default: - /* c8 ignore next 1 */ - throw Error(`Internal error: unhandled XML type ${this.options.type}`); + const options = GENERATOR_OPTIONS[this.type]; + if (!options) { + /* c8 ignore next 1 */ + throw Error(`Internal error: unhandled XML type ${this.type}`); } + return new xml2js.Builder(Object.assign({}, options)); // Shallow clone in case the options are mutated. } } diff --git a/developer/src/common/web/utils/test/test-xml-utils.ts b/developer/src/common/web/utils/test/test-xml-utils.ts index d9e7f63ae3d..fbe32f8da4f 100644 --- a/developer/src/common/web/utils/test/test-xml-utils.ts +++ b/developer/src/common/web/utils/test/test-xml-utils.ts @@ -12,20 +12,20 @@ import { env } from 'node:process'; import { readFileSync, writeFileSync } from 'node:fs'; -import { KeymanXMLOptions, KeymanXMLReader, KeymanXMLWriter } from '../src/xml-utils.js'; +import { KeymanXMLType, KeymanXMLReader, KeymanXMLWriter } from '../src/xml-utils.js'; import { makePathToFixture } from './helpers/index.js'; // if true, attempt to WRITE the fixtures const { GEN_XML_FIXTURES } = env; class Case { - options: KeymanXMLOptions; + type: KeymanXMLType; paths: string[]; }; const read_cases: Case[] = [ { - options: { type: 'keyboard3' }, + type: 'keyboard3', paths: [ // keyboards 'disp_maximal.xml', @@ -34,26 +34,26 @@ const read_cases: Case[] = [ 'tran_fail-empty.xml', ], }, { - options: { type: 'keyboard3-test' }, + type: 'keyboardTest3', paths: [ // keyboard test 'k_020_fr-test.xml', ], }, { - options: { type: 'kvks' }, + type: 'kvks', paths: [ // kvks 'khmer_angkor.kvks', ], }, { - options: { type: 'kps' }, + type: 'kps', paths: [ // kps 'test_valid.kps', // 'error_invalid_package_file.kps', ], }, { - options: { type: 'kpj' }, + type: 'kpj', paths: [ // kpj 'khmer_angkor.kpj', @@ -63,7 +63,7 @@ const read_cases: Case[] = [ const write_cases: Case[] = [ { - options: { type: 'kvks' }, + type: 'kvks', paths: [ // kvks 'khmer_angkor2.kvks', // similar to the 'read case' with the similar name, except for whitespace differences and the prologue @@ -93,10 +93,8 @@ function writeJson(path: string, data: any) { describe(`XML Reader Test ${GEN_XML_FIXTURES && '(update mode!)' || ''}`, () => { for (const c of read_cases) { - const { options, paths } = c; - describe(`test reading ${JSON.stringify(options)}`, () => { - const reader = new KeymanXMLReader(options); - assert.ok(reader); + const { type, paths } = c; + describe(`test reading ${type}`, () => { for (const path of paths) { const xmlPath = makePathToFixture('xml', `${path}`); const jsonPath = makePathToFixture('xml', `${path}.json`); @@ -105,12 +103,16 @@ describe(`XML Reader Test ${GEN_XML_FIXTURES && '(update mode!)' || ''}`, () => const xml = readData(xmlPath); assert.ok(xml, `Could not read ${xmlPath}`); + const reader = new KeymanXMLReader(type); + assert.ok(reader); + // now, parse. subsitute endings for Win const actual = reader.parse(xml.replace(/\r\n/g, '\n')); assert.ok(actual, `Parser failed on ${xmlPath}`); // get the expected const expect = readJson(jsonPath); + if (GEN_XML_FIXTURES) { console.log(`GEN_XML_FIXTURES: writing ${jsonPath} from actual`); writeJson(jsonPath, actual); @@ -127,9 +129,9 @@ describe(`XML Reader Test ${GEN_XML_FIXTURES && '(update mode!)' || ''}`, () => describe(`XML Writer Test ${GEN_XML_FIXTURES && '(update mode!)' || ''}`, () => { for (const c of write_cases) { - const { options, paths } = c; - describe(`test writing ${JSON.stringify(options)}`, () => { - const writer = new KeymanXMLWriter(options); + const { type, paths } = c; + describe(`test writing ${type}`, () => { + const writer = new KeymanXMLWriter(type); assert.ok(writer); for (const path of paths) { const jsonPath = makePathToFixture('xml', `${path}.json`); diff --git a/developer/src/kmc-package/src/compiler/kmp-compiler.ts b/developer/src/kmc-package/src/compiler/kmp-compiler.ts index d187f4831e8..1708d69704c 100644 --- a/developer/src/kmc-package/src/compiler/kmp-compiler.ts +++ b/developer/src/kmc-package/src/compiler/kmp-compiler.ts @@ -182,7 +182,7 @@ export class KmpCompiler implements KeymanCompiler { let a: KpsFile.KpsPackage; try { - a = new KeymanXMLReader({ type: 'kps' }) + a = new KeymanXMLReader('kps') .parse(data.toString()) as KpsFile.KpsPackage; } catch(e) { this.callbacks.reportMessage(PackageCompilerMessages.Error_InvalidPackageFile({e}));