@keymanapp/kmc-kmn 18.0.40-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 (45) hide show
  1. package/build/src/compiler/compiler.d.ts +181 -181
  2. package/build/src/compiler/compiler.js +493 -496
  3. package/build/src/compiler/compiler.js.map +1 -1
  4. package/build/src/compiler/kmn-compiler-messages.d.ts +458 -458
  5. package/build/src/compiler/kmn-compiler-messages.d.ts.map +1 -1
  6. package/build/src/compiler/kmn-compiler-messages.js +459 -462
  7. package/build/src/compiler/kmn-compiler-messages.js.map +1 -1
  8. package/build/src/compiler/osk.d.ts +31 -31
  9. package/build/src/compiler/osk.js +84 -87
  10. package/build/src/compiler/osk.js.map +1 -1
  11. package/build/src/import/kmcmplib/wasm-host.d.ts +2 -2
  12. package/build/src/import/kmcmplib/wasm-host.js +3262 -3265
  13. package/build/src/import/kmcmplib/wasm-host.js.map +1 -1
  14. package/build/src/import/kmcmplib/wasm-host.wasm +0 -0
  15. package/build/src/kmw-compiler/compiler-globals.d.ts +19 -19
  16. package/build/src/kmw-compiler/compiler-globals.js +38 -41
  17. package/build/src/kmw-compiler/compiler-globals.js.map +1 -1
  18. package/build/src/kmw-compiler/constants.d.ts +16 -16
  19. package/build/src/kmw-compiler/constants.js +79 -82
  20. package/build/src/kmw-compiler/constants.js.map +1 -1
  21. package/build/src/kmw-compiler/javascript-strings.d.ts +32 -32
  22. package/build/src/kmw-compiler/javascript-strings.js +852 -855
  23. package/build/src/kmw-compiler/javascript-strings.js.map +1 -1
  24. package/build/src/kmw-compiler/keymanweb-key-codes.d.ts +22 -22
  25. package/build/src/kmw-compiler/keymanweb-key-codes.js +819 -822
  26. package/build/src/kmw-compiler/keymanweb-key-codes.js.map +1 -1
  27. package/build/src/kmw-compiler/kmw-compiler-messages.d.ts +30 -30
  28. package/build/src/kmw-compiler/kmw-compiler-messages.js +40 -43
  29. package/build/src/kmw-compiler/kmw-compiler-messages.js.map +1 -1
  30. package/build/src/kmw-compiler/kmw-compiler.d.ts +13 -13
  31. package/build/src/kmw-compiler/kmw-compiler.js +547 -550
  32. package/build/src/kmw-compiler/kmw-compiler.js.map +1 -1
  33. package/build/src/kmw-compiler/util.d.ts +74 -74
  34. package/build/src/kmw-compiler/util.js +247 -250
  35. package/build/src/kmw-compiler/util.js.map +1 -1
  36. package/build/src/kmw-compiler/validate-layout-file.d.ts +8 -8
  37. package/build/src/kmw-compiler/validate-layout-file.js +276 -279
  38. package/build/src/kmw-compiler/validate-layout-file.js.map +1 -1
  39. package/build/src/kmw-compiler/visual-keyboard-compiler.d.ts +2 -2
  40. package/build/src/kmw-compiler/visual-keyboard-compiler.js +119 -122
  41. package/build/src/kmw-compiler/visual-keyboard-compiler.js.map +1 -1
  42. package/build/src/main.d.ts +9 -9
  43. package/build/src/main.js +10 -13
  44. package/build/src/main.js.map +1 -1
  45. package/package.json +5 -5
@@ -1,497 +1,494 @@
1
- /*
2
- TODO: implement additional interfaces:
3
-
4
- typedef bool (*kmcmp_ValidateJsonMessageProc)();
5
- extern "C" bool kmcmp_ValidateJsonFile();
6
- */
7
- // TODO: rename wasm-host?
1
+ /*
2
+ TODO: implement additional interfaces:
8
3
 
9
- !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]="34494c24-3f16-529c-b3cb-80e168f103f9")}catch(e){}}();
10
- import { UnicodeSet, KvkFileReader } from '@keymanapp/common-types';
11
- import { KvkFileWriter, KvksFileReader } from '@keymanapp/common-types';
12
- import * as Osk from './osk.js';
13
- import loadWasmHost from '../import/kmcmplib/wasm-host.js';
14
- import { CompilerMessages, mapErrorFromKmcmplib } from './kmn-compiler-messages.js';
15
- import { WriteCompiledKeyboard } from '../kmw-compiler/kmw-compiler.js';
16
- //
17
- // Matches kmcmplibapi.h definitions
18
- //
19
- export const STORETYPE_STORE = 0x01;
20
- export const STORETYPE_RESERVED = 0x02;
21
- export const STORETYPE_OPTION = 0x04;
22
- export const STORETYPE_DEBUG = 0x08;
23
- export const STORETYPE_CALL = 0x10;
24
- export const STORETYPE__MASK = 0x1F;
25
- ;
26
- ;
27
- export const COMPILETARGETS_KMX = 0x01;
28
- export const COMPILETARGETS_JS = 0x02;
29
- export const COMPILETARGETS__MASK = 0x03;
30
- ;
31
- ;
32
- ;
33
- ;
34
- const baseOptions = {
35
- shouldAddCompilerVersion: true,
36
- saveDebug: true,
37
- compilerWarningsAsErrors: false,
38
- warnDeprecatedCode: true,
39
- };
40
- /**
41
- * Allows multiple instances of the Compiler class, by ensuring that the
42
- * 'unique' kmnCompilerCallback global will be correlated with a specific
43
- * instance of the Compiler class
44
- */
45
- let callbackProcIdentifier = 0;
46
- const callbackPrefix = 'kmnCompilerCallbacks_';
47
- ;
48
- let Module;
49
- /**
50
- * @public
51
- * Compiles a .kmn file to a .kmx, .kvk, and/or .js. The compiler does not read
52
- * or write from filesystem or network directly, but relies on callbacks for all
53
- * external IO.
54
- */
55
- export class KmnCompiler {
56
- callbackID; // a unique numeric id added to globals with prefixed names
57
- callbacks;
58
- wasmExports;
59
- options;
60
- constructor() {
61
- this.callbackID = callbackPrefix + callbackProcIdentifier.toString();
62
- callbackProcIdentifier++;
63
- }
64
- /**
65
- * Initialize the compiler, including loading the WASM host for kmcmplib.
66
- * Copies options.
67
- * @param callbacks - Callbacks for external interfaces, including message
68
- * reporting and file io
69
- * @param options - Compiler options
70
- * @returns false if initialization fails
71
- */
72
- async init(callbacks, options) {
73
- this.callbacks = callbacks;
74
- this.options = { ...options };
75
- if (!Module) {
76
- try {
77
- Module = await loadWasmHost();
78
- }
79
- catch (e) {
80
- /* c8 ignore next 3 */
81
- this.callbacks.reportMessage(CompilerMessages.Fatal_MissingWasmModule({ e }));
82
- return false;
83
- }
84
- }
85
- this.wasmExports = (Module.wasmExports ?? Module.asm);
86
- return this.verifyInitialized();
87
- }
88
- /**
89
- * Verify that wasm is spun up OK.
90
- * @returns true if OK
91
- */
92
- verifyInitialized() {
93
- if (!this.callbacks) {
94
- // Can't report a message here.
95
- throw Error('Must call Compiler.init(callbacks) before proceeding');
96
- }
97
- if (!Module) {
98
- /* c8 ignore next 4 */
99
- // fail if wasm not loaded or function not found
100
- this.callbacks.reportMessage(CompilerMessages.Fatal_MissingWasmModule({}));
101
- return false;
102
- }
103
- return true;
104
- }
105
- /**
106
- * Write artifacts from a successful compile to disk, via callbacks methods.
107
- * The artifacts written may include:
108
- *
109
- * - .kmx file - binary keyboard used by Keyman on desktop platforms
110
- * - .kvk file - binary on screen keyboard used by Keyman on desktop platforms
111
- * - .js file - Javascript keyboard for web and touch platforms
112
- *
113
- * @param artifacts - object containing artifact binary data to write out
114
- * @returns true on success
115
- */
116
- async write(artifacts) {
117
- if (!artifacts) {
118
- throw Error('artifacts must be defined');
119
- }
120
- if (artifacts.kmx) {
121
- this.callbacks.fs.writeFileSync(artifacts.kmx.filename, artifacts.kmx.data);
122
- }
123
- if (artifacts.kvk) {
124
- this.callbacks.fs.writeFileSync(artifacts.kvk.filename, artifacts.kvk.data);
125
- }
126
- if (artifacts.js) {
127
- this.callbacks.fs.writeFileSync(artifacts.js.filename, artifacts.js.data);
128
- }
129
- return true;
130
- }
131
- compilerMessageCallback = (line, code, msg) => {
132
- this.callbacks.reportMessage(mapErrorFromKmcmplib(line, code, msg));
133
- return 1;
134
- };
135
- cachedFile = {
136
- filename: null,
137
- data: null
138
- };
139
- loadFileCallback = (filename, baseFilename, buffer, bufferSize) => {
140
- let resolvedFilename = this.callbacks.resolveFilename(baseFilename, filename);
141
- let data;
142
- if (this.cachedFile.filename == resolvedFilename) {
143
- data = this.cachedFile.data;
144
- }
145
- else {
146
- data = this.callbacks.loadFile(resolvedFilename);
147
- if (!data) {
148
- return -1;
149
- }
150
- this.cachedFile.filename = resolvedFilename;
151
- this.cachedFile.data = data;
152
- }
153
- if (buffer == 0) {
154
- /* We need to return buffer size required */
155
- return data.byteLength;
156
- }
157
- if (bufferSize != data.byteLength) {
158
- /* c8 ignore next 2 */
159
- throw new Error(`loadFileCallback: second call, expected file size ${bufferSize} == ${data.byteLength}`);
160
- }
161
- Module.HEAP8.set(data, buffer);
162
- return 1;
163
- };
164
- copyWasmResult(wasm_result) {
165
- let result = {
166
- // We cannot Object.assign or {...} on a wasm-defined object, so...
167
- artifacts: {},
168
- extra: {
169
- targets: wasm_result.extra.targets,
170
- displayMapFilename: wasm_result.extra.displayMapFilename,
171
- kvksFilename: wasm_result.extra.kvksFilename,
172
- stores: [],
173
- groups: [],
174
- },
175
- displayMap: null
176
- };
177
- for (let store of wasm_result.extra.stores) {
178
- result.extra.stores.push({ storeType: store.storeType, name: store.name, line: store.line });
179
- }
180
- for (let group of wasm_result.extra.groups) {
181
- result.extra.groups.push({ isReadOnly: group.isReadOnly, name: group.name });
182
- }
183
- return result;
184
- }
185
- /**
186
- * By default, when a `Uint8Array` is created from an `ArrayBuffer` (e.g.
187
- * `Module.HEAP8.buffer`), it is a dynamic view into that buffer. This module
188
- * buffer can be dynamically reallocated at any time, which can happen when
189
- * allocating memory in WASM code (so the change will look _really_ weird in
190
- * a stack trace). Thus, to ensure we don't trip over ourselves, we need to
191
- * copy the buffer. Fortunately, creating a `Uint8Array` from a `Uint8Array`
192
- * copies the data, and is pretty quick.
193
- * @param offset - Offset into the WASM memory space, in bytes.
194
- * @param size - Size of the buffer to copy, in bytes.
195
- * @returns A _copy_ of the data in a new Uint8Array.
196
- */
197
- copyWasmBuffer(offset, size) {
198
- return new Uint8Array(new Uint8Array(Module.HEAP8.buffer, offset, size));
199
- }
200
- /**
201
- * Compiles a .kmn file to .kmx, .kvk, and/or .js files. Returns an object
202
- * containing binary artifacts on success. The files are passed in by name,
203
- * and the compiler will use callbacks as passed to the
204
- * {@link KmnCompiler.init} function to read any input files by disk.
205
- * @param infile - Path to source file. Path will be parsed to find relative
206
- * references in the .kmn file, such as icon or On Screen
207
- * Keyboard file
208
- * @param outfile - Path to output file. The file will not be written to, but
209
- * will be included in the result for use by
210
- * {@link KmnCompiler.write}.
211
- * @returns Binary artifacts on success, null on failure.
212
- */
213
- async run(infile, outfile) {
214
- if (!this.verifyInitialized()) {
215
- /* c8 ignore next 2 */
216
- return null;
217
- }
218
- const options = { ...baseOptions, ...this.options };
219
- outfile = outfile ?? infile.replace(/\.kmn$/i, '.kmx');
220
- globalThis[this.callbackID] = {
221
- message: this.compilerMessageCallback,
222
- loadFile: this.loadFileCallback
223
- };
224
- let wasm_interface = new Module.CompilerInterface();
225
- let wasm_options = new Module.CompilerOptions();
226
- let wasm_result = null;
227
- try {
228
- wasm_options.saveDebug = options.saveDebug;
229
- wasm_options.compilerWarningsAsErrors = options.compilerWarningsAsErrors;
230
- wasm_options.warnDeprecatedCode = options.warnDeprecatedCode;
231
- wasm_options.shouldAddCompilerVersion = options.shouldAddCompilerVersion;
232
- wasm_options.target = 0; // CKF_KEYMAN; TODO use COMPILETARGETS_KMX
233
- wasm_interface.callbacksKey = this.callbackID; // key of object on globalThis
234
- wasm_result = Module.kmcmp_compile(infile, wasm_options, wasm_interface);
235
- if (!wasm_result.result) {
236
- return null;
237
- }
238
- const result = this.copyWasmResult(wasm_result);
239
- if (result.extra.targets & COMPILETARGETS_KMX) {
240
- result.artifacts.kmx = {
241
- filename: outfile,
242
- data: this.copyWasmBuffer(wasm_result.kmx, wasm_result.kmxSize)
243
- };
244
- }
245
- //
246
- // Visual Keyboard transform
247
- //
248
- if (result.extra.displayMapFilename) {
249
- result.displayMap = this.loadDisplayMapping(infile, result.extra.displayMapFilename);
250
- if (!result.displayMap) {
251
- return null;
252
- }
253
- }
254
- if (result.extra.kvksFilename) {
255
- result.artifacts.kvk = this.runKvkCompiler(result.extra.kvksFilename, infile, outfile, result.displayMap);
256
- if (!result.artifacts.kvk) {
257
- return null;
258
- }
259
- }
260
- //
261
- // KeymanWeb compiler
262
- //
263
- if (wasm_result.extra.targets & COMPILETARGETS_JS) {
264
- wasm_options.target = 1; // CKF_KEYMANWEB TODO use COMPILETARGETS_JS
265
- // We always want debug data in the intermediate .kmx, so that error
266
- // messages from KMW compiler can give line numbers in .kmn. This
267
- // should have no impact on the final .js if options.debug is false
268
- wasm_options.saveDebug = true;
269
- wasm_result = Module.kmcmp_compile(infile, wasm_options, wasm_interface);
270
- if (!wasm_result.result) {
271
- return null;
272
- }
273
- const kmw_result = this.copyWasmResult(wasm_result);
274
- kmw_result.displayMap = result.displayMap; // we can safely re-use the kmx compile displayMap
275
- const web_kmx = this.copyWasmBuffer(wasm_result.kmx, wasm_result.kmxSize);
276
- result.artifacts.js = this.runWebCompiler(infile, outfile, web_kmx, result.artifacts.kvk?.data, kmw_result, options);
277
- if (!result.artifacts.js) {
278
- return null;
279
- }
280
- }
281
- return result;
282
- }
283
- catch (e) {
284
- /* c8 ignore next 3 */
285
- this.callbacks.reportMessage(CompilerMessages.Fatal_UnexpectedException({ e: e }));
286
- return null;
287
- }
288
- finally {
289
- if (wasm_result) {
290
- wasm_result.delete();
291
- }
292
- wasm_interface.delete();
293
- wasm_options.delete();
294
- delete globalThis[this.callbackID];
295
- }
296
- }
297
- runWebCompiler(kmnFilename, kmxFilename, web_kmx, kvk, kmxResult, options) {
298
- const data = WriteCompiledKeyboard(this.callbacks, kmnFilename, web_kmx, kvk, kmxResult, options.saveDebug);
299
- if (!data) {
300
- return null;
301
- }
302
- return {
303
- filename: this.callbacks.path.join(this.callbacks.path.dirname(kmxFilename), this.keyboardIdFromKmnFilename(kmnFilename) + ".js" /* KeymanFileTypes.Binary.WebKeyboard */),
304
- data: new TextEncoder().encode(data)
305
- };
306
- }
307
- keyboardIdFromKmnFilename(kmnFilename) {
308
- return this.callbacks.path.basename(kmnFilename, ".kmn" /* KeymanFileTypes.Source.KeymanKeyboard */);
309
- }
310
- runKvkCompiler(kvksFilename, kmnFilename, kmxFilename, displayMap) {
311
- // The compiler detected a .kvks file, which needs to be captured
312
- kvksFilename = this.callbacks.resolveFilename(kmnFilename, kvksFilename);
313
- const data = this.callbacks.loadFile(kvksFilename);
314
- if (!data) {
315
- this.callbacks.reportMessage(CompilerMessages.Error_FileNotFound({ filename: kvksFilename }));
316
- return null;
317
- }
318
- const filename = this.callbacks.path.basename(kvksFilename);
319
- let basename = null;
320
- let vk = null;
321
- if (filename.endsWith('.kvk')) {
322
- /* Legacy keyboards may reference a binary .kvk. That's not an error */
323
- // TODO: (lowpri) add hint to convert to .kvks?
324
- basename = this.callbacks.path.basename(kvksFilename, ".kvk" /* KeymanFileTypes.Binary.VisualKeyboard */);
325
- const reader = new KvkFileReader();
326
- try {
327
- vk = reader.read(data);
328
- }
329
- catch (e) {
330
- this.callbacks.reportMessage(CompilerMessages.Error_InvalidKvkFile({ filename, e }));
331
- return null;
332
- }
333
- }
334
- else {
335
- basename = this.callbacks.path.basename(kvksFilename, ".kvks" /* KeymanFileTypes.Source.VisualKeyboard */);
336
- const reader = new KvksFileReader();
337
- let kvks = null;
338
- try {
339
- kvks = reader.read(data);
340
- reader.validate(kvks);
341
- }
342
- catch (e) {
343
- this.callbacks.reportMessage(CompilerMessages.Error_InvalidKvksFile({ filename, e }));
344
- return null;
345
- }
346
- let invalidVkeys = [];
347
- vk = reader.transform(kvks, invalidVkeys);
348
- for (let invalidVkey of invalidVkeys) {
349
- this.callbacks.reportMessage(CompilerMessages.Warn_InvalidVkeyInKvksFile({ filename, invalidVkey }));
350
- }
351
- }
352
- // Make sure that we maintain the correspondence between source keyboard and
353
- // .kvk. Appears to be used currently only by Windows package installer.
354
- vk.header.associatedKeyboard = this.keyboardIdFromKmnFilename(kmnFilename);
355
- if (displayMap) {
356
- // Remap using the osk-char-use-rewriter
357
- Osk.remapVisualKeyboard(vk, displayMap);
358
- }
359
- let writer = new KvkFileWriter();
360
- return {
361
- filename: this.callbacks.path.join(this.callbacks.path.dirname(kmxFilename), basename + ".kvk" /* KeymanFileTypes.Binary.VisualKeyboard */),
362
- data: writer.write(vk)
363
- };
364
- }
365
- loadDisplayMapping(kmnFilename, displayMapFilename) {
366
- // Remap using the osk-char-use-rewriter
367
- displayMapFilename = this.callbacks.resolveFilename(kmnFilename, displayMapFilename);
368
- try {
369
- // Expected file format: displaymap.schema.json
370
- const data = this.callbacks.loadFile(displayMapFilename);
371
- if (!data) {
372
- this.callbacks.reportMessage(CompilerMessages.Error_FileNotFound({ filename: displayMapFilename }));
373
- return null;
374
- }
375
- const mapping = JSON.parse(new TextDecoder().decode(data));
376
- return Osk.parseMapping(mapping);
377
- }
378
- catch (e) {
379
- this.callbacks.reportMessage(CompilerMessages.Error_InvalidDisplayMapFile({ filename: displayMapFilename, e }));
380
- return null;
381
- }
382
- }
383
- /**
384
- * @internal
385
- * Generates an exception in kmcmplib to verify that Sentry error capture is
386
- * working correctly
387
- */
388
- testSentry() {
389
- if (!this.verifyInitialized()) {
390
- return null;
391
- }
392
- return Module.kmcmp_testSentry();
393
- }
394
- /**
395
- * @internal
396
- * convert `\u{1234}` to `\u1234` etc
397
- */
398
- static fixNewPattern(pattern) {
399
- pattern = pattern.replaceAll(/\\u\{([0-9a-fA-F]{6})\}/g, `\\U00$1`);
400
- pattern = pattern.replaceAll(/\\u\{([0-9a-fA-F]{5})\}/g, `\\U000$1`);
401
- pattern = pattern.replaceAll(/\\u\{([0-9a-fA-F]{4})\}/g, `\\u$1`);
402
- pattern = pattern.replaceAll(/\\u\{([0-9a-fA-F]{3})\}/g, `\\u0$1`);
403
- pattern = pattern.replaceAll(/\\u\{([0-9a-fA-F]{2})\}/g, `\\u00$1`);
404
- pattern = pattern.replaceAll(/\\u\{([0-9a-fA-F]{1})\}/g, `\\u000$1`);
405
- return pattern;
406
- }
407
- /**
408
- * @internal
409
- * @param pattern - UnicodeSet pattern such as `[a-z]`
410
- * @param rangeCount - number of ranges to allocate
411
- * @returns UnicodeSet accessor object, or null on failure
412
- */
413
- parseUnicodeSet(pattern, rangeCount) {
414
- if (!this.verifyInitialized()) {
415
- /* c8 ignore next 2 */
416
- // verifyInitialized will set a callback if needed
417
- return null;
418
- }
419
- if ((rangeCount * 2) < 0) {
420
- throw new RangeError(`Internal error: negative rangeCount * 2 = ${rangeCount * 2}`);
421
- }
422
- const buf = this.wasmExports.malloc(rangeCount * 2 * Module.HEAPU32.BYTES_PER_ELEMENT);
423
- if (buf <= 0) {
424
- // out of memory will return zero.
425
- throw new RangeError(`Internal error: wasm malloc() returned ${buf}`);
426
- }
427
- // fix \u1234 pattern format
428
- pattern = KmnCompiler.fixNewPattern(pattern);
429
- const rc = Module.kmcmp_parseUnicodeSet(pattern, buf, rangeCount * 2);
430
- if (rc >= 0) {
431
- // If >= 0: it's a range count (which could be zero, an empty set).
432
- const ranges = [];
433
- const startu = (buf / Module.HEAPU32.BYTES_PER_ELEMENT);
434
- for (let i = 0; i < rc; i++) {
435
- const start = Module.HEAPU32[startu + (i * 2) + 0];
436
- const end = Module.HEAPU32[startu + (i * 2) + 1];
437
- ranges.push([start, end]);
438
- }
439
- this.wasmExports.free(buf);
440
- return new UnicodeSet(pattern, ranges);
441
- }
442
- else {
443
- // rc is negative: it's an error code.
444
- this.wasmExports.free(buf);
445
- // translate error code into callback
446
- this.callbacks.reportMessage(getUnicodeSetError(rc));
447
- return null;
448
- }
449
- }
450
- /**
451
- * @internal
452
- */
453
- sizeUnicodeSet(pattern) {
454
- if (!this.verifyInitialized()) {
455
- /* c8 ignore next 2 */
456
- return null;
457
- }
458
- // fix \u1234 pattern format
459
- pattern = KmnCompiler.fixNewPattern(pattern);
460
- // call with rangeCount = 0 to invoke in 'preflight' mode.
461
- const rc = Module.kmcmp_parseUnicodeSet(pattern, 0, 0);
462
- if (rc >= 0) {
463
- return rc;
464
- }
465
- else {
466
- this.callbacks.reportMessage(getUnicodeSetError(rc));
467
- return -1;
468
- }
469
- }
470
- }
471
- /**
472
- * translate UnicodeSet return code into a compiler event
473
- * @param rc parseUnicodeSet error code
474
- * @returns the compiler event
475
- */
476
- function getUnicodeSetError(rc) {
477
- // from kmcmplib.h
478
- const KMCMP_ERROR_SYNTAX_ERR = -1;
479
- const KMCMP_ERROR_HAS_STRINGS = -2;
480
- const KMCMP_ERROR_UNSUPPORTED_PROPERTY = -3;
481
- const KMCMP_FATAL_OUT_OF_RANGE = -4;
482
- switch (rc) {
483
- case KMCMP_ERROR_SYNTAX_ERR:
484
- return CompilerMessages.Error_UnicodeSetSyntaxError();
485
- case KMCMP_ERROR_HAS_STRINGS:
486
- return CompilerMessages.Error_UnicodeSetHasStrings();
487
- case KMCMP_ERROR_UNSUPPORTED_PROPERTY:
488
- return CompilerMessages.Error_UnicodeSetHasProperties();
489
- case KMCMP_FATAL_OUT_OF_RANGE:
490
- return CompilerMessages.Fatal_UnicodeSetOutOfRange();
491
- default:
492
- /* c8 ignore next */
493
- return CompilerMessages.Fatal_UnexpectedException({ e: `Unexpected UnicodeSet error code ${rc}` });
494
- }
495
- }
496
- //# sourceMappingURL=compiler.js.map
497
- //# debugId=34494c24-3f16-529c-b3cb-80e168f103f9
4
+ typedef bool (*kmcmp_ValidateJsonMessageProc)();
5
+ extern "C" bool kmcmp_ValidateJsonFile();
6
+ */
7
+ // TODO: rename wasm-host?
8
+ import { UnicodeSet, KvkFileReader } from '@keymanapp/common-types';
9
+ import { KvkFileWriter, KvksFileReader } from '@keymanapp/common-types';
10
+ import * as Osk from './osk.js';
11
+ import loadWasmHost from '../import/kmcmplib/wasm-host.js';
12
+ import { CompilerMessages, mapErrorFromKmcmplib } from './kmn-compiler-messages.js';
13
+ import { WriteCompiledKeyboard } from '../kmw-compiler/kmw-compiler.js';
14
+ //
15
+ // Matches kmcmplibapi.h definitions
16
+ //
17
+ export const STORETYPE_STORE = 0x01;
18
+ export const STORETYPE_RESERVED = 0x02;
19
+ export const STORETYPE_OPTION = 0x04;
20
+ export const STORETYPE_DEBUG = 0x08;
21
+ export const STORETYPE_CALL = 0x10;
22
+ export const STORETYPE__MASK = 0x1F;
23
+ ;
24
+ ;
25
+ export const COMPILETARGETS_KMX = 0x01;
26
+ export const COMPILETARGETS_JS = 0x02;
27
+ export const COMPILETARGETS__MASK = 0x03;
28
+ ;
29
+ ;
30
+ ;
31
+ ;
32
+ const baseOptions = {
33
+ shouldAddCompilerVersion: true,
34
+ saveDebug: true,
35
+ compilerWarningsAsErrors: false,
36
+ warnDeprecatedCode: true,
37
+ };
38
+ /**
39
+ * Allows multiple instances of the Compiler class, by ensuring that the
40
+ * 'unique' kmnCompilerCallback global will be correlated with a specific
41
+ * instance of the Compiler class
42
+ */
43
+ let callbackProcIdentifier = 0;
44
+ const callbackPrefix = 'kmnCompilerCallbacks_';
45
+ ;
46
+ let Module;
47
+ /**
48
+ * @public
49
+ * Compiles a .kmn file to a .kmx, .kvk, and/or .js. The compiler does not read
50
+ * or write from filesystem or network directly, but relies on callbacks for all
51
+ * external IO.
52
+ */
53
+ export class KmnCompiler {
54
+ callbackID; // a unique numeric id added to globals with prefixed names
55
+ callbacks;
56
+ wasmExports;
57
+ options;
58
+ constructor() {
59
+ this.callbackID = callbackPrefix + callbackProcIdentifier.toString();
60
+ callbackProcIdentifier++;
61
+ }
62
+ /**
63
+ * Initialize the compiler, including loading the WASM host for kmcmplib.
64
+ * Copies options.
65
+ * @param callbacks - Callbacks for external interfaces, including message
66
+ * reporting and file io
67
+ * @param options - Compiler options
68
+ * @returns false if initialization fails
69
+ */
70
+ async init(callbacks, options) {
71
+ this.callbacks = callbacks;
72
+ this.options = { ...options };
73
+ if (!Module) {
74
+ try {
75
+ Module = await loadWasmHost();
76
+ }
77
+ catch (e) {
78
+ /* c8 ignore next 3 */
79
+ this.callbacks.reportMessage(CompilerMessages.Fatal_MissingWasmModule({ e }));
80
+ return false;
81
+ }
82
+ }
83
+ this.wasmExports = (Module.wasmExports ?? Module.asm);
84
+ return this.verifyInitialized();
85
+ }
86
+ /**
87
+ * Verify that wasm is spun up OK.
88
+ * @returns true if OK
89
+ */
90
+ verifyInitialized() {
91
+ if (!this.callbacks) {
92
+ // Can't report a message here.
93
+ throw Error('Must call Compiler.init(callbacks) before proceeding');
94
+ }
95
+ if (!Module) {
96
+ /* c8 ignore next 4 */
97
+ // fail if wasm not loaded or function not found
98
+ this.callbacks.reportMessage(CompilerMessages.Fatal_MissingWasmModule({}));
99
+ return false;
100
+ }
101
+ return true;
102
+ }
103
+ /**
104
+ * Write artifacts from a successful compile to disk, via callbacks methods.
105
+ * The artifacts written may include:
106
+ *
107
+ * - .kmx file - binary keyboard used by Keyman on desktop platforms
108
+ * - .kvk file - binary on screen keyboard used by Keyman on desktop platforms
109
+ * - .js file - Javascript keyboard for web and touch platforms
110
+ *
111
+ * @param artifacts - object containing artifact binary data to write out
112
+ * @returns true on success
113
+ */
114
+ async write(artifacts) {
115
+ if (!artifacts) {
116
+ throw Error('artifacts must be defined');
117
+ }
118
+ if (artifacts.kmx) {
119
+ this.callbacks.fs.writeFileSync(artifacts.kmx.filename, artifacts.kmx.data);
120
+ }
121
+ if (artifacts.kvk) {
122
+ this.callbacks.fs.writeFileSync(artifacts.kvk.filename, artifacts.kvk.data);
123
+ }
124
+ if (artifacts.js) {
125
+ this.callbacks.fs.writeFileSync(artifacts.js.filename, artifacts.js.data);
126
+ }
127
+ return true;
128
+ }
129
+ compilerMessageCallback = (line, code, msg) => {
130
+ this.callbacks.reportMessage(mapErrorFromKmcmplib(line, code, msg));
131
+ return 1;
132
+ };
133
+ cachedFile = {
134
+ filename: null,
135
+ data: null
136
+ };
137
+ loadFileCallback = (filename, baseFilename, buffer, bufferSize) => {
138
+ let resolvedFilename = this.callbacks.resolveFilename(baseFilename, filename);
139
+ let data;
140
+ if (this.cachedFile.filename == resolvedFilename) {
141
+ data = this.cachedFile.data;
142
+ }
143
+ else {
144
+ data = this.callbacks.loadFile(resolvedFilename);
145
+ if (!data) {
146
+ return -1;
147
+ }
148
+ this.cachedFile.filename = resolvedFilename;
149
+ this.cachedFile.data = data;
150
+ }
151
+ if (buffer == 0) {
152
+ /* We need to return buffer size required */
153
+ return data.byteLength;
154
+ }
155
+ if (bufferSize != data.byteLength) {
156
+ /* c8 ignore next 2 */
157
+ throw new Error(`loadFileCallback: second call, expected file size ${bufferSize} == ${data.byteLength}`);
158
+ }
159
+ Module.HEAP8.set(data, buffer);
160
+ return 1;
161
+ };
162
+ copyWasmResult(wasm_result) {
163
+ let result = {
164
+ // We cannot Object.assign or {...} on a wasm-defined object, so...
165
+ artifacts: {},
166
+ extra: {
167
+ targets: wasm_result.extra.targets,
168
+ displayMapFilename: wasm_result.extra.displayMapFilename,
169
+ kvksFilename: wasm_result.extra.kvksFilename,
170
+ stores: [],
171
+ groups: [],
172
+ },
173
+ displayMap: null
174
+ };
175
+ for (let store of wasm_result.extra.stores) {
176
+ result.extra.stores.push({ storeType: store.storeType, name: store.name, line: store.line });
177
+ }
178
+ for (let group of wasm_result.extra.groups) {
179
+ result.extra.groups.push({ isReadOnly: group.isReadOnly, name: group.name });
180
+ }
181
+ return result;
182
+ }
183
+ /**
184
+ * By default, when a `Uint8Array` is created from an `ArrayBuffer` (e.g.
185
+ * `Module.HEAP8.buffer`), it is a dynamic view into that buffer. This module
186
+ * buffer can be dynamically reallocated at any time, which can happen when
187
+ * allocating memory in WASM code (so the change will look _really_ weird in
188
+ * a stack trace). Thus, to ensure we don't trip over ourselves, we need to
189
+ * copy the buffer. Fortunately, creating a `Uint8Array` from a `Uint8Array`
190
+ * copies the data, and is pretty quick.
191
+ * @param offset - Offset into the WASM memory space, in bytes.
192
+ * @param size - Size of the buffer to copy, in bytes.
193
+ * @returns A _copy_ of the data in a new Uint8Array.
194
+ */
195
+ copyWasmBuffer(offset, size) {
196
+ return new Uint8Array(new Uint8Array(Module.HEAP8.buffer, offset, size));
197
+ }
198
+ /**
199
+ * Compiles a .kmn file to .kmx, .kvk, and/or .js files. Returns an object
200
+ * containing binary artifacts on success. The files are passed in by name,
201
+ * and the compiler will use callbacks as passed to the
202
+ * {@link KmnCompiler.init} function to read any input files by disk.
203
+ * @param infile - Path to source file. Path will be parsed to find relative
204
+ * references in the .kmn file, such as icon or On Screen
205
+ * Keyboard file
206
+ * @param outfile - Path to output file. The file will not be written to, but
207
+ * will be included in the result for use by
208
+ * {@link KmnCompiler.write}.
209
+ * @returns Binary artifacts on success, null on failure.
210
+ */
211
+ async run(infile, outfile) {
212
+ if (!this.verifyInitialized()) {
213
+ /* c8 ignore next 2 */
214
+ return null;
215
+ }
216
+ const options = { ...baseOptions, ...this.options };
217
+ outfile = outfile ?? infile.replace(/\.kmn$/i, '.kmx');
218
+ globalThis[this.callbackID] = {
219
+ message: this.compilerMessageCallback,
220
+ loadFile: this.loadFileCallback
221
+ };
222
+ let wasm_interface = new Module.CompilerInterface();
223
+ let wasm_options = new Module.CompilerOptions();
224
+ let wasm_result = null;
225
+ try {
226
+ wasm_options.saveDebug = options.saveDebug;
227
+ wasm_options.compilerWarningsAsErrors = options.compilerWarningsAsErrors;
228
+ wasm_options.warnDeprecatedCode = options.warnDeprecatedCode;
229
+ wasm_options.shouldAddCompilerVersion = options.shouldAddCompilerVersion;
230
+ wasm_options.target = 0; // CKF_KEYMAN; TODO use COMPILETARGETS_KMX
231
+ wasm_interface.callbacksKey = this.callbackID; // key of object on globalThis
232
+ wasm_result = Module.kmcmp_compile(infile, wasm_options, wasm_interface);
233
+ if (!wasm_result.result) {
234
+ return null;
235
+ }
236
+ const result = this.copyWasmResult(wasm_result);
237
+ if (result.extra.targets & COMPILETARGETS_KMX) {
238
+ result.artifacts.kmx = {
239
+ filename: outfile,
240
+ data: this.copyWasmBuffer(wasm_result.kmx, wasm_result.kmxSize)
241
+ };
242
+ }
243
+ //
244
+ // Visual Keyboard transform
245
+ //
246
+ if (result.extra.displayMapFilename) {
247
+ result.displayMap = this.loadDisplayMapping(infile, result.extra.displayMapFilename);
248
+ if (!result.displayMap) {
249
+ return null;
250
+ }
251
+ }
252
+ if (result.extra.kvksFilename) {
253
+ result.artifacts.kvk = this.runKvkCompiler(result.extra.kvksFilename, infile, outfile, result.displayMap);
254
+ if (!result.artifacts.kvk) {
255
+ return null;
256
+ }
257
+ }
258
+ //
259
+ // KeymanWeb compiler
260
+ //
261
+ if (wasm_result.extra.targets & COMPILETARGETS_JS) {
262
+ wasm_options.target = 1; // CKF_KEYMANWEB TODO use COMPILETARGETS_JS
263
+ // We always want debug data in the intermediate .kmx, so that error
264
+ // messages from KMW compiler can give line numbers in .kmn. This
265
+ // should have no impact on the final .js if options.debug is false
266
+ wasm_options.saveDebug = true;
267
+ wasm_result = Module.kmcmp_compile(infile, wasm_options, wasm_interface);
268
+ if (!wasm_result.result) {
269
+ return null;
270
+ }
271
+ const kmw_result = this.copyWasmResult(wasm_result);
272
+ kmw_result.displayMap = result.displayMap; // we can safely re-use the kmx compile displayMap
273
+ const web_kmx = this.copyWasmBuffer(wasm_result.kmx, wasm_result.kmxSize);
274
+ result.artifacts.js = this.runWebCompiler(infile, outfile, web_kmx, result.artifacts.kvk?.data, kmw_result, options);
275
+ if (!result.artifacts.js) {
276
+ return null;
277
+ }
278
+ }
279
+ return result;
280
+ }
281
+ catch (e) {
282
+ /* c8 ignore next 3 */
283
+ this.callbacks.reportMessage(CompilerMessages.Fatal_UnexpectedException({ e: e }));
284
+ return null;
285
+ }
286
+ finally {
287
+ if (wasm_result) {
288
+ wasm_result.delete();
289
+ }
290
+ wasm_interface.delete();
291
+ wasm_options.delete();
292
+ delete globalThis[this.callbackID];
293
+ }
294
+ }
295
+ runWebCompiler(kmnFilename, kmxFilename, web_kmx, kvk, kmxResult, options) {
296
+ const data = WriteCompiledKeyboard(this.callbacks, kmnFilename, web_kmx, kvk, kmxResult, options.saveDebug);
297
+ if (!data) {
298
+ return null;
299
+ }
300
+ return {
301
+ filename: this.callbacks.path.join(this.callbacks.path.dirname(kmxFilename), this.keyboardIdFromKmnFilename(kmnFilename) + ".js" /* KeymanFileTypes.Binary.WebKeyboard */),
302
+ data: new TextEncoder().encode(data)
303
+ };
304
+ }
305
+ keyboardIdFromKmnFilename(kmnFilename) {
306
+ return this.callbacks.path.basename(kmnFilename, ".kmn" /* KeymanFileTypes.Source.KeymanKeyboard */);
307
+ }
308
+ runKvkCompiler(kvksFilename, kmnFilename, kmxFilename, displayMap) {
309
+ // The compiler detected a .kvks file, which needs to be captured
310
+ kvksFilename = this.callbacks.resolveFilename(kmnFilename, kvksFilename);
311
+ const data = this.callbacks.loadFile(kvksFilename);
312
+ if (!data) {
313
+ this.callbacks.reportMessage(CompilerMessages.Error_FileNotFound({ filename: kvksFilename }));
314
+ return null;
315
+ }
316
+ const filename = this.callbacks.path.basename(kvksFilename);
317
+ let basename = null;
318
+ let vk = null;
319
+ if (filename.endsWith('.kvk')) {
320
+ /* Legacy keyboards may reference a binary .kvk. That's not an error */
321
+ // TODO: (lowpri) add hint to convert to .kvks?
322
+ basename = this.callbacks.path.basename(kvksFilename, ".kvk" /* KeymanFileTypes.Binary.VisualKeyboard */);
323
+ const reader = new KvkFileReader();
324
+ try {
325
+ vk = reader.read(data);
326
+ }
327
+ catch (e) {
328
+ this.callbacks.reportMessage(CompilerMessages.Error_InvalidKvkFile({ filename, e }));
329
+ return null;
330
+ }
331
+ }
332
+ else {
333
+ basename = this.callbacks.path.basename(kvksFilename, ".kvks" /* KeymanFileTypes.Source.VisualKeyboard */);
334
+ const reader = new KvksFileReader();
335
+ let kvks = null;
336
+ try {
337
+ kvks = reader.read(data);
338
+ reader.validate(kvks);
339
+ }
340
+ catch (e) {
341
+ this.callbacks.reportMessage(CompilerMessages.Error_InvalidKvksFile({ filename, e }));
342
+ return null;
343
+ }
344
+ let invalidVkeys = [];
345
+ vk = reader.transform(kvks, invalidVkeys);
346
+ for (let invalidVkey of invalidVkeys) {
347
+ this.callbacks.reportMessage(CompilerMessages.Warn_InvalidVkeyInKvksFile({ filename, invalidVkey }));
348
+ }
349
+ }
350
+ // Make sure that we maintain the correspondence between source keyboard and
351
+ // .kvk. Appears to be used currently only by Windows package installer.
352
+ vk.header.associatedKeyboard = this.keyboardIdFromKmnFilename(kmnFilename);
353
+ if (displayMap) {
354
+ // Remap using the osk-char-use-rewriter
355
+ Osk.remapVisualKeyboard(vk, displayMap);
356
+ }
357
+ let writer = new KvkFileWriter();
358
+ return {
359
+ filename: this.callbacks.path.join(this.callbacks.path.dirname(kmxFilename), basename + ".kvk" /* KeymanFileTypes.Binary.VisualKeyboard */),
360
+ data: writer.write(vk)
361
+ };
362
+ }
363
+ loadDisplayMapping(kmnFilename, displayMapFilename) {
364
+ // Remap using the osk-char-use-rewriter
365
+ displayMapFilename = this.callbacks.resolveFilename(kmnFilename, displayMapFilename);
366
+ try {
367
+ // Expected file format: displaymap.schema.json
368
+ const data = this.callbacks.loadFile(displayMapFilename);
369
+ if (!data) {
370
+ this.callbacks.reportMessage(CompilerMessages.Error_FileNotFound({ filename: displayMapFilename }));
371
+ return null;
372
+ }
373
+ const mapping = JSON.parse(new TextDecoder().decode(data));
374
+ return Osk.parseMapping(mapping);
375
+ }
376
+ catch (e) {
377
+ this.callbacks.reportMessage(CompilerMessages.Error_InvalidDisplayMapFile({ filename: displayMapFilename, e }));
378
+ return null;
379
+ }
380
+ }
381
+ /**
382
+ * @internal
383
+ * Generates an exception in kmcmplib to verify that Sentry error capture is
384
+ * working correctly
385
+ */
386
+ testSentry() {
387
+ if (!this.verifyInitialized()) {
388
+ return null;
389
+ }
390
+ return Module.kmcmp_testSentry();
391
+ }
392
+ /**
393
+ * @internal
394
+ * convert `\u{1234}` to `\u1234` etc
395
+ */
396
+ static fixNewPattern(pattern) {
397
+ pattern = pattern.replaceAll(/\\u\{([0-9a-fA-F]{6})\}/g, `\\U00$1`);
398
+ pattern = pattern.replaceAll(/\\u\{([0-9a-fA-F]{5})\}/g, `\\U000$1`);
399
+ pattern = pattern.replaceAll(/\\u\{([0-9a-fA-F]{4})\}/g, `\\u$1`);
400
+ pattern = pattern.replaceAll(/\\u\{([0-9a-fA-F]{3})\}/g, `\\u0$1`);
401
+ pattern = pattern.replaceAll(/\\u\{([0-9a-fA-F]{2})\}/g, `\\u00$1`);
402
+ pattern = pattern.replaceAll(/\\u\{([0-9a-fA-F]{1})\}/g, `\\u000$1`);
403
+ return pattern;
404
+ }
405
+ /**
406
+ * @internal
407
+ * @param pattern - UnicodeSet pattern such as `[a-z]`
408
+ * @param rangeCount - number of ranges to allocate
409
+ * @returns UnicodeSet accessor object, or null on failure
410
+ */
411
+ parseUnicodeSet(pattern, rangeCount) {
412
+ if (!this.verifyInitialized()) {
413
+ /* c8 ignore next 2 */
414
+ // verifyInitialized will set a callback if needed
415
+ return null;
416
+ }
417
+ if ((rangeCount * 2) < 0) {
418
+ throw new RangeError(`Internal error: negative rangeCount * 2 = ${rangeCount * 2}`);
419
+ }
420
+ const buf = this.wasmExports.malloc(rangeCount * 2 * Module.HEAPU32.BYTES_PER_ELEMENT);
421
+ if (buf <= 0) {
422
+ // out of memory will return zero.
423
+ throw new RangeError(`Internal error: wasm malloc() returned ${buf}`);
424
+ }
425
+ // fix \u1234 pattern format
426
+ pattern = KmnCompiler.fixNewPattern(pattern);
427
+ const rc = Module.kmcmp_parseUnicodeSet(pattern, buf, rangeCount * 2);
428
+ if (rc >= 0) {
429
+ // If >= 0: it's a range count (which could be zero, an empty set).
430
+ const ranges = [];
431
+ const startu = (buf / Module.HEAPU32.BYTES_PER_ELEMENT);
432
+ for (let i = 0; i < rc; i++) {
433
+ const start = Module.HEAPU32[startu + (i * 2) + 0];
434
+ const end = Module.HEAPU32[startu + (i * 2) + 1];
435
+ ranges.push([start, end]);
436
+ }
437
+ this.wasmExports.free(buf);
438
+ return new UnicodeSet(pattern, ranges);
439
+ }
440
+ else {
441
+ // rc is negative: it's an error code.
442
+ this.wasmExports.free(buf);
443
+ // translate error code into callback
444
+ this.callbacks.reportMessage(getUnicodeSetError(rc));
445
+ return null;
446
+ }
447
+ }
448
+ /**
449
+ * @internal
450
+ */
451
+ sizeUnicodeSet(pattern) {
452
+ if (!this.verifyInitialized()) {
453
+ /* c8 ignore next 2 */
454
+ return null;
455
+ }
456
+ // fix \u1234 pattern format
457
+ pattern = KmnCompiler.fixNewPattern(pattern);
458
+ // call with rangeCount = 0 to invoke in 'preflight' mode.
459
+ const rc = Module.kmcmp_parseUnicodeSet(pattern, 0, 0);
460
+ if (rc >= 0) {
461
+ return rc;
462
+ }
463
+ else {
464
+ this.callbacks.reportMessage(getUnicodeSetError(rc));
465
+ return -1;
466
+ }
467
+ }
468
+ }
469
+ /**
470
+ * translate UnicodeSet return code into a compiler event
471
+ * @param rc parseUnicodeSet error code
472
+ * @returns the compiler event
473
+ */
474
+ function getUnicodeSetError(rc) {
475
+ // from kmcmplib.h
476
+ const KMCMP_ERROR_SYNTAX_ERR = -1;
477
+ const KMCMP_ERROR_HAS_STRINGS = -2;
478
+ const KMCMP_ERROR_UNSUPPORTED_PROPERTY = -3;
479
+ const KMCMP_FATAL_OUT_OF_RANGE = -4;
480
+ switch (rc) {
481
+ case KMCMP_ERROR_SYNTAX_ERR:
482
+ return CompilerMessages.Error_UnicodeSetSyntaxError();
483
+ case KMCMP_ERROR_HAS_STRINGS:
484
+ return CompilerMessages.Error_UnicodeSetHasStrings();
485
+ case KMCMP_ERROR_UNSUPPORTED_PROPERTY:
486
+ return CompilerMessages.Error_UnicodeSetHasProperties();
487
+ case KMCMP_FATAL_OUT_OF_RANGE:
488
+ return CompilerMessages.Fatal_UnicodeSetOutOfRange();
489
+ default:
490
+ /* c8 ignore next */
491
+ return CompilerMessages.Fatal_UnexpectedException({ e: `Unexpected UnicodeSet error code ${rc}` });
492
+ }
493
+ }
494
+ //# sourceMappingURL=compiler.js.map