Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(common): unified XML parser/writer #12482

Merged
merged 8 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions developer/src/common/web/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,5 @@ 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';
19 changes: 4 additions & 15 deletions developer/src/common/web/utils/src/types/kpj/kpj-file-reader.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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) {
Expand Down Expand Up @@ -126,4 +115,4 @@ export class KPJFileReader {
util.boxXmlArray(source.KeymanDeveloperProject.Files, 'File');
return source;
}
}
}
38 changes: 11 additions & 27 deletions developer/src/common/web/utils/src/types/kvks/kvks-file-reader.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -197,4 +181,4 @@ export default class KVKSFileReader {
}
return 0;
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 = '';
Expand All @@ -37,8 +25,6 @@ export default class KVKSFileWriter {
flags.useunderlying = '';
}



const kvks: KVKSourceFile = {
visualkeyboard: {
header: {
Expand Down Expand Up @@ -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);
}

Expand All @@ -124,4 +110,4 @@ export default class KVKSFileWriter {
}
return '';
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand Down
122 changes: 122 additions & 0 deletions developer/src/common/web/utils/src/xml-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { xml2js } from "./index.js";
srl295 marked this conversation as resolved.
Show resolved Hide resolved

export class KeymanXMLOptions {
type: 'keyboard3' // LDML <keyboard3>
srl295 marked this conversation as resolved.
Show resolved Hide resolved
| 'keyboard3-test' // LDML <keyboardTest3>
| 'kps' // <Package>
| 'kvks' // <visualkeyboard>
| 'kpj' // // <KeymanDeveloperProject>
;
srl295 marked this conversation as resolved.
Show resolved Hide resolved
}

/** wrapper for XML parsing support */
export class KeymanXMLReader {
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 parser() {
const { type } = this.options;
switch (type) {
case 'keyboard3':
srl295 marked this conversation as resolved.
Show resolved Hide resolved
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.
srl295 marked this conversation as resolved.
Show resolved Hide resolved
});
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) {
}

public builder() {
switch(this.options.type) {
srl295 marked this conversation as resolved.
Show resolved Hide resolved
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}`);
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>

<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="mt" conformsTo="45">
<info name="disp-maximal"/>

<displays>
<display keyId="g" display="(g)"/>
<display output="f" display="(f)"/> <!-- Note: in opposite lexical order, as the compiler will sort -->
<display output="${eee}" display="(${eee})"/>
<displayOptions baseCharacter="x" />
</displays>

<variables>
<string id="eee" value="e" />
</variables>
</keyboard3>
Loading
Loading