@keymanapp/kmc-package 17.0.300-beta → 17.0.302-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/build/src/compiler/cp1252.js +289 -288
  2. package/build/src/compiler/cp1252.js.map +1 -1
  3. package/build/src/compiler/kmp-compiler.js +494 -493
  4. package/build/src/compiler/kmp-compiler.js.map +1 -1
  5. package/build/src/compiler/kmp-inf-writer.js +145 -144
  6. package/build/src/compiler/kmp-inf-writer.js.map +1 -1
  7. package/build/src/compiler/kmx-keyboard-metadata.js +19 -18
  8. package/build/src/compiler/kmx-keyboard-metadata.js.map +1 -1
  9. package/build/src/compiler/markdown.js +49 -48
  10. package/build/src/compiler/markdown.js.map +1 -1
  11. package/build/src/compiler/package-compiler-messages.js +79 -78
  12. package/build/src/compiler/package-compiler-messages.js.map +1 -1
  13. package/build/src/compiler/package-keyboard-target-validator.js +45 -44
  14. package/build/src/compiler/package-keyboard-target-validator.js.map +1 -1
  15. package/build/src/compiler/package-metadata-collector.js +78 -77
  16. package/build/src/compiler/package-metadata-collector.js.map +1 -1
  17. package/build/src/compiler/package-metadata-updater.js +13 -12
  18. package/build/src/compiler/package-metadata-updater.js.map +1 -1
  19. package/build/src/compiler/package-validation.js +180 -179
  20. package/build/src/compiler/package-validation.js.map +1 -1
  21. package/build/src/compiler/package-version-validator.js +92 -91
  22. package/build/src/compiler/package-version-validator.js.map +1 -1
  23. package/build/src/compiler/redist-files.js +59 -58
  24. package/build/src/compiler/redist-files.js.map +1 -1
  25. package/build/src/compiler/web-keyboard-metadata.js +37 -36
  26. package/build/src/compiler/web-keyboard-metadata.js.map +1 -1
  27. package/build/src/compiler/windows-package-installer-compiler.js +180 -179
  28. package/build/src/compiler/windows-package-installer-compiler.js.map +1 -1
  29. package/build/src/main.js +7 -6
  30. package/build/src/main.js.map +1 -1
  31. package/package.json +3 -3
@@ -1,494 +1,495 @@
1
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},n=(new Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="513ed790-22b6-567a-bb06-4c9537e9e752")}catch(e){}}();
2
- import * as xml2js from 'xml2js';
3
- import JSZip from 'jszip';
4
- import KEYMAN_VERSION from "@keymanapp/keyman-version";
5
- import { SchemaValidators, KeymanFileTypes, KvkFile } from '@keymanapp/common-types';
6
- import { CompilerMessages } from './package-compiler-messages.js';
7
- import { PackageMetadataCollector } from './package-metadata-collector.js';
8
- import { KmpInfWriter } from './kmp-inf-writer.js';
9
- import { transcodeToCP1252 } from './cp1252.js';
10
- import { MIN_LM_FILEVERSION_KMP_JSON, PackageVersionValidator } from './package-version-validator.js';
11
- import { PackageKeyboardTargetValidator } from './package-keyboard-target-validator.js';
12
- import { PackageMetadataUpdater } from './package-metadata-updater.js';
13
- import { markdownToHTML } from './markdown.js';
14
- import { PackageValidation } from './package-validation.js';
15
- const KMP_JSON_FILENAME = 'kmp.json';
16
- const KMP_INF_FILENAME = 'kmp.inf';
17
- // welcome.htm: this is a legacy filename, as of 17.0 the welcome
18
- // (documentation) filename can be any file, but we will fallback to detecting
19
- // this filename for existing keyboard packages.
20
- const WELCOME_HTM_FILENAME = 'welcome.htm';
21
- ;
22
- ;
23
- ;
24
- /**
25
- * @public
26
- * Compiles a .kps file to a .kmp archive. The compiler does not read or write
27
- * from filesystem or network directly, but relies on callbacks for all external
28
- * IO.
29
- */
30
- export class KmpCompiler {
31
- callbacks;
32
- options;
33
- /**
34
- * Initialize the compiler.
35
- * Copies options.
36
- * @param callbacks - Callbacks for external interfaces, including message
37
- * reporting and file io
38
- * @param options - Compiler options
39
- * @returns false if initialization fails
40
- */
41
- async init(callbacks, options) {
42
- this.callbacks = callbacks;
43
- this.options = options ? { ...options } : {};
44
- return true;
45
- }
46
- /**
47
- * Compiles a .kps file to .kmp file. Returns an object containing binary
48
- * artifacts on success. The files are passed in by name, and the compiler
49
- * will use callbacks as passed to the {@link KmpCompiler.init} function
50
- * to read any input files by disk.
51
- * @param infile - Path to source file. Path will be parsed to find relative
52
- * references in the .kmn file, such as icon or On Screen
53
- * Keyboard file
54
- * @param outfile - Path to output file. The file will not be written to, but
55
- * will be included in the result for use by
56
- * {@link KmpCompiler.write}.
57
- * @returns Binary artifacts on success, null on failure.
58
- */
59
- async run(inputFilename, outputFilename) {
60
- const kmpJsonData = this.transformKpsToKmpObject(inputFilename);
61
- if (!kmpJsonData) {
62
- return null;
63
- }
64
- //
65
- // Validate the package file
66
- //
67
- const validation = new PackageValidation(this.callbacks, this.options);
68
- if (!validation.validate(inputFilename, kmpJsonData)) {
69
- return null;
70
- }
71
- //
72
- // Build the .kmp package file
73
- //
74
- const data = await this.buildKmpFile(inputFilename, kmpJsonData);
75
- if (!data) {
76
- return null;
77
- }
78
- const result = {
79
- artifacts: {
80
- kmp: {
81
- data,
82
- filename: outputFilename ?? inputFilename.replace(/\.kps$/, '.kmp')
83
- }
84
- }
85
- };
86
- return result;
87
- }
88
- /**
89
- * Write artifacts from a successful compile to disk, via callbacks methods.
90
- * The artifacts written may include:
91
- *
92
- * - .kmp file - binary keyboard package used by Keyman on desktop and touch
93
- * platforms
94
- *
95
- * @param artifacts - object containing artifact binary data to write out
96
- * @returns true on success
97
- */
98
- async write(artifacts) {
99
- this.callbacks.fs.writeFileSync(artifacts.kmp.filename, artifacts.kmp.data);
100
- return true;
101
- }
102
- /**
103
- * @internal
104
- */
105
- transformKpsToKmpObject(kpsFilename) {
106
- const kps = this.loadKpsFile(kpsFilename);
107
- if (!kps) {
108
- // errors will already have been reported by loadKpsFile
109
- return null;
110
- }
111
- const kmp = this.transformKpsFileToKmpObject(kpsFilename, kps);
112
- if (!kmp) {
113
- return null;
114
- }
115
- // Verify that the generated kmp.json validates with the kmp.json schema
116
- if (!SchemaValidators.default.kmp(kmp)) {
117
- // This is an internal error, so throwing an exception is appropriate
118
- throw new Error(JSON.stringify(SchemaValidators.default.kmp.errors));
119
- }
120
- return kmp;
121
- }
122
- /**
123
- * @internal
124
- */
125
- loadKpsFile(kpsFilename) {
126
- // Load the KPS data from XML as JS structured data.
127
- const buffer = this.callbacks.loadFile(kpsFilename);
128
- if (!buffer) {
129
- this.callbacks.reportMessage(CompilerMessages.Error_FileDoesNotExist({ filename: kpsFilename }));
130
- return null;
131
- }
132
- const data = new TextDecoder().decode(buffer);
133
- const kpsPackage = (() => {
134
- let a;
135
- let parser = new xml2js.Parser({
136
- explicitArray: false
137
- });
138
- try {
139
- parser.parseString(data, (e, r) => { if (e)
140
- throw e; a = r; });
141
- }
142
- catch (e) {
143
- this.callbacks.reportMessage(CompilerMessages.Error_InvalidPackageFile({ e }));
144
- }
145
- return a;
146
- })();
147
- if (!kpsPackage) {
148
- return null;
149
- }
150
- const kps = kpsPackage.Package;
151
- return kps;
152
- }
153
- normalizePath = (path) => path || path === '' ? path.trim().replaceAll('\\', '/') : undefined;
154
- /**
155
- * @internal
156
- */
157
- transformKpsFileToKmpObject(kpsFilename, kps) {
158
- //
159
- // To convert to kmp.json, we need to:
160
- //
161
- // 1. Unwrap arrays (and convert to array where single object)
162
- // 2. Fix casing on `iD`
163
- // 3. Rewrap info, keyboard.languages, lexicalModel.languages, startMenu.items elements
164
- // 4. Remove options.followKeyboardVersion, file.fileType
165
- // 5. Convert file.copyLocation to a Number
166
- // 6. Filenames need to be basenames (but this comes after processing)
167
- //
168
- // Start to construct the kmp.json file from the .kps file
169
- let kmp = {
170
- system: {
171
- fileVersion: null,
172
- keymanDeveloperVersion: KEYMAN_VERSION.VERSION
173
- },
174
- options: {}
175
- };
176
- //
177
- // Fill in additional fields
178
- //
179
- if (kps.Options) {
180
- kmp.options.executeProgram = this.normalizePath(kps.Options.ExecuteProgram || undefined);
181
- kmp.options.graphicFile = this.normalizePath(kps.Options.GraphicFile || undefined);
182
- kmp.options.msiFilename = this.normalizePath(kps.Options.MSIFileName || undefined);
183
- kmp.options.msiOptions = kps.Options.MSIOptions || undefined;
184
- kmp.options.readmeFile = this.normalizePath(kps.Options.ReadMeFile || undefined);
185
- kmp.options.licenseFile = this.normalizePath(kps.Options.LicenseFile || undefined);
186
- kmp.options.welcomeFile = this.normalizePath(kps.Options.WelcomeFile || undefined);
187
- }
188
- //
189
- // Add basic metadata
190
- //
191
- if (kps.Info) {
192
- kmp.info = this.kpsInfoToKmpInfo(kps.Info);
193
- }
194
- //
195
- // Add related package metadata
196
- //
197
- if (kps.RelatedPackages) {
198
- // Note: 'relationship' field is required for kmp.json but optional for .kps, only
199
- // two values are supported -- deprecates or related.
200
- kmp.relatedPackages = this.arrayWrap(kps.RelatedPackages.RelatedPackage).map(p => ({ id: p.$.ID, relationship: p.$.Relationship == 'deprecates' ? 'deprecates' : 'related' }));
201
- }
202
- //
203
- // Add file metadata
204
- //
205
- if (kps.Files && kps.Files.File) {
206
- kmp.files = this.arrayWrap(kps.Files.File).map((file) => {
207
- return {
208
- name: this.normalizePath(file.Name),
209
- description: file.Description.trim(),
210
- copyLocation: parseInt(file.CopyLocation, 10) || undefined
211
- // note: we don't emit fileType as that is not permitted in kmp.json
212
- };
213
- });
214
- }
215
- kmp.files = kmp.files ?? [];
216
- // Keyboard packages also include a legacy kmp.inf file (this will be removed,
217
- // one day)
218
- if (kps.Keyboards && kps.Keyboards.Keyboard) {
219
- kmp.files.push({
220
- name: KMP_INF_FILENAME,
221
- description: "Package information"
222
- });
223
- }
224
- // Add the standard kmp.json self-referential to match existing implementations
225
- kmp.files.push({
226
- name: KMP_JSON_FILENAME,
227
- description: "Package information (JSON)"
228
- });
229
- //
230
- // Add keyboard metadata
231
- //
232
- if (kps.Keyboards && kps.Keyboards.Keyboard) {
233
- kmp.keyboards = this.arrayWrap(kps.Keyboards.Keyboard).map((keyboard) => ({
234
- displayFont: keyboard.DisplayFont ? this.callbacks.path.basename(this.normalizePath(keyboard.DisplayFont)) : undefined,
235
- oskFont: keyboard.OSKFont ? this.callbacks.path.basename(this.normalizePath(keyboard.OSKFont)) : undefined,
236
- name: keyboard.Name.trim(),
237
- id: keyboard.ID.trim(),
238
- version: keyboard.Version.trim(),
239
- rtl: keyboard.RTL == 'True' ? true : undefined,
240
- languages: keyboard.Languages ?
241
- this.kpsLanguagesToKmpLanguages(this.arrayWrap(keyboard.Languages.Language)) :
242
- [],
243
- examples: keyboard.Examples ?
244
- this.arrayWrap(keyboard.Examples.Example).map(e => ({ id: e.$.ID, keys: e.$.Keys, text: e.$.Text, note: e.$.Note })) :
245
- undefined,
246
- webDisplayFonts: keyboard.WebDisplayFonts ?
247
- this.arrayWrap(keyboard.WebDisplayFonts.Font).map(e => (this.callbacks.path.basename(this.normalizePath(e.$.Filename)))) :
248
- undefined,
249
- webOskFonts: keyboard.WebOSKFonts ?
250
- this.arrayWrap(keyboard.WebOSKFonts.Font).map(e => (this.callbacks.path.basename(this.normalizePath(e.$.Filename)))) :
251
- undefined,
252
- }));
253
- }
254
- //
255
- // Add lexical-model metadata
256
- //
257
- if (kps.LexicalModels && kps.LexicalModels.LexicalModel) {
258
- kmp.lexicalModels = this.arrayWrap(kps.LexicalModels.LexicalModel).map((model) => ({
259
- name: model.Name.trim(),
260
- id: model.ID.trim(),
261
- languages: model.Languages ?
262
- this.kpsLanguagesToKmpLanguages(this.arrayWrap(model.Languages.Language)) : []
263
- }));
264
- }
265
- //
266
- // Collect metadata from keyboards (and later models) in order to update
267
- // the kmp.json metadata for use downstream in apps. This will also be
268
- // used later to fill in .keyboard_info file data.
269
- //
270
- const collector = new PackageMetadataCollector(this.callbacks);
271
- const metadata = collector.collectKeyboardMetadata(kpsFilename, kmp);
272
- if (metadata == null) {
273
- return null;
274
- }
275
- //
276
- // Verify keyboard versions and update version metadata where appropriate
277
- //
278
- const versionValidator = new PackageVersionValidator(this.callbacks);
279
- if (!versionValidator.validateAndUpdateVersions(kps, kmp, metadata)) {
280
- return null;
281
- }
282
- if (kps.Keyboards && kps.Keyboards.Keyboard) {
283
- kmp.system.fileVersion = versionValidator.getMinKeymanVersion(metadata);
284
- }
285
- else {
286
- kmp.system.fileVersion = MIN_LM_FILEVERSION_KMP_JSON;
287
- }
288
- //
289
- // Verify that packages that target mobile devices include a .js file
290
- //
291
- const targetValidator = new PackageKeyboardTargetValidator(this.callbacks);
292
- targetValidator.verifyAllTargets(kmp, metadata);
293
- //
294
- // Update assorted keyboard metadata from the keyboards in the package
295
- //
296
- const updater = new PackageMetadataUpdater();
297
- updater.updatePackage(metadata);
298
- //
299
- // Add Windows Start Menu metadata
300
- //
301
- if (kps.StartMenu && (kps.StartMenu.Folder || kps.StartMenu.Items)) {
302
- kmp.startMenu = {};
303
- if (kps.StartMenu.AddUninstallEntry === '')
304
- kmp.startMenu.addUninstallEntry = true;
305
- if (kps.StartMenu.Folder)
306
- kmp.startMenu.folder = kps.StartMenu.Folder;
307
- if (kps.StartMenu.Items && kps.StartMenu.Items.Item) {
308
- kmp.startMenu.items = this.arrayWrap(kps.StartMenu.Items.Item).map((item) => ({
309
- filename: item.FileName,
310
- name: item.Name,
311
- arguments: item.Arguments,
312
- icon: item.Icon,
313
- location: item.Location
314
- }));
315
- // Remove default values
316
- for (let item of kmp.startMenu.items) {
317
- if (item.icon == '')
318
- delete item.icon;
319
- if (item.location == 'psmelStartMenu')
320
- delete item.location;
321
- if (item.arguments == '')
322
- delete item.arguments;
323
- }
324
- }
325
- else {
326
- kmp.startMenu.items = [];
327
- }
328
- }
329
- kmp = this.stripUndefined(kmp);
330
- return kmp;
331
- }
332
- // Helper functions
333
- kpsInfoToKmpInfo(kpsInfo) {
334
- let kmpInfo = {};
335
- const keys = [
336
- ['Author', 'author', false],
337
- ['Copyright', 'copyright', false],
338
- ['Name', 'name', false],
339
- ['Version', 'version', false],
340
- ['WebSite', 'website', false],
341
- ['Description', 'description', true],
342
- ];
343
- for (let [src, dst, isMarkdown] of keys) {
344
- if (kpsInfo[src]) {
345
- kmpInfo[dst] = {
346
- description: (kpsInfo[src]._ ?? (typeof kpsInfo[src] == 'string' ? kpsInfo[src].toString() : '')).trim()
347
- };
348
- if (isMarkdown) {
349
- kmpInfo[dst].description = markdownToHTML(kmpInfo[dst].description, false).trim();
350
- }
351
- if (kpsInfo[src].$?.URL) {
352
- kmpInfo[dst].url = kpsInfo[src].$.URL.trim();
353
- }
354
- }
355
- }
356
- return kmpInfo;
357
- }
358
- ;
359
- arrayWrap(a) {
360
- if (Array.isArray(a)) {
361
- return a;
362
- }
363
- return [a];
364
- }
365
- ;
366
- kpsLanguagesToKmpLanguages(language) {
367
- if (language.length == 0 || language[0] == undefined) {
368
- return [];
369
- }
370
- return language.map((element) => { return { name: element._, id: element.$.ID }; });
371
- }
372
- ;
373
- stripUndefined(o) {
374
- for (const key in o) {
375
- if (o[key] === undefined) {
376
- delete o[key];
377
- }
378
- else if (typeof o[key] == 'object') {
379
- o[key] = this.stripUndefined(o[key]);
380
- }
381
- }
382
- return o;
383
- }
384
- /**
385
- * @internal
386
- * Returns a Promise to the serialized data which can then be written to a .kmp file.
387
- *
388
- * @param kpsFilename - Filename of the kps, not read, used only for calculating relative paths
389
- * @param kmpJsonData - The kmp.json Object
390
- */
391
- buildKmpFile(kpsFilename, kmpJsonData) {
392
- const zip = JSZip();
393
- // Make a copy of kmpJsonData, as we mutate paths for writing
394
- const data = JSON.parse(JSON.stringify(kmpJsonData));
395
- if (!data.files) {
396
- data.files = [];
397
- }
398
- const hasKmpInf = !!data.files.find(file => file.name == KMP_INF_FILENAME);
399
- let failed = false;
400
- data.files.forEach((value) => {
401
- // Get the path of the file
402
- let filename = value.name;
403
- // We add this separately after zipping all other files
404
- if (filename == KMP_JSON_FILENAME || filename == KMP_INF_FILENAME) {
405
- return;
406
- }
407
- if (this.callbacks.path.isAbsolute(filename)) {
408
- // absolute paths are not portable to other computers
409
- this.callbacks.reportMessage(CompilerMessages.Warn_AbsolutePath({ filename: filename }));
410
- }
411
- filename = this.callbacks.resolveFilename(kpsFilename, filename);
412
- const basename = this.callbacks.path.basename(filename);
413
- if (!this.callbacks.fs.existsSync(filename)) {
414
- this.callbacks.reportMessage(CompilerMessages.Error_FileDoesNotExist({ filename: filename }));
415
- failed = true;
416
- return;
417
- }
418
- let memberFileData;
419
- try {
420
- memberFileData = this.callbacks.loadFile(filename);
421
- }
422
- catch (e) {
423
- this.callbacks.reportMessage(CompilerMessages.Error_FileCouldNotBeRead({ filename: filename, e: e }));
424
- failed = true;
425
- return;
426
- }
427
- this.warnIfKvkFileIsNotBinary(filename, memberFileData);
428
- zip.file(basename, memberFileData);
429
- // Remove path data from files before JSON save
430
- value.name = basename;
431
- });
432
- if (failed) {
433
- return null;
434
- }
435
- // TODO #9477: transform .md to .htm
436
- // Remove path data from file references in options
437
- if (data.options.graphicFile) {
438
- data.options.graphicFile = this.callbacks.path.basename(data.options.graphicFile);
439
- }
440
- if (data.options.readmeFile) {
441
- data.options.readmeFile = this.callbacks.path.basename(data.options.readmeFile);
442
- }
443
- if (data.options.licenseFile) {
444
- data.options.licenseFile = this.callbacks.path.basename(data.options.licenseFile);
445
- }
446
- if (data.options.welcomeFile) {
447
- data.options.welcomeFile = this.callbacks.path.basename(data.options.welcomeFile);
448
- }
449
- else if (data.files.find(file => file.name == WELCOME_HTM_FILENAME)) {
450
- // We will, for improved backward-compatibility with existing packages, add a
451
- // reference to the file welcome.htm is it is present in the package. This allows
452
- // newer tools to avoid knowing about welcome.htm, if we assume that they work with
453
- // packages compiled with kmc-package (17.0+) and not kmcomp (5.x-16.x).
454
- data.options.welcomeFile = WELCOME_HTM_FILENAME;
455
- }
456
- if (data.options.msiFilename) {
457
- data.options.msiFilename = this.callbacks.path.basename(data.options.msiFilename);
458
- }
459
- // Write kmp.json and kmp.inf
460
- zip.file(KMP_JSON_FILENAME, JSON.stringify(data, null, 2));
461
- if (hasKmpInf) {
462
- zip.file(KMP_INF_FILENAME, this.buildKmpInf(data));
463
- }
464
- // Generate kmp file
465
- return zip.generateAsync({ type: 'uint8array', compression: 'DEFLATE' });
466
- }
467
- buildKmpInf(data) {
468
- const writer = new KmpInfWriter(data);
469
- const s = writer.write();
470
- return transcodeToCP1252(s);
471
- }
472
- /**
473
- * Legacy .kmp compiler would transform xml-format .kvk files into a binary .kvk file; now
474
- * we want that to remain the responsibility of the keyboard compiler, so we'll warn the
475
- * few users who are still doing this
476
- */
477
- warnIfKvkFileIsNotBinary(filename, data) {
478
- if (!KeymanFileTypes.filenameIs(filename, ".kvk" /* KeymanFileTypes.Binary.VisualKeyboard */)) {
479
- return;
480
- }
481
- if (data.byteLength < 4) {
482
- // TODO: Not a valid .kvk file; should we be reporting this?
483
- return;
484
- }
485
- if (data[0] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[0] ||
486
- data[1] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[1] ||
487
- data[2] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[2] ||
488
- data[3] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[3]) {
489
- this.callbacks.reportMessage(CompilerMessages.Warn_FileIsNotABinaryKvkFile({ filename: filename }));
490
- }
491
- }
492
- }
493
- //# debugId=513ed790-22b6-567a-bb06-4c9537e9e752
1
+
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},n=(new Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="b6fb6e95-5ebd-510f-bb92-192fc2ad2371")}catch(e){}}();
3
+ import * as xml2js from 'xml2js';
4
+ import JSZip from 'jszip';
5
+ import KEYMAN_VERSION from "@keymanapp/keyman-version";
6
+ import { SchemaValidators, KeymanFileTypes, KvkFile } from '@keymanapp/common-types';
7
+ import { CompilerMessages } from './package-compiler-messages.js';
8
+ import { PackageMetadataCollector } from './package-metadata-collector.js';
9
+ import { KmpInfWriter } from './kmp-inf-writer.js';
10
+ import { transcodeToCP1252 } from './cp1252.js';
11
+ import { MIN_LM_FILEVERSION_KMP_JSON, PackageVersionValidator } from './package-version-validator.js';
12
+ import { PackageKeyboardTargetValidator } from './package-keyboard-target-validator.js';
13
+ import { PackageMetadataUpdater } from './package-metadata-updater.js';
14
+ import { markdownToHTML } from './markdown.js';
15
+ import { PackageValidation } from './package-validation.js';
16
+ const KMP_JSON_FILENAME = 'kmp.json';
17
+ const KMP_INF_FILENAME = 'kmp.inf';
18
+ // welcome.htm: this is a legacy filename, as of 17.0 the welcome
19
+ // (documentation) filename can be any file, but we will fallback to detecting
20
+ // this filename for existing keyboard packages.
21
+ const WELCOME_HTM_FILENAME = 'welcome.htm';
22
+ ;
23
+ ;
24
+ ;
25
+ /**
26
+ * @public
27
+ * Compiles a .kps file to a .kmp archive. The compiler does not read or write
28
+ * from filesystem or network directly, but relies on callbacks for all external
29
+ * IO.
30
+ */
31
+ export class KmpCompiler {
32
+ callbacks;
33
+ options;
34
+ /**
35
+ * Initialize the compiler.
36
+ * Copies options.
37
+ * @param callbacks - Callbacks for external interfaces, including message
38
+ * reporting and file io
39
+ * @param options - Compiler options
40
+ * @returns false if initialization fails
41
+ */
42
+ async init(callbacks, options) {
43
+ this.callbacks = callbacks;
44
+ this.options = options ? { ...options } : {};
45
+ return true;
46
+ }
47
+ /**
48
+ * Compiles a .kps file to .kmp file. Returns an object containing binary
49
+ * artifacts on success. The files are passed in by name, and the compiler
50
+ * will use callbacks as passed to the {@link KmpCompiler.init} function
51
+ * to read any input files by disk.
52
+ * @param infile - Path to source file. Path will be parsed to find relative
53
+ * references in the .kmn file, such as icon or On Screen
54
+ * Keyboard file
55
+ * @param outfile - Path to output file. The file will not be written to, but
56
+ * will be included in the result for use by
57
+ * {@link KmpCompiler.write}.
58
+ * @returns Binary artifacts on success, null on failure.
59
+ */
60
+ async run(inputFilename, outputFilename) {
61
+ const kmpJsonData = this.transformKpsToKmpObject(inputFilename);
62
+ if (!kmpJsonData) {
63
+ return null;
64
+ }
65
+ //
66
+ // Validate the package file
67
+ //
68
+ const validation = new PackageValidation(this.callbacks, this.options);
69
+ if (!validation.validate(inputFilename, kmpJsonData)) {
70
+ return null;
71
+ }
72
+ //
73
+ // Build the .kmp package file
74
+ //
75
+ const data = await this.buildKmpFile(inputFilename, kmpJsonData);
76
+ if (!data) {
77
+ return null;
78
+ }
79
+ const result = {
80
+ artifacts: {
81
+ kmp: {
82
+ data,
83
+ filename: outputFilename ?? inputFilename.replace(/\.kps$/, '.kmp')
84
+ }
85
+ }
86
+ };
87
+ return result;
88
+ }
89
+ /**
90
+ * Write artifacts from a successful compile to disk, via callbacks methods.
91
+ * The artifacts written may include:
92
+ *
93
+ * - .kmp file - binary keyboard package used by Keyman on desktop and touch
94
+ * platforms
95
+ *
96
+ * @param artifacts - object containing artifact binary data to write out
97
+ * @returns true on success
98
+ */
99
+ async write(artifacts) {
100
+ this.callbacks.fs.writeFileSync(artifacts.kmp.filename, artifacts.kmp.data);
101
+ return true;
102
+ }
103
+ /**
104
+ * @internal
105
+ */
106
+ transformKpsToKmpObject(kpsFilename) {
107
+ const kps = this.loadKpsFile(kpsFilename);
108
+ if (!kps) {
109
+ // errors will already have been reported by loadKpsFile
110
+ return null;
111
+ }
112
+ const kmp = this.transformKpsFileToKmpObject(kpsFilename, kps);
113
+ if (!kmp) {
114
+ return null;
115
+ }
116
+ // Verify that the generated kmp.json validates with the kmp.json schema
117
+ if (!SchemaValidators.default.kmp(kmp)) {
118
+ // This is an internal error, so throwing an exception is appropriate
119
+ throw new Error(JSON.stringify(SchemaValidators.default.kmp.errors));
120
+ }
121
+ return kmp;
122
+ }
123
+ /**
124
+ * @internal
125
+ */
126
+ loadKpsFile(kpsFilename) {
127
+ // Load the KPS data from XML as JS structured data.
128
+ const buffer = this.callbacks.loadFile(kpsFilename);
129
+ if (!buffer) {
130
+ this.callbacks.reportMessage(CompilerMessages.Error_FileDoesNotExist({ filename: kpsFilename }));
131
+ return null;
132
+ }
133
+ const data = new TextDecoder().decode(buffer);
134
+ const kpsPackage = (() => {
135
+ let a;
136
+ let parser = new xml2js.Parser({
137
+ explicitArray: false
138
+ });
139
+ try {
140
+ parser.parseString(data, (e, r) => { if (e)
141
+ throw e; a = r; });
142
+ }
143
+ catch (e) {
144
+ this.callbacks.reportMessage(CompilerMessages.Error_InvalidPackageFile({ e }));
145
+ }
146
+ return a;
147
+ })();
148
+ if (!kpsPackage) {
149
+ return null;
150
+ }
151
+ const kps = kpsPackage.Package;
152
+ return kps;
153
+ }
154
+ normalizePath = (path) => path || path === '' ? path.trim().replaceAll('\\', '/') : undefined;
155
+ /**
156
+ * @internal
157
+ */
158
+ transformKpsFileToKmpObject(kpsFilename, kps) {
159
+ //
160
+ // To convert to kmp.json, we need to:
161
+ //
162
+ // 1. Unwrap arrays (and convert to array where single object)
163
+ // 2. Fix casing on `iD`
164
+ // 3. Rewrap info, keyboard.languages, lexicalModel.languages, startMenu.items elements
165
+ // 4. Remove options.followKeyboardVersion, file.fileType
166
+ // 5. Convert file.copyLocation to a Number
167
+ // 6. Filenames need to be basenames (but this comes after processing)
168
+ //
169
+ // Start to construct the kmp.json file from the .kps file
170
+ let kmp = {
171
+ system: {
172
+ fileVersion: null,
173
+ keymanDeveloperVersion: KEYMAN_VERSION.VERSION
174
+ },
175
+ options: {}
176
+ };
177
+ //
178
+ // Fill in additional fields
179
+ //
180
+ if (kps.Options) {
181
+ kmp.options.executeProgram = this.normalizePath(kps.Options.ExecuteProgram || undefined);
182
+ kmp.options.graphicFile = this.normalizePath(kps.Options.GraphicFile || undefined);
183
+ kmp.options.msiFilename = this.normalizePath(kps.Options.MSIFileName || undefined);
184
+ kmp.options.msiOptions = kps.Options.MSIOptions || undefined;
185
+ kmp.options.readmeFile = this.normalizePath(kps.Options.ReadMeFile || undefined);
186
+ kmp.options.licenseFile = this.normalizePath(kps.Options.LicenseFile || undefined);
187
+ kmp.options.welcomeFile = this.normalizePath(kps.Options.WelcomeFile || undefined);
188
+ }
189
+ //
190
+ // Add basic metadata
191
+ //
192
+ if (kps.Info) {
193
+ kmp.info = this.kpsInfoToKmpInfo(kps.Info);
194
+ }
195
+ //
196
+ // Add related package metadata
197
+ //
198
+ if (kps.RelatedPackages) {
199
+ // Note: 'relationship' field is required for kmp.json but optional for .kps, only
200
+ // two values are supported -- deprecates or related.
201
+ kmp.relatedPackages = this.arrayWrap(kps.RelatedPackages.RelatedPackage).map(p => ({ id: p.$.ID, relationship: p.$.Relationship == 'deprecates' ? 'deprecates' : 'related' }));
202
+ }
203
+ //
204
+ // Add file metadata
205
+ //
206
+ if (kps.Files && kps.Files.File) {
207
+ kmp.files = this.arrayWrap(kps.Files.File).map((file) => {
208
+ return {
209
+ name: this.normalizePath(file.Name),
210
+ description: file.Description.trim(),
211
+ copyLocation: parseInt(file.CopyLocation, 10) || undefined
212
+ // note: we don't emit fileType as that is not permitted in kmp.json
213
+ };
214
+ });
215
+ }
216
+ kmp.files = kmp.files ?? [];
217
+ // Keyboard packages also include a legacy kmp.inf file (this will be removed,
218
+ // one day)
219
+ if (kps.Keyboards && kps.Keyboards.Keyboard) {
220
+ kmp.files.push({
221
+ name: KMP_INF_FILENAME,
222
+ description: "Package information"
223
+ });
224
+ }
225
+ // Add the standard kmp.json self-referential to match existing implementations
226
+ kmp.files.push({
227
+ name: KMP_JSON_FILENAME,
228
+ description: "Package information (JSON)"
229
+ });
230
+ //
231
+ // Add keyboard metadata
232
+ //
233
+ if (kps.Keyboards && kps.Keyboards.Keyboard) {
234
+ kmp.keyboards = this.arrayWrap(kps.Keyboards.Keyboard).map((keyboard) => ({
235
+ displayFont: keyboard.DisplayFont ? this.callbacks.path.basename(this.normalizePath(keyboard.DisplayFont)) : undefined,
236
+ oskFont: keyboard.OSKFont ? this.callbacks.path.basename(this.normalizePath(keyboard.OSKFont)) : undefined,
237
+ name: keyboard.Name.trim(),
238
+ id: keyboard.ID.trim(),
239
+ version: keyboard.Version.trim(),
240
+ rtl: keyboard.RTL == 'True' ? true : undefined,
241
+ languages: keyboard.Languages ?
242
+ this.kpsLanguagesToKmpLanguages(this.arrayWrap(keyboard.Languages.Language)) :
243
+ [],
244
+ examples: keyboard.Examples ?
245
+ this.arrayWrap(keyboard.Examples.Example).map(e => ({ id: e.$.ID, keys: e.$.Keys, text: e.$.Text, note: e.$.Note })) :
246
+ undefined,
247
+ webDisplayFonts: keyboard.WebDisplayFonts ?
248
+ this.arrayWrap(keyboard.WebDisplayFonts.Font).map(e => (this.callbacks.path.basename(this.normalizePath(e.$.Filename)))) :
249
+ undefined,
250
+ webOskFonts: keyboard.WebOSKFonts ?
251
+ this.arrayWrap(keyboard.WebOSKFonts.Font).map(e => (this.callbacks.path.basename(this.normalizePath(e.$.Filename)))) :
252
+ undefined,
253
+ }));
254
+ }
255
+ //
256
+ // Add lexical-model metadata
257
+ //
258
+ if (kps.LexicalModels && kps.LexicalModels.LexicalModel) {
259
+ kmp.lexicalModels = this.arrayWrap(kps.LexicalModels.LexicalModel).map((model) => ({
260
+ name: model.Name.trim(),
261
+ id: model.ID.trim(),
262
+ languages: model.Languages ?
263
+ this.kpsLanguagesToKmpLanguages(this.arrayWrap(model.Languages.Language)) : []
264
+ }));
265
+ }
266
+ //
267
+ // Collect metadata from keyboards (and later models) in order to update
268
+ // the kmp.json metadata for use downstream in apps. This will also be
269
+ // used later to fill in .keyboard_info file data.
270
+ //
271
+ const collector = new PackageMetadataCollector(this.callbacks);
272
+ const metadata = collector.collectKeyboardMetadata(kpsFilename, kmp);
273
+ if (metadata == null) {
274
+ return null;
275
+ }
276
+ //
277
+ // Verify keyboard versions and update version metadata where appropriate
278
+ //
279
+ const versionValidator = new PackageVersionValidator(this.callbacks);
280
+ if (!versionValidator.validateAndUpdateVersions(kps, kmp, metadata)) {
281
+ return null;
282
+ }
283
+ if (kps.Keyboards && kps.Keyboards.Keyboard) {
284
+ kmp.system.fileVersion = versionValidator.getMinKeymanVersion(metadata);
285
+ }
286
+ else {
287
+ kmp.system.fileVersion = MIN_LM_FILEVERSION_KMP_JSON;
288
+ }
289
+ //
290
+ // Verify that packages that target mobile devices include a .js file
291
+ //
292
+ const targetValidator = new PackageKeyboardTargetValidator(this.callbacks);
293
+ targetValidator.verifyAllTargets(kmp, metadata);
294
+ //
295
+ // Update assorted keyboard metadata from the keyboards in the package
296
+ //
297
+ const updater = new PackageMetadataUpdater();
298
+ updater.updatePackage(metadata);
299
+ //
300
+ // Add Windows Start Menu metadata
301
+ //
302
+ if (kps.StartMenu && (kps.StartMenu.Folder || kps.StartMenu.Items)) {
303
+ kmp.startMenu = {};
304
+ if (kps.StartMenu.AddUninstallEntry === '')
305
+ kmp.startMenu.addUninstallEntry = true;
306
+ if (kps.StartMenu.Folder)
307
+ kmp.startMenu.folder = kps.StartMenu.Folder;
308
+ if (kps.StartMenu.Items && kps.StartMenu.Items.Item) {
309
+ kmp.startMenu.items = this.arrayWrap(kps.StartMenu.Items.Item).map((item) => ({
310
+ filename: item.FileName,
311
+ name: item.Name,
312
+ arguments: item.Arguments,
313
+ icon: item.Icon,
314
+ location: item.Location
315
+ }));
316
+ // Remove default values
317
+ for (let item of kmp.startMenu.items) {
318
+ if (item.icon == '')
319
+ delete item.icon;
320
+ if (item.location == 'psmelStartMenu')
321
+ delete item.location;
322
+ if (item.arguments == '')
323
+ delete item.arguments;
324
+ }
325
+ }
326
+ else {
327
+ kmp.startMenu.items = [];
328
+ }
329
+ }
330
+ kmp = this.stripUndefined(kmp);
331
+ return kmp;
332
+ }
333
+ // Helper functions
334
+ kpsInfoToKmpInfo(kpsInfo) {
335
+ let kmpInfo = {};
336
+ const keys = [
337
+ ['Author', 'author', false],
338
+ ['Copyright', 'copyright', false],
339
+ ['Name', 'name', false],
340
+ ['Version', 'version', false],
341
+ ['WebSite', 'website', false],
342
+ ['Description', 'description', true],
343
+ ];
344
+ for (let [src, dst, isMarkdown] of keys) {
345
+ if (kpsInfo[src]) {
346
+ kmpInfo[dst] = {
347
+ description: (kpsInfo[src]._ ?? (typeof kpsInfo[src] == 'string' ? kpsInfo[src].toString() : '')).trim()
348
+ };
349
+ if (isMarkdown) {
350
+ kmpInfo[dst].description = markdownToHTML(kmpInfo[dst].description, false).trim();
351
+ }
352
+ if (kpsInfo[src].$?.URL) {
353
+ kmpInfo[dst].url = kpsInfo[src].$.URL.trim();
354
+ }
355
+ }
356
+ }
357
+ return kmpInfo;
358
+ }
359
+ ;
360
+ arrayWrap(a) {
361
+ if (Array.isArray(a)) {
362
+ return a;
363
+ }
364
+ return [a];
365
+ }
366
+ ;
367
+ kpsLanguagesToKmpLanguages(language) {
368
+ if (language.length == 0 || language[0] == undefined) {
369
+ return [];
370
+ }
371
+ return language.map((element) => { return { name: element._, id: element.$.ID }; });
372
+ }
373
+ ;
374
+ stripUndefined(o) {
375
+ for (const key in o) {
376
+ if (o[key] === undefined) {
377
+ delete o[key];
378
+ }
379
+ else if (typeof o[key] == 'object') {
380
+ o[key] = this.stripUndefined(o[key]);
381
+ }
382
+ }
383
+ return o;
384
+ }
385
+ /**
386
+ * @internal
387
+ * Returns a Promise to the serialized data which can then be written to a .kmp file.
388
+ *
389
+ * @param kpsFilename - Filename of the kps, not read, used only for calculating relative paths
390
+ * @param kmpJsonData - The kmp.json Object
391
+ */
392
+ buildKmpFile(kpsFilename, kmpJsonData) {
393
+ const zip = JSZip();
394
+ // Make a copy of kmpJsonData, as we mutate paths for writing
395
+ const data = JSON.parse(JSON.stringify(kmpJsonData));
396
+ if (!data.files) {
397
+ data.files = [];
398
+ }
399
+ const hasKmpInf = !!data.files.find(file => file.name == KMP_INF_FILENAME);
400
+ let failed = false;
401
+ data.files.forEach((value) => {
402
+ // Get the path of the file
403
+ let filename = value.name;
404
+ // We add this separately after zipping all other files
405
+ if (filename == KMP_JSON_FILENAME || filename == KMP_INF_FILENAME) {
406
+ return;
407
+ }
408
+ if (this.callbacks.path.isAbsolute(filename)) {
409
+ // absolute paths are not portable to other computers
410
+ this.callbacks.reportMessage(CompilerMessages.Warn_AbsolutePath({ filename: filename }));
411
+ }
412
+ filename = this.callbacks.resolveFilename(kpsFilename, filename);
413
+ const basename = this.callbacks.path.basename(filename);
414
+ if (!this.callbacks.fs.existsSync(filename)) {
415
+ this.callbacks.reportMessage(CompilerMessages.Error_FileDoesNotExist({ filename: filename }));
416
+ failed = true;
417
+ return;
418
+ }
419
+ let memberFileData;
420
+ try {
421
+ memberFileData = this.callbacks.loadFile(filename);
422
+ }
423
+ catch (e) {
424
+ this.callbacks.reportMessage(CompilerMessages.Error_FileCouldNotBeRead({ filename: filename, e: e }));
425
+ failed = true;
426
+ return;
427
+ }
428
+ this.warnIfKvkFileIsNotBinary(filename, memberFileData);
429
+ zip.file(basename, memberFileData);
430
+ // Remove path data from files before JSON save
431
+ value.name = basename;
432
+ });
433
+ if (failed) {
434
+ return null;
435
+ }
436
+ // TODO #9477: transform .md to .htm
437
+ // Remove path data from file references in options
438
+ if (data.options.graphicFile) {
439
+ data.options.graphicFile = this.callbacks.path.basename(data.options.graphicFile);
440
+ }
441
+ if (data.options.readmeFile) {
442
+ data.options.readmeFile = this.callbacks.path.basename(data.options.readmeFile);
443
+ }
444
+ if (data.options.licenseFile) {
445
+ data.options.licenseFile = this.callbacks.path.basename(data.options.licenseFile);
446
+ }
447
+ if (data.options.welcomeFile) {
448
+ data.options.welcomeFile = this.callbacks.path.basename(data.options.welcomeFile);
449
+ }
450
+ else if (data.files.find(file => file.name == WELCOME_HTM_FILENAME)) {
451
+ // We will, for improved backward-compatibility with existing packages, add a
452
+ // reference to the file welcome.htm is it is present in the package. This allows
453
+ // newer tools to avoid knowing about welcome.htm, if we assume that they work with
454
+ // packages compiled with kmc-package (17.0+) and not kmcomp (5.x-16.x).
455
+ data.options.welcomeFile = WELCOME_HTM_FILENAME;
456
+ }
457
+ if (data.options.msiFilename) {
458
+ data.options.msiFilename = this.callbacks.path.basename(data.options.msiFilename);
459
+ }
460
+ // Write kmp.json and kmp.inf
461
+ zip.file(KMP_JSON_FILENAME, JSON.stringify(data, null, 2));
462
+ if (hasKmpInf) {
463
+ zip.file(KMP_INF_FILENAME, this.buildKmpInf(data));
464
+ }
465
+ // Generate kmp file
466
+ return zip.generateAsync({ type: 'uint8array', compression: 'DEFLATE' });
467
+ }
468
+ buildKmpInf(data) {
469
+ const writer = new KmpInfWriter(data);
470
+ const s = writer.write();
471
+ return transcodeToCP1252(s);
472
+ }
473
+ /**
474
+ * Legacy .kmp compiler would transform xml-format .kvk files into a binary .kvk file; now
475
+ * we want that to remain the responsibility of the keyboard compiler, so we'll warn the
476
+ * few users who are still doing this
477
+ */
478
+ warnIfKvkFileIsNotBinary(filename, data) {
479
+ if (!KeymanFileTypes.filenameIs(filename, ".kvk" /* KeymanFileTypes.Binary.VisualKeyboard */)) {
480
+ return;
481
+ }
482
+ if (data.byteLength < 4) {
483
+ // TODO: Not a valid .kvk file; should we be reporting this?
484
+ return;
485
+ }
486
+ if (data[0] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[0] ||
487
+ data[1] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[1] ||
488
+ data[2] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[2] ||
489
+ data[3] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[3]) {
490
+ this.callbacks.reportMessage(CompilerMessages.Warn_FileIsNotABinaryKvkFile({ filename: filename }));
491
+ }
492
+ }
493
+ }
494
494
  //# sourceMappingURL=kmp-compiler.js.map
495
+ //# debugId=b6fb6e95-5ebd-510f-bb92-192fc2ad2371