@keymanapp/kmc-package 18.0.41-alpha → 18.0.45-alpha

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