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() {