diff --git a/developer/src/common/web/utils/src/common-messages.ts b/developer/src/common/web/utils/src/common-messages.ts
index 5b3fd7d5070..724dadf9cd8 100644
--- a/developer/src/common/web/utils/src/common-messages.ts
+++ b/developer/src/common/web/utils/src/common-messages.ts
@@ -1,5 +1,5 @@
/*
- * Keyman is copyright (C) SIL International. MIT License.
+ * Keyman is copyright (C) SIL Global. MIT License.
*/
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageDef as def, CompilerMessageSpec as m } from './compiler-interfaces.js';
import { constants } from '@keymanapp/ldml-keyboard-constants';
@@ -50,4 +50,10 @@ export class CommonTypesMessages {
static ERROR_InvalidXml = SevError | 0x0008;
static Error_InvalidXml = (o:{e: any}) =>
m(this.ERROR_InvalidXml, `The XML file could not be read: ${(o.e ?? '').toString()}`);
+
+ static ERROR_InvalidPackageFile = SevError | 0x0009;
+ static Error_InvalidPackageFile = (o:{e:any}) => m(
+ this.ERROR_InvalidPackageFile,
+ `Package source file is invalid: ${(o.e ?? 'unknown error').toString()}`
+ );
};
diff --git a/developer/src/common/web/utils/src/index.ts b/developer/src/common/web/utils/src/index.ts
index 21b59cc749f..aca8ba1a59e 100644
--- a/developer/src/common/web/utils/src/index.ts
+++ b/developer/src/common/web/utils/src/index.ts
@@ -1,3 +1,7 @@
+/*
+ * Keyman is copyright (C) SIL Global. MIT License.
+ */
+
export { validateMITLicense } from './utils/validate-mit-license.js';
export { KeymanSentry, SentryNodeOptions } from './utils/KeymanSentry.js';
export { getOption, loadOptions, clearOptions } from './utils/options.js';
@@ -10,6 +14,8 @@ export { KeymanDeveloperProject, KeymanDeveloperProjectFile, KeymanDeveloperProj
export { isValidEmail } from './is-valid-email.js';
export * as KpsFile from './types/kps/kps-file.js';
+export { KpsFileReader } from './types/kps/kps-file-reader.js';
+export { KpsFileWriter } from './types/kps/kps-file-writer.js';
export { default as KvksFileReader } from './types/kvks/kvks-file-reader.js';
export { default as KvksFileWriter } from './types/kvks/kvks-file-writer.js';
diff --git a/developer/src/common/web/utils/src/types/kps/kps-file-reader.ts b/developer/src/common/web/utils/src/types/kps/kps-file-reader.ts
new file mode 100644
index 00000000000..ad77d447d6d
--- /dev/null
+++ b/developer/src/common/web/utils/src/types/kps/kps-file-reader.ts
@@ -0,0 +1,71 @@
+/*
+ * Keyman is copyright (C) SIL Global. MIT License.
+ *
+ * Created by mcdurdin on 2024-10-16
+ *
+ * XML reader for .kps file format
+ */
+
+import { util } from '@keymanapp/common-types';
+import boxXmlArray = util.boxXmlArray;
+
+import { CommonTypesMessages } from "../../common-messages.js";
+import { CompilerCallbacks } from "../../compiler-interfaces.js";
+import { KeymanXMLReader } from "../../xml-utils.js";
+import { KpsPackage } from "./kps-file.js";
+
+export class KpsFileReader {
+ constructor(private callbacks: CompilerCallbacks) {
+
+ }
+
+ public read(file: Uint8Array): KpsPackage {
+ const data = new TextDecoder().decode(file);
+
+ const kpsPackage = (() => {
+ let a: KpsPackage;
+
+ try {
+ a = new KeymanXMLReader('kps')
+ .parse(data) as KpsPackage;
+ } catch(e) {
+ this.callbacks.reportMessage(CommonTypesMessages.Error_InvalidPackageFile({e}));
+ }
+ return a;
+ })();
+
+ if(!kpsPackage) {
+ return null;
+ }
+
+ return this.boxArrays(kpsPackage);
+ }
+
+ private boxArrays(data: KpsPackage): KpsPackage {
+ boxXmlArray(data.Package?.Files, 'File');
+ boxXmlArray(data.Package?.Keyboards, 'Keyboard');
+ boxXmlArray(data.Package?.LexicalModels, 'LexicalModel');
+ boxXmlArray(data.Package?.RelatedPackages, 'RelatedPackage');
+
+ if(data.Package?.Keyboards?.Keyboard) {
+ for(const k of data.Package.Keyboards.Keyboard) {
+ boxXmlArray(k.Examples, 'Example');
+ boxXmlArray(k.Languages, 'Language');
+ boxXmlArray(k.WebDisplayFonts, 'Font');
+ boxXmlArray(k.WebOSKFonts, 'Font');
+ }
+ }
+
+ if(data.Package?.LexicalModels?.LexicalModel) {
+ for(const lm of data.Package.LexicalModels.LexicalModel) {
+ boxXmlArray(lm.Languages, 'Language');
+ }
+ }
+
+ boxXmlArray(data.Package?.RelatedPackages, 'RelatedPackage');
+ boxXmlArray(data.Package?.StartMenu?.Items, 'Item');
+ boxXmlArray(data.Package?.Strings, 'String');
+
+ return data;
+ }
+};
diff --git a/developer/src/common/web/utils/src/types/kps/kps-file-writer.ts b/developer/src/common/web/utils/src/types/kps/kps-file-writer.ts
new file mode 100644
index 00000000000..c3d13422877
--- /dev/null
+++ b/developer/src/common/web/utils/src/types/kps/kps-file-writer.ts
@@ -0,0 +1,17 @@
+/*
+ * Keyman is copyright (C) SIL Global. MIT License.
+ *
+ * Created by mcdurdin on 2024-10-16
+ *
+ * XML writer for .kps file format
+ */
+
+import { KeymanXMLWriter } from '../../index.js';
+import { KpsPackage } from './kps-file.js';
+
+export class KpsFileWriter {
+ public write(kpsFile: KpsPackage): string {
+ const result = new KeymanXMLWriter('kps').write(kpsFile);
+ return result;
+ }
+}
diff --git a/developer/src/common/web/utils/src/types/kps/kps-file.ts b/developer/src/common/web/utils/src/types/kps/kps-file.ts
index 6ab578d0bc2..7724f4a6490 100644
--- a/developer/src/common/web/utils/src/types/kps/kps-file.ts
+++ b/developer/src/common/web/utils/src/types/kps/kps-file.ts
@@ -1,13 +1,16 @@
+/*
+ * Keyman is copyright (C) SIL Global. MIT License.
+ */
+
//
-// The interfaces in this file are designed with reference to the
-// mapped structures produced by xml2js when passed a .kps file.
+// The interfaces in this file are designed with reference to the mapped
+// structures produced by fast-xml-parser when passed a .kps file.
//
// A few notes:
//
-// * Casing is updated to camelCase during load (leaving `iD` as a
-// mixed up beastie).
// * Arrays are buried a layer too deep (e.g.
-// leads to KpsFiles.KpsFile[]
+// leads to KpsFiles.KpsFile[]. These are boxed automatically by
+// KpsFileReader.
// * Properties such as used in Info Items use `_` and `$` and must be
// extracted.
// * Strings element is not yet checked to be correct
@@ -63,7 +66,7 @@ export interface KpsFileInfoItem {
}
export interface KpsFileContentFiles {
- File: KpsFileContentFile[] | KpsFileContentFile;
+ File: KpsFileContentFile[];
}
export interface KpsFileContentFile {
@@ -83,11 +86,11 @@ export interface KpsFileLexicalModel {
}
export interface KpsFileLexicalModels {
- LexicalModel: KpsFileLexicalModel[] | KpsFileLexicalModel;
+ LexicalModel: KpsFileLexicalModel[];
}
export interface KpsFileLanguages {
- Language: KpsFileLanguage[] | KpsFileLanguage;
+ Language: KpsFileLanguage[];
}
export interface KpsFileLanguage {
@@ -96,7 +99,7 @@ export interface KpsFileLanguage {
}
export interface KpsFileRelatedPackages {
- RelatedPackage: KpsFileRelatedPackage | KpsFileRelatedPackage[];
+ RelatedPackage: KpsFileRelatedPackage[];
}
export interface KpsFileRelatedPackage {
@@ -129,7 +132,7 @@ export interface KpsFileKeyboard {
}
export interface KpsFileFonts {
- Font: KpsFileFont[] | KpsFileFont;
+ Font: KpsFileFont[];
}
export interface KpsFileFont {
@@ -139,7 +142,7 @@ export interface KpsFileFont {
}
export interface KpsFileKeyboards {
- Keyboard: KpsFileKeyboard[] | KpsFileKeyboard;
+ Keyboard: KpsFileKeyboard[];
}
export interface KpsFileStartMenu {
@@ -157,11 +160,11 @@ export interface KpsFileStartMenuItem {
}
export interface KpsFileStartMenuItems {
- Item: KpsFileStartMenuItem[] | KpsFileStartMenuItem;
+ Item: KpsFileStartMenuItem[];
}
export interface KpsFileStrings {
- String: KpsFileString[] | KpsFileString;
+ String: KpsFileString[];
}
export interface KpsFileString {
@@ -172,7 +175,7 @@ export interface KpsFileString {
}
export interface KpsFileLanguageExamples {
- Example: KpsFileLanguageExample | KpsFileLanguageExample[];
+ Example: KpsFileLanguageExample[];
}
/**
diff --git a/developer/src/common/web/utils/src/xml-utils.ts b/developer/src/common/web/utils/src/xml-utils.ts
index 999da59fde3..022328ff942 100644
--- a/developer/src/common/web/utils/src/xml-utils.ts
+++ b/developer/src/common/web/utils/src/xml-utils.ts
@@ -100,6 +100,20 @@ const GENERATOR_OPTIONS: KeymanXMLOptionsBag = {
textNodeName: '_',
suppressEmptyNode: true,
},
+ kpj: {
+ attributeNamePrefix: '$',
+ ignoreAttributes: false,
+ format: true,
+ textNodeName: '_',
+ suppressEmptyNode: true,
+ },
+ kps: {
+ attributeNamePrefix: '$',
+ ignoreAttributes: false,
+ format: true,
+ textNodeName: '_',
+ suppressEmptyNode: true,
+ },
};
/** wrapper for XML parsing support */
diff --git a/developer/src/kmc-package/test/fixtures/invalid/error_invalid_package_file.kps b/developer/src/common/web/utils/test/fixtures/kps/error_invalid_package_file.kps
similarity index 100%
rename from developer/src/kmc-package/test/fixtures/invalid/error_invalid_package_file.kps
rename to developer/src/common/web/utils/test/fixtures/kps/error_invalid_package_file.kps
diff --git a/developer/src/common/web/utils/test/fixtures/kps/khmer_angkor.kps b/developer/src/common/web/utils/test/fixtures/kps/khmer_angkor.kps
new file mode 100644
index 00000000000..cb4628a0f56
--- /dev/null
+++ b/developer/src/common/web/utils/test/fixtures/kps/khmer_angkor.kps
@@ -0,0 +1,132 @@
+
+
+
+ 15.0.266.0
+ 7.0
+
+
+
+ readme.htm
+ splash.gif
+ welcome.htm
+
+
+
+
+
+
+
+
+
+ Khmer Angkor
+ © 2015-2022 SIL International
+ Makara Sok
+
+ https://keyman.com/keyboards/khmer_angkor
+
+
+
+ ..\build\khmer_angkor.js
+ File khmer_angkor.js
+ 0
+ .js
+
+
+ ..\build\khmer_angkor.kvk
+ File khmer_angkor.kvk
+ 0
+ .kvk
+
+
+ ..\build\khmer_angkor.kmx
+ Keyboard Khmer Angkor
+ 0
+ .kmx
+
+
+ welcome\keyboard_layout.png
+ File keyboard_layout.png
+ 0
+ .png
+
+
+ welcome\welcome.htm
+ File welcome.htm
+ 0
+ .htm
+
+
+ ..\shared\fonts\khmer\mondulkiri\FONTLOG.txt
+ File FONTLOG.txt
+ 0
+ .txt
+
+
+ ..\shared\fonts\khmer\mondulkiri\Mondulkiri-R.ttf
+ Font Khmer Mondulkiri
+ 0
+ .ttf
+
+
+ ..\shared\fonts\khmer\mondulkiri\OFL.txt
+ File OFL.txt
+ 0
+ .txt
+
+
+ ..\shared\fonts\khmer\mondulkiri\OFL-FAQ.txt
+ File OFL-FAQ.txt
+ 0
+ .txt
+
+
+ welcome\KAK_Documentation_EN.pdf
+ File KAK_Documentation_EN.pdf
+ 0
+ .pdf
+
+
+ welcome\KAK_Documentation_KH.pdf
+ File KAK_Documentation_KH.pdf
+ 0
+ .pdf
+
+
+ readme.htm
+ File readme.htm
+ 0
+ .htm
+
+
+ welcome\image002.png
+ File image002.png
+ 0
+ .png
+
+
+ ..\shared\fonts\khmer\busrakbd\khmer_busra_kbd.ttf
+ Font KhmerBusraKbd
+ 0
+ .ttf
+
+
+ splash.gif
+ File splash.gif
+ 0
+ .gif
+
+
+
+
+ Khmer Angkor
+ khmer_angkor
+ 1.3
+ ..\shared\fonts\khmer\busrakbd\khmer_busra_kbd.ttf
+ ..\shared\fonts\khmer\mondulkiri\Mondulkiri-R.ttf
+
+ Central Khmer (Khmer, Cambodia)
+
+
+
+
+
diff --git a/developer/src/common/web/utils/test/kps/test-kps-file-reader.ts b/developer/src/common/web/utils/test/kps/test-kps-file-reader.ts
new file mode 100644
index 00000000000..1b809f39d07
--- /dev/null
+++ b/developer/src/common/web/utils/test/kps/test-kps-file-reader.ts
@@ -0,0 +1,85 @@
+/*
+ * Keyman is copyright (C) SIL Global. MIT License.
+ *
+ * Created by mcdurdin on 2024-10-16
+ */
+
+import * as fs from 'fs';
+import 'mocha';
+import {assert} from 'chai';
+
+import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers';
+
+import { makePathToFixture } from '../helpers/index.js';
+
+import { KpsFileReader } from "../../src/types/kps/kps-file-reader.js";
+import { KpsFileWriter } from '../../src/types/kps/kps-file-writer.js';
+import { CommonTypesMessages } from '../../src/common-messages.js';
+
+const callbacks = new TestCompilerCallbacks();
+
+describe('kps-file-reader', function () {
+ it('kps-file-reader should read a valid file', function() {
+ const input = fs.readFileSync(makePathToFixture('kps', 'khmer_angkor.kps'));
+ const reader = new KpsFileReader(callbacks);
+ const kps = reader.read(input);
+
+ assert.equal(kps.Package.System.FileVersion, '7.0');
+ assert.equal(kps.Package.System.KeymanDeveloperVersion, '15.0.266.0');
+ assert.equal(kps.Package.Info.Author._, 'Makara Sok');
+ assert.equal(kps.Package.Info.Author.$.URL, 'mailto:makara_sok@sil.org');
+ assert.equal(kps.Package.Info.Copyright._, '© 2015-2022 SIL International');
+ assert.isUndefined(kps.Package.Info.Description);
+ assert.equal(kps.Package.Info.Name._, 'Khmer Angkor');
+ assert.isUndefined(kps.Package.Info.Version._);
+ assert.equal(kps.Package.Info.WebSite._, 'https://keyman.com/keyboards/khmer_angkor');
+ assert.equal(kps.Package.Info.WebSite.$.URL, 'https://keyman.com/keyboards/khmer_angkor');
+
+ assert.isUndefined(kps.Package.Options.LicenseFile);
+ assert.equal(kps.Package.Options.FollowKeyboardVersion, '');
+ assert.equal(kps.Package.Options.ReadMeFile, 'readme.htm');
+ assert.equal(kps.Package.Options.WelcomeFile, 'welcome.htm');
+ assert.equal(kps.Package.Options.GraphicFile, 'splash.gif');
+
+ assert.lengthOf(kps.Package.Files.File, 15);
+ assert.equal(kps.Package.Files.File[0].Name, '..\\build\\khmer_angkor.js');
+ assert.equal(kps.Package.Files.File[1].Name, '..\\build\\khmer_angkor.kvk');
+
+ assert.lengthOf(kps.Package.Keyboards.Keyboard, 1);
+ assert.equal(kps.Package.Keyboards.Keyboard[0].ID, 'khmer_angkor');
+ assert.equal(kps.Package.Keyboards.Keyboard[0].Name, 'Khmer Angkor');
+ assert.equal(kps.Package.Keyboards.Keyboard[0].Version, '1.3');
+
+ assert.lengthOf(kps.Package.Keyboards.Keyboard[0].Languages.Language, 1);
+ assert.equal(kps.Package.Keyboards.Keyboard[0].Languages.Language[0]._, 'Central Khmer (Khmer, Cambodia)');
+ assert.equal(kps.Package.Keyboards.Keyboard[0].Languages.Language[0].$.ID, 'km');
+
+ assert.equal(kps.Package.Keyboards.Keyboard[0].OSKFont, '..\\shared\\fonts\\khmer\\busrakbd\\khmer_busra_kbd.ttf');
+ assert.equal(kps.Package.Keyboards.Keyboard[0].DisplayFont, '..\\shared\\fonts\\khmer\\mondulkiri\\Mondulkiri-R.ttf');
+ });
+
+ it('kps-file-reader should round-trip with kps-file-writer', function() {
+ const input = fs.readFileSync(makePathToFixture('kps', 'khmer_angkor.kps'));
+ const reader = new KpsFileReader(callbacks);
+ const kps = reader.read(input);
+
+ const writer = new KpsFileWriter();
+ const output = writer.write(kps);
+
+ // Round Trip
+ const kps2 = reader.read(new TextEncoder().encode(output));
+ assert.deepEqual(kps2, kps);
+ });
+
+ // ERROR_InvalidPackageFile
+
+ it('should generate ERROR_InvalidPackageFile if package source file contains invalid XML', async function() {
+ const input = fs.readFileSync(makePathToFixture('kps', 'error_invalid_package_file.kps'));
+ const reader = new KpsFileReader(callbacks);
+ const kps = reader.read(input);
+
+ assert.isNull(kps);
+ assert.lengthOf(callbacks.messages, 1);
+ assert.isTrue(callbacks.hasMessage(CommonTypesMessages.ERROR_InvalidPackageFile));
+ });
+});
diff --git a/developer/src/kmc-package/src/compiler/kmp-compiler.ts b/developer/src/kmc-package/src/compiler/kmp-compiler.ts
index e256c37b88d..3b50ee7ca39 100644
--- a/developer/src/kmc-package/src/compiler/kmp-compiler.ts
+++ b/developer/src/kmc-package/src/compiler/kmp-compiler.ts
@@ -1,9 +1,14 @@
-import { KeymanXMLReader } from '@keymanapp/developer-utils';
+/*
+ * Keyman is copyright (C) SIL Global. MIT License.
+ */
+
import JSZip from 'jszip';
-import KEYMAN_VERSION from "@keymanapp/keyman-version";
+import { KpsFileReader } from '@keymanapp/developer-utils';
+import KEYMAN_VERSION from "@keymanapp/keyman-version";
import { KmpJsonFile, SchemaValidators, KeymanFileTypes, KvkFile } from '@keymanapp/common-types';
import { CompilerCallbacks, KpsFile, KeymanCompiler, CompilerOptions, KeymanCompilerResult, KeymanCompilerArtifacts, KeymanCompilerArtifact } from '@keymanapp/developer-utils';
+
import { PackageCompilerMessages } from './package-compiler-messages.js';
import { PackageMetadataCollector } from './package-metadata-collector.js';
import { KmpInfWriter } from './kmp-inf-writer.js';
@@ -147,12 +152,18 @@ export class KmpCompiler implements KeymanCompiler {
* @internal
*/
public transformKpsToKmpObject(kpsFilename: string): KmpJsonFile.KmpJsonFile {
- const kps = this.loadKpsFile(kpsFilename);
+ const reader = new KpsFileReader(this.callbacks);
+ const data = this.callbacks.loadFile(kpsFilename);
+ if(!data) {
+ this.callbacks.reportMessage(PackageCompilerMessages.Error_FileDoesNotExist({filename: kpsFilename}));
+ return null;
+ }
+ const kps = reader.read(data);
if(!kps) {
- // errors will already have been reported by loadKpsFile
+ // errors will already have been reported by KpsFileReader
return null;
}
- const kmp = this.transformKpsFileToKmpObject(kpsFilename, kps);
+ const kmp = this.transformKpsFileToKmpObject(kpsFilename, kps.Package);
if(!kmp) {
return null;
}
@@ -166,38 +177,6 @@ export class KmpCompiler implements KeymanCompiler {
return kmp;
}
- /**
- * @internal
- */
- public loadKpsFile(kpsFilename: string): KpsFile.KpsFile {
- // Load the KPS data from XML as JS structured data.
- const buffer = this.callbacks.loadFile(kpsFilename);
- if(!buffer) {
- this.callbacks.reportMessage(PackageCompilerMessages.Error_FileDoesNotExist({filename: kpsFilename}));
- return null;
- }
- const data = new TextDecoder().decode(buffer);
-
- const kpsPackage = (() => {
- let a: KpsFile.KpsPackage;
-
- try {
- a = new KeymanXMLReader('kps')
- .parse(data) as KpsFile.KpsPackage;
- } catch(e) {
- this.callbacks.reportMessage(PackageCompilerMessages.Error_InvalidPackageFile({e}));
- }
- return a;
- })();
-
- if(!kpsPackage) {
- return null;
- }
-
- const kps: KpsFile.KpsFile = kpsPackage.Package;
- return kps;
- }
-
readonly normalizePath = (path: string) => path || path === '' ? path.trim().replaceAll('\\','/') : undefined;
/**
@@ -252,10 +231,10 @@ export class KmpCompiler implements KeymanCompiler {
// Add related package metadata
//
- if(kps.RelatedPackages) {
+ if(kps.RelatedPackages?.RelatedPackage?.length) {
// Note: 'relationship' field is required for kmp.json but optional for .kps, only
// two values are supported -- deprecates or related.
- kmp.relatedPackages = (this.arrayWrap(kps.RelatedPackages.RelatedPackage) as KpsFile.KpsFileRelatedPackage[]).map(p =>
+ kmp.relatedPackages = kps.RelatedPackages.RelatedPackage.map(p =>
({id: p.$.ID, relationship: p.$.Relationship == 'deprecates' ? 'deprecates' : 'related'})
);
}
@@ -264,8 +243,8 @@ export class KmpCompiler implements KeymanCompiler {
// Add file metadata
//
- if(kps.Files && kps.Files.File) {
- kmp.files = this.arrayWrap(kps.Files.File).map((file: KpsFile.KpsFileContentFile) => {
+ if(kps.Files?.File?.length) {
+ kmp.files = kps.Files.File.map((file: KpsFile.KpsFileContentFile) => {
return {
name: this.normalizePath(file.Name),
description: (file.Description ?? '').trim(),
@@ -305,29 +284,29 @@ export class KmpCompiler implements KeymanCompiler {
// Add keyboard metadata
//
- if(kps.Keyboards && kps.Keyboards.Keyboard) {
- kmp.keyboards = this.arrayWrap(kps.Keyboards.Keyboard).map((keyboard: KpsFile.KpsFileKeyboard) => ({
+ if(kps.Keyboards?.Keyboard?.length) {
+ kmp.keyboards = kps.Keyboards.Keyboard.map((keyboard: KpsFile.KpsFileKeyboard) => ({
displayFont: keyboard.DisplayFont ? this.callbacks.path.basename(this.normalizePath(keyboard.DisplayFont)) : undefined,
oskFont: keyboard.OSKFont ? this.callbacks.path.basename(this.normalizePath(keyboard.OSKFont)) : undefined,
name:keyboard.Name?.trim(),
id:keyboard.ID?.trim(),
version:keyboard.Version?.trim(),
rtl:keyboard.RTL == 'True' ? true : undefined,
- languages: keyboard.Languages ?
- this.kpsLanguagesToKmpLanguages(this.arrayWrap(keyboard.Languages.Language) as KpsFile.KpsFileLanguage[]) :
+ languages: keyboard.Languages?.Language?.length ?
+ this.kpsLanguagesToKmpLanguages(keyboard.Languages.Language) :
[],
- examples: keyboard.Examples ?
- (this.arrayWrap(keyboard.Examples.Example) as KpsFile.KpsFileLanguageExample[]).map(
+ examples: keyboard.Examples?.Example?.length ?
+ keyboard.Examples.Example.map(
e => ({id: e.$.ID, keys: e.$.Keys, text: e.$.Text, note: e.$.Note})
) as KmpJsonFile.KmpJsonFileExample[] :
undefined,
- webDisplayFonts: keyboard.WebDisplayFonts ?
- (this.arrayWrap(keyboard.WebDisplayFonts.Font) as KpsFile.KpsFileFont[]).map(
+ webDisplayFonts: keyboard.WebDisplayFonts?.Font?.length ?
+ keyboard.WebDisplayFonts.Font.map(
e => (this.callbacks.path.basename(this.normalizePath(e.$.Filename)))
) :
undefined,
- webOskFonts: keyboard.WebOSKFonts ?
- (this.arrayWrap(keyboard.WebOSKFonts.Font) as KpsFile.KpsFileFont[]).map(
+ webOskFonts: keyboard.WebOSKFonts?.Font?.length ?
+ keyboard.WebOSKFonts.Font.map(
e => (this.callbacks.path.basename(this.normalizePath(e.$.Filename)))
) :
undefined,
@@ -338,12 +317,12 @@ export class KmpCompiler implements KeymanCompiler {
// Add lexical-model metadata
//
- if(kps.LexicalModels && kps.LexicalModels.LexicalModel) {
- kmp.lexicalModels = this.arrayWrap(kps.LexicalModels.LexicalModel).map((model: KpsFile.KpsFileLexicalModel) => ({
+ if(kps.LexicalModels?.LexicalModel?.length) {
+ kmp.lexicalModels = kps.LexicalModels.LexicalModel.map((model: KpsFile.KpsFileLexicalModel) => ({
name:model.Name.trim(),
id:model.ID.trim(),
- languages: model.Languages ?
- this.kpsLanguagesToKmpLanguages(this.arrayWrap(model.Languages.Language) as KpsFile.KpsFileLanguage[]) : []
+ languages: model.Languages?.Language?.length ?
+ this.kpsLanguagesToKmpLanguages(model.Languages.Language) : []
}));
}
@@ -396,8 +375,8 @@ export class KmpCompiler implements KeymanCompiler {
kmp.startMenu = {};
if(kps.StartMenu.AddUninstallEntry === '') kmp.startMenu.addUninstallEntry = true;
if(kps.StartMenu.Folder) kmp.startMenu.folder = kps.StartMenu.Folder;
- if(kps.StartMenu.Items && kps.StartMenu.Items.Item) {
- kmp.startMenu.items = this.arrayWrap(kps.StartMenu.Items.Item).map((item: KpsFile.KpsFileStartMenuItem) => ({
+ if(kps.StartMenu?.Items?.Item?.length) {
+ kmp.startMenu.items = kps.StartMenu.Items.Item.map((item: KpsFile.KpsFileStartMenuItem) => ({
filename: item.FileName,
name: item.Name,
arguments: item.Arguments,
@@ -452,13 +431,6 @@ export class KmpCompiler implements KeymanCompiler {
return kmpInfo;
};
- private arrayWrap(a: unknown) {
- if (Array.isArray(a)) {
- return a;
- }
- return [a];
- };
-
private kpsLanguagesToKmpLanguages(language: KpsFile.KpsFileLanguage[]): KmpJsonFile.KmpJsonFileLanguage[] {
if(language.length == 0 || language[0] == undefined) {
return [];
diff --git a/developer/src/kmc-package/src/compiler/package-compiler-messages.ts b/developer/src/kmc-package/src/compiler/package-compiler-messages.ts
index 00d8a380b5e..f3df62a8973 100644
--- a/developer/src/kmc-package/src/compiler/package-compiler-messages.ts
+++ b/developer/src/kmc-package/src/compiler/package-compiler-messages.ts
@@ -1,3 +1,7 @@
+/*
+ * Keyman is copyright (C) SIL Global. MIT License.
+ */
+
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def, CompilerMessageSpecWithException } from "@keymanapp/developer-utils";
const Namespace = CompilerErrorNamespace.PackageCompiler;
@@ -127,9 +131,7 @@ export class PackageCompilerMessages {
static Hint_PackageContainsSourceFile = (o:{filename:string}) => m(this.HINT_PackageContainsSourceFile,
`The source file ${def(o.filename)} should not be included in the package; instead include the compiled result.`);
- static ERROR_InvalidPackageFile = SevError | 0x001E;
- static Error_InvalidPackageFile = (o:{e:any}) => m(this.ERROR_InvalidPackageFile,
- `Package source file is invalid: ${(o.e ?? 'unknown error').toString()}`);
+ // 0x001E was ERROR_InvalidPackageFile, now CommonTypesMessages.Error_InvalidPackageFile
static ERROR_FileRecordIsMissingName = SevError | 0x001F;
static Error_FileRecordIsMissingName = (o:{description:string}) => m(this.ERROR_FileRecordIsMissingName,
diff --git a/developer/src/kmc-package/src/compiler/windows-package-installer-compiler.ts b/developer/src/kmc-package/src/compiler/windows-package-installer-compiler.ts
index 3eefada8988..f964e71c876 100644
--- a/developer/src/kmc-package/src/compiler/windows-package-installer-compiler.ts
+++ b/developer/src/kmc-package/src/compiler/windows-package-installer-compiler.ts
@@ -1,3 +1,7 @@
+/*
+ * Keyman is copyright (C) SIL Global. MIT License.
+ */
+
/**
* Create a .exe installer that bundles one or more .kmp files, together with
* setup.exe, keymandesktop.msi, and generates and includes a setup.inf also.
@@ -12,7 +16,7 @@
import JSZip from 'jszip';
import { KeymanFileTypes, KmpJsonFile } from "@keymanapp/common-types";
-import { CompilerCallbacks, KeymanCompiler, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult, KpsFile } from '@keymanapp/developer-utils';
+import { CompilerCallbacks, KeymanCompiler, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult, KpsFile, KpsFileReader } from '@keymanapp/developer-utils';
import KEYMAN_VERSION from "@keymanapp/keyman-version";
import { KmpCompiler, KmpCompilerOptions } from "./kmp-compiler.js";
import { PackageCompilerMessages } from "./package-compiler-messages.js";
@@ -112,9 +116,16 @@ export class WindowsPackageInstallerCompiler implements KeymanCompiler {
*/
public async run(inputFilename: string, outputFilename?: string): Promise {
const sources = this.options.sources;
- const kps = this.kmpCompiler.loadKpsFile(inputFilename);
+ const reader = new KpsFileReader(this.callbacks);
+ const data = this.callbacks.loadFile(inputFilename);
+ if(!data) {
+ this.callbacks.reportMessage(PackageCompilerMessages.Error_FileDoesNotExist({filename: inputFilename}));
+ return null;
+ }
+
+ const kps = reader.read(data);
if(!kps) {
- // errors will already have been reported by loadKpsFile
+ // errors will already have been reported by KpsFileReader
return null;
}
@@ -138,7 +149,7 @@ export class WindowsPackageInstallerCompiler implements KeymanCompiler {
// Nor do we use the MSIOptions field.
// Build the zip
- const zipBuffer = await this.buildZip(kps, inputFilename, sources);
+ const zipBuffer = await this.buildZip(kps.Package, inputFilename, sources);
if(!zipBuffer) {
// Error messages already reported by buildZip
return null;
diff --git a/developer/src/kmc-package/test/test-messages.ts b/developer/src/kmc-package/test/test-messages.ts
index 2d8df44eb50..ba5ec56b08b 100644
--- a/developer/src/kmc-package/test/test-messages.ts
+++ b/developer/src/kmc-package/test/test-messages.ts
@@ -1,3 +1,7 @@
+/*
+ * Keyman is copyright (C) SIL Global. MIT License.
+ */
+
import 'mocha';
import { assert } from 'chai';
import { TestCompilerCallbacks, verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers';
@@ -6,10 +10,16 @@ import { makePathToFixture } from './helpers/index.js';
import { KmpCompiler } from '../src/compiler/kmp-compiler.js';
import { CompilerErrorNamespace, CompilerOptions } from '@keymanapp/developer-utils';
-const debug = false;
const callbacks = new TestCompilerCallbacks();
describe('PackageCompilerMessages', function () {
+
+ this.afterEach(function() {
+ if(this.currentTest.isFailed()) {
+ callbacks.printMessages();
+ }
+ });
+
it('should have a valid PackageCompilerMessages object', function() {
return verifyCompilerMessagesObject(PackageCompilerMessages, CompilerErrorNamespace.PackageCompiler);
});
@@ -30,8 +40,6 @@ describe('PackageCompilerMessages', function () {
await kmpCompiler.run(kpsPath);
- if(debug) callbacks.printMessages();
-
if(messageId) {
assert.lengthOf(callbacks.messages, 1);
assert.isTrue(callbacks.hasMessage(messageId));
@@ -217,13 +225,6 @@ describe('PackageCompilerMessages', function () {
PackageCompilerMessages.HINT_PackageContainsSourceFile);
});
- // ERROR_InvalidPackageFile
-
- it('should generate ERROR_InvalidPackageFile if package source file contains invalid XML', async function() {
- await testForMessage(this, ['invalid', 'error_invalid_package_file.kps'],
- PackageCompilerMessages.ERROR_InvalidPackageFile);
- });
-
// ERROR_InvalidAuthorEmail
it('should generate ERROR_InvalidAuthorEmail if author email address has multiple addresses', async function() {