diff --git a/developer/src/kmc-convert/src/converter-messages.ts b/developer/src/kmc-convert/src/converter-messages.ts index 8e22f4d49e..bc411300cc 100644 --- a/developer/src/kmc-convert/src/converter-messages.ts +++ b/developer/src/kmc-convert/src/converter-messages.ts @@ -6,7 +6,7 @@ import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def } from '@keymanapp/developer-utils'; const Namespace = CompilerErrorNamespace.Converter; - const SevInfo = CompilerErrorSeverity.Info | Namespace; +const SevInfo = CompilerErrorSeverity.Info | Namespace; // const SevHint = CompilerErrorSeverity.Hint | Namespace; // const SevWarn = CompilerErrorSeverity.Warn | Namespace; const SevError = CompilerErrorSeverity.Error | Namespace; @@ -53,8 +53,11 @@ export class ConverterMessages { ); static INFO_UnsupportedCharactersDetected = SevInfo | 0x0007; - static Info_UnsupportedCharactersDetected = (o: { inputFilename: string, keymap_index: string, key: string,KeyName:string, output: string; }) => m( - this.INFO_UnsupportedCharactersDetected, `INFO: Input file ${def(o.inputFilename)} contains unsupported character '${def(o.output)}' at keyMap index ${def(o.keymap_index)} on Keycode ${def(o.key)} (${def(o.KeyName)})` + static Info_UnsupportedCharactersDetected = (o: { inputFilename: string, keymap_index: string, key: string, KeyName: string, output: string; }) => m( + this.INFO_UnsupportedCharactersDetected, `INFO: Input file ${def(o.inputFilename)} + contains unsupported character '${def(o.output)} + ' at keyMap index ${def(o.keymap_index)} + on Keycode ${def(o.key)} (${def(o.KeyName)})` ); static ERROR_InvalidFile = SevError | 0x0008; diff --git a/developer/src/kmc-convert/src/converter.ts b/developer/src/kmc-convert/src/converter.ts index 2e9911073a..646d0de15f 100644 --- a/developer/src/kmc-convert/src/converter.ts +++ b/developer/src/kmc-convert/src/converter.ts @@ -81,9 +81,9 @@ export class Converter implements KeymanCompiler { } const converter = new ConverterClass(this.callbacks, converterOptions); - const artifacts = await converter.run(inputFilename, outputFilename); + const result = await converter.run(inputFilename, outputFilename); // Note: any subsequent errors in conversion will have been reported by the converter - return artifacts ? { artifacts } : null; + return result ? result : null; } /** diff --git a/developer/src/kmc-convert/src/keylayout-to-kmn/keylayout-to-kmn-converter.ts b/developer/src/kmc-convert/src/keylayout-to-kmn/keylayout-to-kmn-converter.ts index 49be9e039e..8463b1a2a8 100644 --- a/developer/src/kmc-convert/src/keylayout-to-kmn/keylayout-to-kmn-converter.ts +++ b/developer/src/kmc-convert/src/keylayout-to-kmn/keylayout-to-kmn-converter.ts @@ -7,19 +7,34 @@ * */ -import { CompilerCallbacks, CompilerOptions } from "@keymanapp/developer-utils"; -import { ConverterToKmnArtifacts } from "../converter-artifacts.js"; +import { CompilerCallbacks, CompilerOptions, KeymanCompilerResult, } from "@keymanapp/developer-utils"; import { KmnFileWriter } from './kmn-file-writer.js'; import { KeylayoutFileReader } from './keylayout-file-reader.js'; import { ConverterMessages } from '../converter-messages.js'; -import { KeylayoutXMLSourceFile } from '@keymanapp/developer-utils'; +import { ConverterArtifacts } from "../converter-artifacts.js"; +import { ConverterToKmnArtifacts } from "../converter-artifacts.js"; +import { KeylayoutXMLSourceFile } from '../../../common/web/utils/src/types/keylayout/keylayout-xml.js'; + + +export interface ConverterResult extends KeymanCompilerResult { + /** + * Internal in-memory build artifacts from a successful compilation. Caller + * can write these to disk with {@link Converter.write} + */ + artifacts: ConverterArtifacts; +}; + +export interface ConverterToKmnResult extends ConverterResult { + /** + * Internal in-memory build artifacts from a successful compilation. Caller + * can write these to disk with {@link Converter.write} + */ + artifacts: ConverterToKmnArtifacts; +}; + + + -/** - * Object holding all important data for the conversion between - * input (*.keylayout) format and output (*.kmn) format. - * It contains input and output filenames, an array of all used modifiers - * and all preprocessed key rules for up to 3 key/modifier combinations. - */ export interface ProcesData { keylayout_filename: string, kmn_filename: string, @@ -51,12 +66,12 @@ export interface ActionStateOutput { */ export function find_usedKeysCount(data: any, pos: number): number { - let usedKeyCount = KeylayoutToKmnConverter.MAX_KEY_COUNT; - if (data.keyboard.keyMapSet[0].keyMap[pos].key.length < usedKeyCount ) { - // set the max to n-1 (keys are zero indexed ) - usedKeyCount = data.keyboard.keyMapSet[0].keyMap[pos].key.length - 1; + let usedKeyCount = KeylayoutToKmnConverter.MAX_KEY_COUNT; + if (data.keyboard.keyMapSet[0].keyMap[pos].key.length < usedKeyCount) { + // set max to n-1 (keys are zero indexed ) + usedKeyCount = data.keyboard.keyMapSet[0].keyMap[pos].key.length - 1; } - return usedKeyCount ; + return usedKeyCount; } export class KeylayoutToKmnConverter { @@ -83,8 +98,7 @@ export class KeylayoutToKmnConverter { * @param outputFilename the resulting keyman .kmn-file * @return null on success */ - async run(inputFilename: string, outputFilename?: string): Promise { - + async run(inputFilename: string, outputFilename?: string): Promise { if (!inputFilename) { this.callbacks.reportMessage(ConverterMessages.Error_FileNotFound({ inputFilename })); @@ -123,16 +137,21 @@ export class KeylayoutToKmnConverter { const kmnFileWriter = new KmnFileWriter(this.callbacks, this.options); - const out_text_ok: boolean = kmnFileWriter.write(outArray); - if (!out_text_ok) { + // write to object/ConverterToKmnResult + const out_Uint8: Uint8Array = kmnFileWriter.write(outArray); + const Result_toBeReturned: ConverterToKmnResult = { + artifacts: { + kmn: { data: out_Uint8, filename: outputFilename } + } + }; + + if (!out_Uint8) { this.callbacks.reportMessage(ConverterMessages.Error_UnableToWrite({ outputFilename })); return null; } - - return null; + return Result_toBeReturned; } - /** * @brief member function to read filename and behaviour of a json object into a ProcesData * @param jsonObj containing filename, behaviour and rules of a json object @@ -148,10 +167,8 @@ export class KeylayoutToKmnConverter { arrayOf_Modifiers: [], arrayOf_Rules: [] }; -//_S2 do I need to "validate again here?" - if ((jsonObj !== null) && (jsonObj.hasOwnProperty("keyboard"))) { - //if ((jsonObj !== null) ) { + if ((jsonObj !== null) && (jsonObj.hasOwnProperty("keyboard"))) { data_object.keylayout_filename = outputfilename.replace(/\.kmn$/, '.keylayout'); data_object.kmn_filename = outputfilename; data_object.arrayOf_Modifiers = modifierBehavior; // ukelele uses behaviours e.g. 18 modifiersCombinations in 8 KeyMapSelect(behaviors) diff --git a/developer/src/kmc-convert/src/keylayout-to-kmn/kmn-file-writer.ts b/developer/src/kmc-convert/src/keylayout-to-kmn/kmn-file-writer.ts index c00330e5d1..31ea2fdc85 100644 --- a/developer/src/kmc-convert/src/keylayout-to-kmn/kmn-file-writer.ts +++ b/developer/src/kmc-convert/src/keylayout-to-kmn/kmn-file-writer.ts @@ -23,12 +23,12 @@ export class KmnFileWriter { * @param outputfilename the file that will be written; if no outputfilename is given an outputfilename will be created from data_ukelele.keylayout_filename * @return true if data has been written; false if not */ - public write(data_ukelele: ProcesData): boolean { + public writeToFile(data_ukelele: ProcesData): boolean { let data: string = "\n"; // add top part of kmn file: STORES - data += this.writeData_Stores(data_ukelele); + data += this.write_KmnFileHeader(data_ukelele); // add bottom part of kmn file: RULES data += this.writeData_Rules(data_ukelele); @@ -37,33 +37,21 @@ export class KmnFileWriter { this.callbacks.fs.writeFileSync(data_ukelele.kmn_filename, new TextEncoder().encode(data)); return true; } catch (err) { - this.callbacks.reportMessage(ConverterMessages.Error_OutputFilenameIsRequired()); + this.callbacks.reportMessage(ConverterMessages.Error_UnableToWrite({outputFilename: data_ukelele.kmn_filename})); return false; } } - public writeToString(data_ukelele: ProcesData): string { + /** + * @brief member function to write data from object to a Uint8Array + * @param data_ukelele the array holding all keyboard data + * @return a Uint8Array holding data + */ + public write(data_ukelele: ProcesData): Uint8Array { let data: string = "\n"; // add top part of kmn file: STORES - data += this.writeData_Stores(data_ukelele); - - // add bottom part of kmn file: RULES - data += this.writeData_Rules(data_ukelele); - - try { - return data; - } catch (err) { - this.callbacks.reportMessage(ConverterMessages.Error_OutputFilenameIsRequired()); - return null; - } - } - - public writeToUint8Array(data_ukelele: ProcesData): Uint8Array { - let data: string = "\n"; - - // add top part of kmn file: STORES - data += this.writeData_Stores(data_ukelele); + data += this.write_KmnFileHeader(data_ukelele); // add bottom part of kmn file: RULES data += this.writeData_Rules(data_ukelele); @@ -71,17 +59,17 @@ export class KmnFileWriter { try { return new TextEncoder().encode(data); } catch (err) { - this.callbacks.reportMessage(ConverterMessages.Error_OutputFilenameIsRequired()); + this.callbacks.reportMessage(ConverterMessages.Error_UnableToWrite({outputFilename: data_ukelele.kmn_filename})); return null; } } /** - * @brief member function to create data for stores that will be printed to the resulting kmn file + * @brief member function to create data for the header (stores) that will be printed to the resulting kmn file * @param data_ukelele an object containing all data read from a .keylayout file * @return string - all stores to be printed */ - public writeData_Stores(data_ukelele: ProcesData): string { + public write_KmnFileHeader(data_ukelele: ProcesData): string { let data: string = ""; @@ -114,6 +102,9 @@ export class KmnFileWriter { let data: string = ""; // filter array of all rules and remove duplicates + // during the process of creating Rule[], duplicate rules might occur + // (e.g. when in a keylayout file the same modifiers occur in several behaviors thus producing the same rules). + // This is to filter out those duplicate Rule objects const unique_data_Rules: Rule[] = data_ukelele.arrayOf_Rules.filter((curr) => { return (!(curr.output === new TextEncoder().encode("") || curr.output === undefined) && (curr.key !== "") @@ -416,7 +407,8 @@ export class KmnFileWriter { /** * @brief member function to review rules for acceptable modifiers, duplicate or ambiguous rules and return an array containing possible warnings. - * Definition of comparisons e.g. 1-1, 2-4, 6-6 + * Keyman can not handle duplicate rules so we need to make sure a rule is written only once by either omitting a duplicate rule or commenting out an ambiguous rule. + * Omitting rules and definition of comparisons e.g. 1-1, 2-4, 6-6 * see https://docs.google.com/document/d/12J3NGO6RxIthCpZDTR8FYSRjiMgXJDLwPY2z9xqKzJ0/edit?tab=t.0#heading=h.pcz8rjyrl5ug * @param rule : Rule[] - an array of all rules * @param index the index of a rule in array[rule] diff --git a/developer/src/kmc-convert/test/kmn-file-writer.tests.ts b/developer/src/kmc-convert/test/kmn-file-writer.tests.ts index a7ea37b70f..806558cb57 100644 --- a/developer/src/kmc-convert/test/kmn-file-writer.tests.ts +++ b/developer/src/kmc-convert/test/kmn-file-writer.tests.ts @@ -22,6 +22,7 @@ describe('KmnFileWriter', function () { compilerTestCallbacks.clear(); }); + describe("write() ", function () { const inputFilename = makePathToFixture('../data/Test.keylayout'); const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); @@ -30,77 +31,12 @@ describe('KmnFileWriter', function () { const read = sut_r.read(inputFilename); const converted = sut.convert_bound.convert(read, inputFilename.replace(/\.keylayout$/, '.kmn')); - // empty ProcesData from unavailable file name - const inputFilename_unavailable = makePathToFixture('../data/X.keylayout'); - const read_unavailable = sut_r.read(inputFilename_unavailable); - const converted_unavailable = sut.convert_bound.convert(read_unavailable, inputFilename_unavailable.replace(/\.keylayout$/, '.kmn')); - - it('write() should return true (no error) if written', async function () { - const result = sut_w.write(converted); - assert.isTrue(result); - }); - - it('write() should return false if no inputfile', async function () { - const result = sut_w.write(converted_unavailable); - assert.isFalse(result); - }); - - }); - - describe("writeToString() ", function () { - const inputFilename = makePathToFixture('../data/Test.keylayout'); - const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); - const sut_r = new KeylayoutFileReader(compilerTestCallbacks); - const sut_w = new KmnFileWriter(compilerTestCallbacks, compilerTestOptions); - const read = sut_r.read(inputFilename); - const converted = sut.convert_bound.convert(read, inputFilename.replace(/\.keylayout$/, '.kmn')); - - const out_expected_first: string = "c ..................................................................................................................\n" - + "c ..................................................................................................................\n" - + "c Keyman keyboard generated by kmn-convert version: " + KEYMAN_VERSION.VERSION + "\n" - + "c from Ukelele file: "; - - const out_expected_last: string = "\n" - + "c ..................................................................................................................\n" - + "c ..................................................................................................................\n" - + "\n" - + "store(&TARGETS) 'desktop'\n" - + "\n" - + "begin Unicode > use(main)\n\n" - + "group(main) using keys\n\n" - + "\n"; - - // empty ProcesData from unavailable file name - const inputFilename_unavailable = makePathToFixture('../data/X.keylayout'); - const read_unavailable = sut_r.read(inputFilename_unavailable); - const converted_unavailable = sut.convert_bound.convert(read_unavailable, inputFilename_unavailable.replace(/\.keylayout$/, '.kmn')); - - it('writeToString() should return result', async function () { - const result = sut_w.writeToString(converted); - assert.isNotNull(result); - }); - - it('writeToString() should return header in case of missing inputfile', async function () { - const result = sut_w.writeToString(converted_unavailable); - assert.equal(result, ("\n" + out_expected_first + out_expected_last)); - }); - }); - - describe("writeToUint8Array() ", function () { - const inputFilename = makePathToFixture('../data/Test.keylayout'); - const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); - const sut_r = new KeylayoutFileReader(compilerTestCallbacks); - const sut_w = new KmnFileWriter(compilerTestCallbacks, compilerTestOptions); - const read = sut_r.read(inputFilename); - const converted = sut.convert_bound.convert(read, inputFilename.replace(/\.keylayout$/, '.kmn')); - - const out_expected_first: string = + const out_expected: string = "c ..................................................................................................................\n" + "c ..................................................................................................................\n" + "c Keyman keyboard generated by kmn-convert version: " + KEYMAN_VERSION.VERSION + "\n" - + "c from Ukelele file: "; - - const out_expected_last: string = "\n" + + "c from Ukelele file: " + + "\n" + "c ..................................................................................................................\n" + "c ..................................................................................................................\n" + "\n" @@ -115,13 +51,13 @@ describe('KmnFileWriter', function () { const inputFilename_unavailable = makePathToFixture('../data/X.keylayout'); const read_unavailable = sut_r.read(inputFilename_unavailable); const converted_unavailable = sut.convert_bound.convert(read_unavailable, inputFilename_unavailable.replace(/\.keylayout$/, '.kmn')); - it('writeToUint8Array() should return header in case of missing inputfile', async function () { - const result = sut_w.writeToUint8Array(converted_unavailable); - assert.equal(new TextDecoder().decode(result), ("\n" + out_expected_first + out_expected_last)); + it('write() should return header in case of missing inputfile', async function () { + const result = sut_w.write(converted_unavailable); + assert.equal(new TextDecoder().decode(result), ("\n" + out_expected)); }); - it('writeToUint8Array() should return result', async function () { - const result = sut_w.writeToUint8Array(converted); + it('write() should return result', async function () { + const result = sut_w.write(converted); assert.isNotNull(result); }); }); @@ -151,7 +87,7 @@ describe('KmnFileWriter', function () { }); - describe("writeData_Stores() ", function () { + describe("write_KmnFileHeader() ", function () { const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); const sut_r = new KeylayoutFileReader(compilerTestCallbacks); const sut_w = new KmnFileWriter(compilerTestCallbacks, compilerTestOptions); @@ -186,18 +122,18 @@ describe('KmnFileWriter', function () { + "group(main) using keys\n\n" + "\n"; - it(('writeData_Stores should return store text with filename ').padEnd(62, " ") + 'on correct input', async function () { - const written_correctName = sut_w.writeData_Stores(converted); + it(('write_KmnFileHeader should return store text with filename ').padEnd(62, " ") + 'on correct input', async function () { + const written_correctName = sut_w.write_KmnFileHeader(converted); assert.equal(written_correctName, (out_expected_first + converted.keylayout_filename + out_expected_last)); }); - it(('writeData_Stores should return store text without filename ').padEnd(62, " ") + 'on empty input', async function () { - const written_emptyName = sut_w.writeData_Stores(converted_empty); + it(('write_KmnFileHeader should return store text without filename ').padEnd(62, " ") + 'on empty input', async function () { + const written_emptyName = sut_w.write_KmnFileHeader(converted_empty); assert.equal(written_emptyName, (out_expected_first + out_expected_last)); }); - it(('writeData_Stores should return store text without filename ').padEnd(62, " ") + 'on only filename as input', async function () { - const written_onlyName = sut_w.writeData_Stores(converted_unavailable); + it(('write_KmnFileHeader should return store text without filename ').padEnd(62, " ") + 'on only filename as input', async function () { + const written_onlyName = sut_w.write_KmnFileHeader(converted_unavailable); assert.equal(written_onlyName, (out_expected_first + converted_unavailable.keylayout_filename + out_expected_last)); }); }); @@ -502,7 +438,7 @@ describe('KmnFileWriter', function () { }); }); - describe('write form intermediate data array', function () { + describe('write from intermediate data array', function () { const sut_w = new KmnFileWriter(compilerTestCallbacks, compilerTestOptions); [ [