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