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