diff --git a/common/web/types/build.sh b/common/web/types/build.sh index 08419fa35df..85ff8c1f44c 100755 --- a/common/web/types/build.sh +++ b/common/web/types/build.sh @@ -28,6 +28,7 @@ builder_parse "$@" function compile_schemas() { # We need the schema files at runtime and bundled, so always copy it for all actions except `clean` local schemas=( + "$KEYMAN_ROOT/resources/standards-data/keylayout/keylayout.schema.json" "$KEYMAN_ROOT/resources/standards-data/ldml-keyboards/46/ldml-keyboard3.schema.json" "$KEYMAN_ROOT/resources/standards-data/ldml-keyboards/46/ldml-keyboardtest3.schema.json" "$KEYMAN_ROOT/common/schemas/kvks/kvks.schema.json" diff --git a/common/web/types/package.json b/common/web/types/package.json index 2b2122f6828..dc22aa5605b 100644 --- a/common/web/types/package.json +++ b/common/web/types/package.json @@ -31,8 +31,8 @@ "url": "https://github.com/keymanapp/keyman/issues" }, "dependencies": { - "@keymanapp/ldml-keyboard-constants": "*", "@keymanapp/keyman-version": "*", + "@keymanapp/ldml-keyboard-constants": "*", "restructure": "3.0.1" }, "devDependencies": { diff --git a/common/web/types/src/consts/virtual-key-constants.ts b/common/web/types/src/consts/virtual-key-constants.ts index 7b083241bdc..ca1719f280a 100644 --- a/common/web/types/src/consts/virtual-key-constants.ts +++ b/common/web/types/src/consts/virtual-key-constants.ts @@ -39,7 +39,7 @@ export const USVirtualKeyCodes = { K_7:55, K_8:56, K_9:57, - K_A:65, + K_A:65, /*34.*/ K_B:66, K_C:67, K_D:68, diff --git a/common/web/types/src/schema-validators.ts b/common/web/types/src/schema-validators.ts index ed07d44fde7..37a0f193674 100644 --- a/common/web/types/src/schema-validators.ts +++ b/common/web/types/src/schema-validators.ts @@ -2,6 +2,7 @@ import kpj from './schemas/kpj.schema.validator.mjs'; import kpj90 from './schemas/kpj-9.0.schema.validator.mjs'; import kvks from './schemas/kvks.schema.validator.mjs'; import ldmlKeyboard3 from './schemas/ldml-keyboard3.schema.validator.mjs'; +import keylayout from './schemas/keylayout.schema.validator.mjs'; import ldmlKeyboardTest3 from './schemas/ldml-keyboardtest3.schema.validator.mjs'; import displayMap from './schemas/displaymap.schema.validator.mjs'; import touchLayoutClean from './schemas/keyman-touch-layout.clean.spec.validator.mjs'; @@ -15,6 +16,7 @@ const SchemaValidators = { kpj, kpj90, kvks, + keylayout, ldmlKeyboard3, ldmlKeyboardTest3, displayMap, diff --git a/common/web/types/tools/schema-bundler.js b/common/web/types/tools/schema-bundler.js index c8086e06509..99a51b11818 100644 --- a/common/web/types/tools/schema-bundler.js +++ b/common/web/types/tools/schema-bundler.js @@ -11,6 +11,7 @@ await esbuild.build({ 'obj/schemas/kvks.schema.validator.cjs', 'obj/schemas/ldml-keyboard3.schema.validator.cjs', 'obj/schemas/ldml-keyboardtest3.schema.validator.cjs', + 'obj/schemas/keylayout.schema.validator.cjs', 'obj/schemas/displaymap.schema.validator.cjs', 'obj/schemas/keyman-touch-layout.clean.spec.validator.cjs', 'obj/schemas/keyman-touch-layout.spec.validator.cjs', diff --git a/developer/src/common/web/utils/src/index.ts b/developer/src/common/web/utils/src/index.ts index a4141395844..7cca6889e66 100644 --- a/developer/src/common/web/utils/src/index.ts +++ b/developer/src/common/web/utils/src/index.ts @@ -30,6 +30,8 @@ export * as LDMLKeyboard from './types/ldml-keyboard/ldml-keyboard-xml.js'; export { LDMLKeyboardTestDataXMLSourceFile } from './types/ldml-keyboard/ldml-keyboard-testdata-xml.js'; export { LDMLKeyboardXMLSourceFileReader, LDMLKeyboardXMLSourceFileReaderOptions } from './types/ldml-keyboard/ldml-keyboard-xml-reader.js'; +export * as Keylayout from './types/keylayout/keylayout-xml.js'; + export { CompilerAsyncCallbacks, CompilerCallbacks, diff --git a/developer/src/common/web/utils/src/types/keylayout/keylayout-xml.ts b/developer/src/common/web/utils/src/types/keylayout/keylayout-xml.ts new file mode 100644 index 00000000000..12f64e2a6cc --- /dev/null +++ b/developer/src/common/web/utils/src/types/keylayout/keylayout-xml.ts @@ -0,0 +1,170 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by S. Schmitt on 2025-06-12 + * + * The interfaces in this file are designed with reference to the mapped + * structures produced by xml2js when passed keylayout .xml file. + * The prefix KL is stands for 'Keylayout' + * + * A keylayout file is a configuration file used macOS to define custom + * keyboard mappings, mapping physical key presses to specific characters + * or Unicode symbols. These XML-based files, created with Ukelele, allow + * users to create custom layouts for different languages or special characters, + * such as mapping Option + Key combinations. With kmc-convert we can convert + * these keylayout files to .kmn files which can be used in Keyman keyboards. + */ + +export interface KeylayoutXMLSourceFile { + /* + * -- the root element + */ + keyboard: KL_Keyboard; +}; + +export interface KL_Keyboard { + /* + attributes of the main element + even if they are not optional kmc-convert might not use some attributes(group, id, maxout) + */ + group: string; + id: string; + name: string; + maxout?: string; + /* + * , , , , + * the 5 main elements. + * + * The keyboard element must contain exactly one element, + * one or more elements, + * one or more elements, + * an optional element, + * and an optional element. (Source: TN2056) + */ + layouts: KL_Layouts[]; + modifierMap: KL_ModifierMap[]; + keyMapSet: KL_KeyMapSet[]; + actions?: KL_Actions; + terminators?: KL_Terminators; +}; + +export interface KL_Layouts { + // the sub element of , contains one or more elements + layout: KL_Layout[]; +}; + +export interface KL_Layout { + /* + * attributes of the sub element + * containing information about the use of mapSet and modifiers for certain keys + * e.g. key 4 - key 5 use mapSet 2a4 and modifiers 19c + * ( ) + * referencing + * even if they are not optional kmc-convert might not use some attributes(first, last, mapSet, modifiers) + */ + first: string; + last: string; + mapSet: string; + modifiers: string; +}; + +export interface KL_ModifierMap { + /* + * attributes of the element + * even if they are not optional kmc-convert might not use some attributes(defaultIndex) + */ + id: string; + defaultIndex: string; + /* + * the sub element of + * The element contains one or more elements, each of which correspond to one table + */ + keyMapSelect: KL_KeyMapSelect[]; +}; + +export interface KL_KeyMapSelect { + /* + * attributes of the element + * containing a set of modifier combinations for a behavior + * referencing + */ + mapIndex: string; + // the sub element of + modifier: KL_Modifier[]; +}; + +export interface KL_Modifier { + /* + * attributes of the element + * each containing one combination of modifier keys + */ + keys: string; +}; + +export interface KL_KeyMapSet { + /* + * attributes of the element + * referencing + */ + id: string; + /* the sub element of + * The contains one or more elements, each of which correspond to one table + */ + keyMap: KL_KeyMap[]; +}; + +export interface KL_KeyMap { + /* + * attributes of the element + * referencing + * even if they are not optional kmc-convert might not use some attributes(baseMapSet, baseIndex) + */ + index: string; + baseMapSet?: string; + baseIndex?: string; + // the sub element of + key: KL_Key[]; +}; + +export interface KL_Key { + /* + * attributes of the element + * containing a keycode and its output or action + */ + code: string; + action?: string; //TODO-KMC-CONVERT: Support sub-element 'anonymous actions' in the future + output?: string; +}; + +export interface KL_Actions { + // the sub element of + action: KL_Action[]; +}; + +export interface KL_Action { + /* + * attributes of the element + * defining an action id + */ + id?: string; + // a sub element of + when?: KL_When[]; +}; + +export interface KL_When { + /* + * attributes of the element + * contain either a state-output pair or a state-next pair + * to define which output or next is followed by a state + * even if they are not optional kmc-convert might not use some attributes(through, multiplier) + */ + state?: string; + through?: string; + output?: string; + multiplier?: string; + next?: string; +}; +export interface KL_Terminators { + // a sub element of + when?: KL_When[]; +}; diff --git a/developer/src/common/web/utils/src/xml-utils.ts b/developer/src/common/web/utils/src/xml-utils.ts index 9da6c000710..b00ab4c0aca 100644 --- a/developer/src/common/web/utils/src/xml-utils.ts +++ b/developer/src/common/web/utils/src/xml-utils.ts @@ -15,8 +15,9 @@ const XML_META_DATA_SYMBOL = XMLParser.getMetaDataSymbol(); export const XML_FILENAME_SYMBOL = Symbol("XML Filename"); export type KeymanXMLType = - 'keyboard3' // LDML + 'keyboard3' // LDML | 'keyboardTest3' // LDML + | 'keylayout' // keylayout | 'kps' // | 'kvks' // | 'kpj' // @@ -64,6 +65,17 @@ const PARSER_OPTIONS: KeymanXMLParserOptionsBag = { ignorePiTags: true, preserveOrder: true, // Gives us a 'special' format }, + 'keylayout': { + attributeNamePrefix: '@__', + htmlEntities: true, + ignoreAttributes: false, // use attributes + tagValueProcessor: (_tagName: string, tagValue: string /*, jPath, hasAttributes, isLeafNode*/) => { + // since trimValues: false, we need to zap any element values that would be trimmed. + return tagValue?.trim(); + }, + trimValues: false, // preserve spaces, but see tagValueProcessor + + }, 'kps': { ...PARSER_COMMON_OPTIONS, }, @@ -73,7 +85,7 @@ const PARSER_OPTIONS: KeymanXMLParserOptionsBag = { }, 'kvks': { ...PARSER_COMMON_OPTIONS, - tagValueProcessor: (_tagName: string, tagValue: string, _jPath: string, _hasAttributes: boolean, isLeafNode: boolean) : string | undefined => { + tagValueProcessor: (_tagName: string, tagValue: string, _jPath: string, _hasAttributes: boolean, isLeafNode: boolean): string | undefined => { if (!isLeafNode) { return tagValue?.trim(); // trimmed value } else { @@ -126,20 +138,20 @@ export class KeymanXMLReader { } /** Get metadata on a node if not already set */ - static getMetaData(o: any) : KeymanXMLMetadata { - if(!o) return o; - const metadata : KeymanXMLMetadata = o[XML_META_DATA_SYMBOL as any]; + static getMetaData(o: any): KeymanXMLMetadata { + if (!o) return o; + const metadata: KeymanXMLMetadata = o[XML_META_DATA_SYMBOL as any]; return metadata; } /** Set metadata if not already set */ - public static setMetaData(o: any, metadata: KeymanXMLMetadata) : KeymanXMLMetadata { - let m : KeymanXMLMetadata = KeymanXMLReader.getMetaData(o); + public static setMetaData(o: any, metadata: KeymanXMLMetadata): KeymanXMLMetadata { + let m: KeymanXMLMetadata = KeymanXMLReader.getMetaData(o); if (!m) { m = {}; } // copy non-symbols - m = {...metadata, ...m}; + m = { ...metadata, ...m }; // copy symbols SymbolUtils.copySymbols(m, metadata); o[XML_META_DATA_SYMBOL as any] = m; @@ -157,21 +169,21 @@ export class KeymanXMLReader { } if (Array.isArray(data)) { data.forEach(e => KeymanXMLReader.setDefaultFilename(e, filename)); - } else for(const k of Object.keys(data)) { + } else for (const k of Object.keys(data)) { KeymanXMLReader.setDefaultFilename(data[k], filename); } } } /** move `{ $abc: 4 }` into `{ $: { abc: 4 } }` */ - private static fixupDollarAttributes(data: any) : any { + private static fixupDollarAttributes(data: any): any { if (typeof data === 'object') { if (Array.isArray(data)) { return data.map(v => KeymanXMLReader.fixupDollarAttributes(v)); } // object - const e : any = []; - const attrs : any = []; + const e: any = []; + const attrs: any = []; Object.entries(data).forEach(([k, v]) => { if (k[0] === '$') { k = k.slice(1); @@ -193,7 +205,7 @@ export class KeymanXMLReader { * Requires attribute prefix @__ (double underscore) * For attributes, just remove @__ and continue. * For objects, replace any empty string "" with an empty object {} */ - private static fixupEmptyStringToEmptyObject(data: any) : any { + private static fixupEmptyStringToEmptyObject(data: any): any { if (typeof data === 'object') { // For arrays of objects, we map "" to {} // "" means an empty object @@ -238,7 +250,6 @@ export class KeymanXMLReader { * @param data input data */ private static fixupPreserveOrder(data: any): any { - // we need to extract the root name specially if (!Array.isArray(data)) { throw Error(`Internal Error: XML parser preserveOrder did not yield an array.`); @@ -258,10 +269,10 @@ export class KeymanXMLReader { /** takes an 'object' with a property `:@` containing attrs, and one other property with the object name */ private static fixupPreserveOrderObject(data: any): any { const attrs = data[':@']; - const mainEntry : any = Object.entries(data).filter(([k,v]) => k !== ':@'); + const mainEntry: any = Object.entries(data).filter(([k, v]) => k !== ':@'); const [elementName, subItems] = mainEntry[0]; - const out : any = {}; - if ( attrs ) { + const out: any = {}; + if (attrs) { out['$'] = attrs; } if (!elementName) { @@ -276,7 +287,7 @@ export class KeymanXMLReader { for (const o of out['$$']) { const subElementName = o['#name']; const nonPreservedElements = out[subElementName] = out[subElementName] ?? []; - const oWithoutName = {...o}; + const oWithoutName = { ...o }; delete oWithoutName['#name']; // #name is only there in the preserved-order form. nonPreservedElements.push(oWithoutName); } @@ -315,19 +326,18 @@ const PROLOGUE = { '?xml': { '$version': '1.0', '$encoding': 'utf-8' } }; /** wrapper for XML generation support */ export class KeymanXMLWriter { - - private static fixDataForWrite(data: any) : any { - if(typeof data === 'object') { + private static fixDataForWrite(data: any): any { + if (typeof data === 'object') { if (Array.isArray(data)) { // just fixup each item of the array return data.map(d => KeymanXMLWriter.fixDataForWrite(d)); } // else object - const e : any = []; - Object.entries(data).forEach(([k,v]) => { + const e: any = []; + Object.entries(data).forEach(([k, v]) => { if (k === '$') { /* convert $: { a: 1, b: 2 } to { $a: 1, $b: 2} */ - Object.entries(v).forEach(([k,v]) => { + Object.entries(v).forEach(([k, v]) => { e.push([`\$${k}`, KeymanXMLWriter.fixDataForWrite(v)]); }); } else { @@ -366,12 +376,12 @@ export class KeymanXMLWriter { * @param path ajv split instancePath, such as '/keyboard3/layers/0'.split('/') * @returns undefined if the path was not present, null if path went to something that wasn't an object, otherwise the compileContext object is returned. */ -export function findInstanceObject(source: any, path: string[]) : any { - if(!path || !source || path.length == 0) { +export function findInstanceObject(source: any, path: string[]): any { + if (!path || !source || path.length == 0) { return source; - } else if(path[0] == '') { + } else if (path[0] == '') { return findInstanceObject(source, path.slice(1)); - } else if(Array.isArray(source) || typeof source == 'object') { + } else if (Array.isArray(source) || typeof source == 'object') { const child = source[path[0]]; if (child == undefined) return child; // nothing here if (!child || typeof child == 'string') { @@ -389,7 +399,7 @@ export function findInstanceObject(source: any, path: string[]) : any { * @param c number for the offset setting * @param x if set, this object will be used as the base object instead of {} */ -export function withOffset(c: number, compileContext?: any) : KeymanXMLMetadata { +export function withOffset(c: number, compileContext?: any): KeymanXMLMetadata { // set metadata on an empty object const o = Object.assign({}, compileContext); KeymanXMLReader.setMetaData(o, { diff --git a/developer/src/kmc-convert/package.json b/developer/src/kmc-convert/package.json index 94a0cdf7048..f84f55996c9 100644 --- a/developer/src/kmc-convert/package.json +++ b/developer/src/kmc-convert/package.json @@ -36,12 +36,15 @@ "@keymanapp/resources-gosh": "*", "@types/node": "^20.4.1", "@types/semver": "^7.3.12", + "ajv": "^8.12.0", + "ajv-cli": "^5.0.0", + "ajv-formats": "^2.1.1", "c8": "^7.12.0", "chalk": "^2.4.2", "typescript": "^5.4.5" }, "mocha": { - "spec": "build/test/**/test-*.js", + "spec": "build/test/**/*.tests.js", "require": [ "source-map-support/register" ] @@ -62,4 +65,4 @@ "type": "git", "url": "git+https://github.com/keymanapp/keyman.git" } -} +} \ No newline at end of file diff --git a/developer/src/kmc-convert/src/converter-artifacts.ts b/developer/src/kmc-convert/src/converter-artifacts.ts index 49b454cca15..9752af966c2 100644 --- a/developer/src/kmc-convert/src/converter-artifacts.ts +++ b/developer/src/kmc-convert/src/converter-artifacts.ts @@ -1,5 +1,5 @@ /* - * Keyman is copyright (C) SIL International. MIT License. + * Keyman is copyright (C) SIL Global. MIT License. * * Output artifacts available from kmc-convert */ diff --git a/developer/src/kmc-convert/src/converter-class-factory.ts b/developer/src/kmc-convert/src/converter-class-factory.ts index 2176b3df008..84981eb47b4 100644 --- a/developer/src/kmc-convert/src/converter-class-factory.ts +++ b/developer/src/kmc-convert/src/converter-class-factory.ts @@ -1,12 +1,14 @@ /* - * Keyman is copyright (C) SIL International. MIT License. + * Keyman is copyright (C) SIL Global. MIT License. * * Lists all the available converters and finds matching converter */ -import { KeylayoutToKmnConverter } from './keylayout-to-kmn/keylayout-to-kmn-converter.js'; +import { KeylayoutToKmnConverter } from './kmc-convert-convert/keylayout-to-kmn-converter.js'; +import { XkbToKmnConverter } from './kmc-convert-convert/xkb-to-kmn-converter.js'; const converters = [ KeylayoutToKmnConverter, + XkbToKmnConverter, ]; export class ConverterClassFactory { diff --git a/developer/src/kmc-convert/src/converter-messages.ts b/developer/src/kmc-convert/src/converter-messages.ts index c270464f217..40816d23155 100644 --- a/developer/src/kmc-convert/src/converter-messages.ts +++ b/developer/src/kmc-convert/src/converter-messages.ts @@ -1,12 +1,12 @@ /* - * Keyman is copyright (C) SIL International. MIT License. + * Keyman is copyright (C) SIL Global. MIT License. * * Converter messages */ 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; @@ -16,15 +16,71 @@ const SevError = CompilerErrorSeverity.Error | Namespace; * @internal */ export class ConverterMessages { - static ERROR_OutputFilenameIsRequired = SevError | 0x0001; - static Error_OutputFilenameIsRequired = () => - m(this.ERROR_OutputFilenameIsRequired, `An output filename is required for keyboard conversion.`); - static ERROR_NoConverterFound = SevError | 0x0002; - static Error_NoConverterFound = (o:{inputFilename: string, outputFilename: string}) => - m(this.ERROR_NoConverterFound, `No converter is available that can convert from '${def(o.inputFilename)}' to '${def(o.outputFilename)}'.`); + static ERROR_InputFilenameIsRequired = SevError | 0x0002; + static Error_InputFilenameIsRequired = () => m( + this.ERROR_InputFilenameIsRequired, + `An input filename is required for keyboard conversion.` + ); static ERROR_FileNotFound = SevError | 0x0003; - static Error_FileNotFound = (o:{inputFilename: string}) => - m(this.ERROR_FileNotFound, `Input filename '${def(o.inputFilename)}' does not exist or could not be loaded.`); + static Error_FileNotFound = (o: { inputFilename: string; }) => m( + this.ERROR_FileNotFound, + `Input filename '${def(o.inputFilename)}' does not exist or could not be loaded.` + ); + + static ERROR_InvalidFile = SevError | 0x0004; + static Error_InvalidFile = (o: { errorText: string; }) => m( + this.ERROR_InvalidFile, + `The source file has an invalid structure: ${def(o.errorText)}` + ); + + static ERROR_UnableToReadFile = SevError | 0x0005; + static Error_UnableToReadFile = (o: { inputFilename: string; }) => m( + this.ERROR_UnableToReadFile, + `Input file '${def(o.inputFilename)}' could not be read.` + ); + + static ERROR_UnableToRead = SevError | 0x0006; + static Error_UnableToRead = () => m( + this.ERROR_UnableToRead, + `Input file could not be read.` + ); + + static ERROR_UnableToParse = SevError | 0x000C; + static Error_UnableToParse = () => m( + this.ERROR_UnableToParse, + `Input data could not be parsed.` + ); + + static ERROR_UnsupportedCharactersDetected = SevError | 0x0007; + static Error_UnsupportedCharactersDetected = (o: { inputFilename: string, keymapIndex: string, key: string, KeyName: string, output: string; }) => m( + this.ERROR_UnsupportedCharactersDetected, + `Input file ${def(o.inputFilename)} contains unsupported character '${def(o.output)}' at keyMap index ${def(o.keymapIndex)} on Keycode ${def(o.key)} (${def(o.KeyName)})` + ); + + static ERROR_UndefinedActionDetected = SevError | 0x0008; + static Error_UndefinedActionDetected = (o: { inputFilename: string, action: string, KeyName: string, keymapIndex: string; }) => m( + this.ERROR_UndefinedActionDetected, + `${def(o.inputFilename)}: Action id ${def(o.action)} of key ${def(o.KeyName)} in keymapIndex ${def(o.keymapIndex)} is not defined` + ); + + static ERROR_NoConverterFound = SevError | 0x0009; + static Error_NoConverterFound = (o: { inputFilename: string, outputFilename: string; }) => m( + this.ERROR_NoConverterFound, + `No converter is available that can convert from '${def(o.inputFilename)}' to '${def(o.outputFilename)}'.` + ); + + static ERROR_UnableToConvert = SevError | 0x000A; + static Error_UnableToConvert = (o: { inputFilename: string; }) => m( + this.ERROR_UnableToConvert, + `Input file '${def(o.inputFilename)}' could not be converted.` + ); + + static ERROR_UnableToWrite = SevError | 0x000B; + static Error_UnableToWrite = (o: { outputFilename: string, errorText: string; }) => m( + this.ERROR_UnableToWrite, + `Output file for '${def(o.outputFilename)}' could not be written. ${def(o.errorText)}` + ); + } diff --git a/developer/src/kmc-convert/src/converter-options.ts b/developer/src/kmc-convert/src/converter-options.ts index 9fdaa109f97..552af230957 100644 --- a/developer/src/kmc-convert/src/converter-options.ts +++ b/developer/src/kmc-convert/src/converter-options.ts @@ -1,5 +1,5 @@ /* - * Keyman is copyright (C) SIL International. MIT License. + * Keyman is copyright (C) SIL Global. MIT License. * * Converter options */ diff --git a/developer/src/kmc-convert/src/converter.ts b/developer/src/kmc-convert/src/converter.ts index 0a35bb3bc87..63da75dd81e 100644 --- a/developer/src/kmc-convert/src/converter.ts +++ b/developer/src/kmc-convert/src/converter.ts @@ -1,5 +1,5 @@ /* - * Keyman is copyright (C) SIL International. MIT License. + * Keyman is copyright (C) SIL Global. MIT License. * * Infrastructure for keyboard source file conversion tools */ @@ -28,6 +28,7 @@ export interface ConverterResult extends KeymanCompilerResult { * compiler does not read or write from filesystem or network directly, but * relies on callbacks for all external IO. */ + export class Converter implements KeymanCompiler { private callbacks: CompilerCallbacks; private options: CompilerOptions; @@ -57,33 +58,26 @@ export class Converter implements KeymanCompiler { * @returns Source artifacts on success, null on failure. */ async run(inputFilename: string, outputFilename?: string): Promise { - const converterOptions: CompilerOptions = { ...defaultCompilerOptions, ...this.options, }; - if(!outputFilename) { - this.callbacks.reportMessage(ConverterMessages.Error_OutputFilenameIsRequired()); + if (!inputFilename) { + this.callbacks.reportMessage(ConverterMessages.Error_InputFilenameIsRequired()); return null; } const ConverterClass = ConverterClassFactory.find(inputFilename, outputFilename); - if(!ConverterClass) { - this.callbacks.reportMessage(ConverterMessages.Error_NoConverterFound({inputFilename, outputFilename})); - return null; - } - - const binaryData = this.callbacks.loadFile(inputFilename); - if(!binaryData) { - this.callbacks.reportMessage(ConverterMessages.Error_FileNotFound({inputFilename})); + if (!ConverterClass) { + this.callbacks.reportMessage(ConverterMessages.Error_NoConverterFound({ inputFilename, outputFilename })); return null; } const converter = new ConverterClass(this.callbacks, converterOptions); - const artifacts = await converter.run(inputFilename, outputFilename, binaryData); + 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; } /** @@ -99,8 +93,8 @@ export class Converter implements KeymanCompiler { * @returns true on success */ async write(artifacts: ConverterArtifacts): Promise { - for(const key of Object.keys(artifacts)) { - if(artifacts[key]) { + for (const key of Object.keys(artifacts)) { + if (artifacts[key]) { this.callbacks.fs.writeFileSync(artifacts[key].filename, artifacts[key].data); } } 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 deleted file mode 100644 index 7b0c1d29e08..00000000000 --- a/developer/src/kmc-convert/src/keylayout-to-kmn/keylayout-to-kmn-converter.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Keyman is copyright (C) SIL International. MIT License. - * - * Converts macOS/Ukelele .keylayout files to Keyman .kmn - */ -import { CompilerCallbacks, CompilerOptions } from "@keymanapp/developer-utils"; -import { ConverterToKmnArtifacts } from "../converter-artifacts.js"; - -export class KeylayoutToKmnConverter { - static readonly INPUT_FILE_EXTENSION = '.keylayout'; - static readonly OUTPUT_FILE_EXTENSION = '.kmn'; - - constructor(/*private*/ _callbacks: CompilerCallbacks, /*private*/ _options: CompilerOptions) { - // TODO: if these are needed, uncomment /*private*/ and remove _, and they will then - // be available as class properties - } - - async run(inputFilename: string, outputFilename: string, binaryData: Uint8Array): Promise { - if(!inputFilename || !outputFilename || !binaryData) { - throw new Error('Invalid parameters'); - } - - console.error('TODO: implement KeylayoutToKmnConverter'); - - return null; - } -} \ No newline at end of file diff --git a/developer/src/kmc-convert/src/kmc-convert-convert/keylayout-to-kmn-converter.ts b/developer/src/kmc-convert/src/kmc-convert-convert/keylayout-to-kmn-converter.ts new file mode 100644 index 00000000000..b65cdbadb8a --- /dev/null +++ b/developer/src/kmc-convert/src/kmc-convert-convert/keylayout-to-kmn-converter.ts @@ -0,0 +1,1047 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by S. Schmitt on 2025-05-12 + * + * Convert macOS/Ukelele .keylayout files to Keyman .kmn + * + */ + +// ToDo-kmc-convert all warnings/squiggely lines will be adressed in PR #15860 +import { CompilerCallbacks, CompilerOptions, KeymanCompilerResult, Keylayout } from "@keymanapp/developer-utils"; + +import { KmnFileWriter } from './../kmc-convert-write/kmn-file-writer.js'; +import { KeylayoutFileReader } from './../kmc-convert-read/keylayout-file-reader.js'; +import { ConverterMessages } from '../converter-messages.js'; +import { ConverterArtifacts, ConverterToKmnArtifacts } from "../converter-artifacts.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; +}; + +/** + * Interface for all data read from a .keylayout file. Also contains all rules processed from input data. + * Data will be used for writing to a .kmn file (e.g. filename, modifier combinations, rules) + */ +export interface ProcessedData { + + keylayoutFilename: string; + kmnFilename: string; + modifiers: string[][]; + rules: Rule[]; +}; +/** + * Interface for storing data read from a .keylayout file and used for processing rules. + * These are used for obtaining one entity form the other (e.g. from action id to output, from keycode to modifier, etc.) + */ +export interface KeylayoutFileData { + actionId?: string; + keyCode?: string; + key?: string; + behavior: string; + modifier?: string; + outchar?: string; +}; + +/** + * Interface for storing data read from a .keylayout file and used for processing rules. + * These are used for obtaining the triplet [action id, state, output] + * e.g. ['a9','1','â'] from for action id a9 + */ +export interface ActionStateOutput { + id: string; + state: string; + output: string; +}; + +/** + * @brief class for all storing a rule containing data for key, deadkey, previous deadkey, output) + */ +export class Rule { + constructor( + public readonly ruleType: string, /* C0, C1, C2, C3, or C4 */ + + public readonly modifierPrevDeadkey: string, /* first key used by C3 rules*/ + public readonly prevDeadkey: string, + public idPrevDeadkey: number, + public uniquePrevDeadkey: number, + + public readonly modifierDeadkey: string, /* second key used by C2,C3 rules*/ + public readonly deadkey: string, + public idDeadkey: number, + public uniqueDeadkey: number, + + public readonly modifierKey: string, /* third key used by C0,C1,C2,C3,C4 rules*/ + public readonly key: string, + public readonly output: Uint8Array, /* output used by C0,C1,C2,C3,C4 rules*/ + ) { } + +} + +export class KeylayoutToKmnConverter { + static readonly INPUT_FILE_EXTENSION = '.keylayout'; + static readonly OUTPUT_FILE_EXTENSION = '.kmn'; + static readonly SKIP_COMMENTED_LINES = false; + static readonly MAX_CTRL_CHARACTER = 0x20; // the hightest control character we print out as a Unicode CodePoint (U+0020) + static readonly MAX_KEY_IDENTIFIER = 49; // We only use key Nr 0 (A) -> key Nr 49 (Space) + + private options: CompilerOptions; + + constructor(private callbacks: CompilerCallbacks, options: CompilerOptions) { + this.options = { ...options }; + }; + + /** + * @brief member function to run read/convert/write + * @param inputFilename the ukelele .keylayout-file to be converted + * @param outputFilename the resulting keyman .kmn-file + * @return null on success + */ + async run(inputFilename: string, outputFilename?: string): Promise { + + + if (!inputFilename) { + this.callbacks.reportMessage(ConverterMessages.Error_FileNotFound({ inputFilename })); + return null; + } + + const KeylayoutReader = new KeylayoutFileReader(this.callbacks/*, this.options*/); + + const binaryData = this.callbacks.loadFile(inputFilename); + const jsonO: Keylayout.KeylayoutXMLSourceFile = KeylayoutReader.read(binaryData); + + if (!jsonO) { + this.callbacks.reportMessage(ConverterMessages.Error_UnableToReadFile({ inputFilename: inputFilename })); + return null; + } + try { + if (!KeylayoutReader.validate(jsonO, inputFilename)) { + return null; + } + } catch (e) { + this.callbacks.reportMessage(ConverterMessages.Error_InvalidFile({ errorText: e.toString() })); + return null; + } + + const processedData = await this.convert(jsonO, inputFilename, outputFilename); + const kmnFileWriter = new KmnFileWriter(this.callbacks, this.options); + + // write to object/ConverterToKmnResult + const outputKmn = kmnFileWriter.write(processedData); + const result: ConverterToKmnResult = { + artifacts: { + kmn: { data: outputKmn, filename: processedData.kmnFilename } + } + }; + return result; + } + + /** + * @brief member function to read filename and behaviorof a json object into a ProcessedData + * @param jsonObj containing filename, behaviorand rules of a json object + * @return an ProcessedData containing all data ready to print out + */ + private convert(jsonObj: Keylayout.KeylayoutXMLSourceFile, inputfilename: string, outputFilename?: string): ProcessedData { + // modifiers for each behavior + const modifierBehavior: string[][] = []; + + // an array of data for a kmn rule + const rules: Rule[] = []; + + // dataObject for all relevant data + const dataObject: ProcessedData = { + keylayoutFilename: "", + kmnFilename: "", + modifiers: [], + rules: [] + }; + + if (jsonObj === null) { + return null; + } + // create an array of modifier combinations and store in dataObject + for (let j = 0; j < jsonObj.keyboard.modifierMap[0].keyMapSelect.length; j++) { + const singleModifierSet: string[] = []; + for (let k = 0; k < jsonObj.keyboard.modifierMap[0].keyMapSelect[j].modifier.length; k++) { + singleModifierSet.push(jsonObj.keyboard.modifierMap[0].keyMapSelect[j].modifier[k]['keys']); + } + modifierBehavior.push(singleModifierSet); + } + + // fill dataObject with filenames, behaviors and (initialized) rules + dataObject.keylayoutFilename = inputfilename; + if (!outputFilename) + dataObject.kmnFilename = inputfilename.replace(/\.keylayout$/, '.kmn'); + else + dataObject.kmnFilename = outputFilename; + dataObject.modifiers = modifierBehavior; // ukelele uses behaviors e.g. 18 modifiersCombinations in 8 KeyMapSelect(behaviors) + dataObject.rules = rules; + + // fill rules into 'rules' of dataObject + return this.createRuleData(dataObject, jsonObj); + } + + /** + * @brief member function to read the rules contained in a json object and add array of Rules[] to an ProcessedData + * @param dataUkelele: an object containing the name of the in/output file, an array of behaviors and an (empty) array of Rules + * @param jsonObj: json Object containing all data read from a keylayout file + * @return an object containing the name of the input file, an array of behaviors and a populated array of Rules[] + */ + public createRuleData(dataUkelele: ProcessedData, jsonObj: Keylayout.KeylayoutXMLSourceFile): ProcessedData { + + const rules: Rule[] = []; + let dkCounterC3: number = 0; + let dkCounterC2: number = 0; + let actionId: string; + + // check if we use CAPS in a modifier throughout the .keylayout file. In this case we need to add NCAPS at places not specifying CAPS + const isCapsused = (this.checkIfCapsIsUsed(dataUkelele.modifiers)); + + for (let j = 0; j <= KeylayoutToKmnConverter.MAX_KEY_IDENTIFIER; j++) { + + // loop behaviors (in ukelele it is possible to define multiple modifier combinations that behave in the same way) + for (let i = 0; i < jsonObj.keyboard.keyMapSet[0].keyMap.length; i++) { + + // if index of keys and behaviors exist + const isItAvailable = ((j < jsonObj.keyboard.keyMapSet[0].keyMap[i].key.length) && (i < jsonObj.keyboard.keyMapSet[0].keyMap.length)); + if (!isItAvailable) { + continue; + } + + let ruleObj: Rule; + + if (j < jsonObj.keyboard.keyMapSet[0].keyMap[i].key.length) { + // ............................................................................................................................... + // case C0: output ............................................................................................................... + // C0 see: https://docs.google.com/document/d/12J3NGO6RxIthCpZDTR8FYSRjiMgXJDLwPY2z9xqKzJ0/edit?tab=t.0#heading=h.g7jwx3lx0ydd ... + // a key is mapped to a character directly ( code -> output) ..................................................................... + // ...............e. g. ............................................................................... + // ............................................................................................................................... + + if ((jsonObj.keyboard.keyMapSet[0].keyMap[i].key[j]['output'] !== undefined) + && (jsonObj.keyboard.keyMapSet[0].keyMap[i].key[j]['output'] !== "")) { + + // loop modifiers + for (let l = 0; l < dataUkelele.modifiers[i].length; l++) { + + if (this.mapUkeleleKeycodeToVK(Number(jsonObj.keyboard.keyMapSet[0].keyMap[i].key[j]['code']))) { + + ruleObj = new Rule( + /* ruleType */ "C0", + + /* modifierPrevDeadkey*/ "", + /* prevDeadkey */ "", + /* idPrevDeadkey */ 0, + /* unique A */ 0, + + /* modifierDeadkey */ "", + /* deadkey */ "", + /* dk for C2*/ 0, + /* unique B */ 0, + + /* modifierKey*/ this.createKmnModifier(dataUkelele.modifiers[i][l], isCapsused), + /* key */ this.mapUkeleleKeycodeToVK(Number(jsonObj.keyboard.keyMapSet[0].keyMap[i].key[j]['code'])), + /* output */ new TextEncoder().encode(jsonObj.keyboard.keyMapSet[0].keyMap[i].key[j]['output']), + ); + rules.push(ruleObj); + } + } + + } + else if (jsonObj.keyboard.keyMapSet[0].keyMap[i].key[j]['action'] !== undefined) { + + actionId = jsonObj.keyboard.keyMapSet[0].keyMap[i].key[j]['action'] ?? ""; + // ............................................................................................................................... + // case C1: action + state none + output ......................................................................................... + // C1 see: https://docs.google.com/document/d/12J3NGO6RxIthCpZDTR8FYSRjiMgXJDLwPY2z9xqKzJ0/edit?tab=t.0#heading=h.g7jwx3lx0ydd ... + // a key is mapped to an action and then to an output ............................................................................ + // KeyMap:code -> KeyMap:action->action:actionState(none) -> actionOutput ...................................................... + // ...............e. g. ............................................................................ + // replace state x with all rules that result in 14 ( for action id a18 ...................................................................................................................... + if (jsonObj.keyboard.actions?.action?.[b1ActionIndex]?.when) { + for (const when of jsonObj.keyboard.actions.action[b1ActionIndex].when) { + if ((when['state'] === "none") // find "none" + && (when['next'] !== undefined)) { // find "next" + + // Data of Block Nr 5 ..................................................................................................................................................................... + // of this state(none)-next-pair get value of next (next="1") ............................................................................................................................. + /* eg: 1 */ const b5ValueNext: string = when['next']; + // ........................................................................................................................................................................................ + + + // Data of Block Nr 4 ..................................................................................................................................................................... + // with present actionId (a18) find all keycode-behavior-pairs that use this action (a18) => (keymapIndex 0/keycode 24 and keymapIndex 3/keycode 24) .................................... + // from these create an array of modifier combinations e.g. [['','caps?'], ['Caps']] ..................................................................................................... + /* eg: [['24', 0], ['24', 3]] */ const b4DeadkeyObj: KeylayoutFileData[] = this.getKeyModifierArrayFromActionID(jsonObj, actionId); + /* e.g. [['','caps?'], ['Caps']]*/ const b4DeadkeyModifierObj: string[][] = this.getModifierArrayFromKeyModifierArray(dataUkelele.modifiers, b4DeadkeyObj); + // ........................................................................................................................................................................................ + + + // Data of Block Nr 6 ..................................................................................................................................................................... + // create an array[action id,state,output] from all state-output-pairs that use state = b5ValueNext (e.g. use 1 in ) ...................................... + /* eg: [ 'a9','1','â'] */ const b6ActionIdObj: ActionStateOutput[] = this.getActionStateOutputArrayFromActionState(jsonObj, b5ValueNext); + // ........................................................................................................................................................................................ + + + // Data of Block Nr 1 .................................................................................................................................................................... + // create array[Keycode,Keyname,action id,actionIndex,output] and array[Keyname,action id,behavior,modifier,output] ...................................................................... + /* eg: ['0','K_A','a9','0','â'] */ const b1KeycodeObj: KeylayoutFileData[] = this.getKeyActionOutputArrayFromActionStateOutputArray(jsonObj, b6ActionIdObj); + /* eg: ['K_A','a9','0','NCAPS','â']*/ const b1ModifierKeyObj: KeylayoutFileData[] = this.getKeyBehaviorModOutputArrayFromKeyActionBehaviorOutputArray(jsonObj, b1KeycodeObj, isCapsused); + // ....................................................................................................................................................................................... + + for (let n1 = 0; n1 < b4DeadkeyModifierObj.length; n1++) { + for (let n2 = 0; n2 < b4DeadkeyModifierObj[n1].length; n2++) { + for (let n3 = 0; n3 < b4DeadkeyObj.length; n3++) { + for (let n4 = 0; n4 < b1ModifierKeyObj.length; n4++) { + + ruleObj = new Rule( + /* ruleType */ "C2", + + /* modifierPrevDeadkey*/ "", + /* prevDeadkey */ "", + /* idPrevDeadkey */ 0, + /* unique A */ 0, + + /* modifierDeadkey */ this.createKmnModifier(b4DeadkeyModifierObj[n1][n2], isCapsused), + /* deadkey */ this.mapUkeleleKeycodeToVK(Number(b4DeadkeyObj[n3].key)), + /* dk for C2*/ dkCounterC2++, + /* unique B */ 0, + + /* modifierKey*/ b1ModifierKeyObj[n4].modifier, + /* key */ b1ModifierKeyObj[n4].key, + /* output */ new TextEncoder().encode(b1ModifierKeyObj[n4].outchar), + ); + if ((b1ModifierKeyObj[n4].outchar !== undefined) + && (b1ModifierKeyObj[n4].outchar !== "undefined") + && (b1ModifierKeyObj[n4].outchar !== "")) { + rules.push(ruleObj); + } + } + } + } + } + } + } + } + + // ............................................................................................................................... + // case C3: action + state Nr + Next ............................................................................................. + // ...............e. g. ................................................................................ + // replace state x with all rules that result in 1 ( for action id a16 ............................................................................................................................. + for (let l = 0; l < jsonObj.keyboard.actions.action[b1ActionIndex].when.length; l++) { + if ((jsonObj.keyboard.actions.action[b1ActionIndex].when[l]['state'] !== "none") + && (jsonObj.keyboard.actions.action[b1ActionIndex].when[l]['next'] !== undefined)) { + + // Data of Block Nr 5 ........................................................................................................................................................................ + // of this state-next-pair get value of next (next="1") and state="3" ........................................................................................................................ + /* e.g. state = 3 */ const b5ValueState: string = jsonObj.keyboard.actions.action[b1ActionIndex].when[l]['state']; + /* e.g. next = 1 */ const b5ValueNext: string = jsonObj.keyboard.actions.action[b1ActionIndex].when[l]['next']; + // ........................................................................................................................................................................................... + + // Data of Block Nr 4 ........................................................................................................................................................................ + // with present actionId (a16) find all keycode-behavior-pairs that use this action (a16) => (keymapIndex 3/keycode 32) .................................................................... + // from these create an array of modifier combinations e.g. [ [ 'anyOption', 'Caps' ] ] ..................................................................................................... + /* e.g. [['32', 3]] */ const b4DeadkeyObj: KeylayoutFileData[] = this.getKeyModifierArrayFromActionID(jsonObj, actionId); + /* e.g. [ [ 'anyOption', 'Caps' ] ]*/ const b4DeadkeyModifierObj: string[][] = this.getModifierArrayFromKeyModifierArray(dataUkelele.modifiers, b4DeadkeyObj); + // ........................................................................................................................................................................................... + + // Data of Block Nr 3 ........................................................................................................................................................................ + // get an action id from a state-output-pair that use state = b5ValueState (e.g. use 3 in ) ................................................................. + /* e.g. actioniD = a17 */ const b3ActionId: string = this.getActionIdFromActionNext(jsonObj, b5ValueState); + // ........................................................................................................................................................................................... + + // Data of Block Nr 2 ....................................................................................................................................................................... + // with present actionId (a17) find all key names and behaviors that use this action (a17) => (keymapIndex 3/keycode 28) .................................................................... + // from these create an array of modifier combinations e.g. [ [ 'anyOption', 'Caps' ] ] ..................................................................................................... + /* eg: index=3 */ const b2PrevDeadkeyObj: KeylayoutFileData[] = this.getKeyModifierArrayFromActionID(jsonObj, b3ActionId); + /* e.g. [ [ 'anyOption', 'Caps' ] ] */ const b2PrevDeadkeyModifierObj: string[][] = this.getModifierArrayFromKeyModifierArray(dataUkelele.modifiers, b2PrevDeadkeyObj); + // ........................................................................................................................................................................................... + // Data of Block Nr 6 ........................................................................................................................................................................ + // create an array[action id,state,output] from all state-output-pairs that use state = b5ValueNext (e.g. use 1 in ) ......................................... + /* eg:[ [ 'a9','1','â'] ]*/ const b6ActionIdObj: ActionStateOutput[] = this.getActionStateOutputArrayFromActionState(jsonObj, b5ValueNext); /* eg:[ [ 'a9','1','â'] ]*/ + // ........................................................................................................................................................................................... + + // Data of Block Nr 1 ....................................................................................................................................................................... + // create array[Keycode,Keyname,action id,actionIndex,output] and array[Keyname,action id,behavior,modifier,output] ......................................................................... + /* eg: ['49','K_SPACE','a0','0','Â'] */ const b1KeycodeObj: KeylayoutFileData[] = this.getKeyActionOutputArrayFromActionStateOutputArray(jsonObj, b6ActionIdObj); + /* eg: ['K_SPACE','a0','0','NCAPS','Â'] */ const b1ModifierKeyObj: KeylayoutFileData[] = this.getKeyBehaviorModOutputArrayFromKeyActionBehaviorOutputArray(jsonObj, b1KeycodeObj, isCapsused); + // ........................................................................................................................................................................................... + + for (let n1 = 0; n1 < b2PrevDeadkeyModifierObj.length; n1++) { + for (let n2 = 0; n2 < b2PrevDeadkeyModifierObj[n1].length; n2++) { + for (let n3 = 0; n3 < b2PrevDeadkeyObj.length; n3++) { + for (let n4 = 0; n4 < b4DeadkeyModifierObj.length; n4++) { + for (let n5 = 0; n5 < b4DeadkeyModifierObj[n4].length; n5++) { + for (let n6 = 0; n6 < b4DeadkeyObj.length; n6++) { + for (let n7 = 0; n7 < b1ModifierKeyObj.length; n7++) { + + ruleObj = new Rule( + /* ruleType */ "C3", + /* modifierPrevDeadkey*/ this.createKmnModifier(b2PrevDeadkeyModifierObj[n1][n2], isCapsused), + /* prevDeadkey */ this.mapUkeleleKeycodeToVK(Number(b2PrevDeadkeyObj[n3].key)), + /* idPrevDeadkey */ dkCounterC3++, + /* unique A */ 0, + + /* modifierDeadkey */ this.createKmnModifier(b4DeadkeyModifierObj[n4][n5], isCapsused), + /* deadkey */ this.mapUkeleleKeycodeToVK(Number(b4DeadkeyObj[n6].key)), + /* dk for C2*/ 0, + /* unique B */ 0, + + /* modifierKey*/ b1ModifierKeyObj[n7].modifier, + /* key */ b1ModifierKeyObj[n7].key, + /* output */ new TextEncoder().encode(b1ModifierKeyObj[n7].outchar), + ); + if ((b1ModifierKeyObj[n7].outchar !== undefined) + && (b1ModifierKeyObj[n7].outchar !== "undefined") + && (b1ModifierKeyObj[n7].outchar !== "")) { + rules.push(ruleObj); + } + } + } + } + } + } + } + } + } + } + } + } else { + this.callbacks.reportMessage(ConverterMessages.Error_UnsupportedCharactersDetected({ + inputFilename: jsonObj.keyboard['name'] + ".keylayout", + keymapIndex: jsonObj.keyboard.keyMapSet[0].keyMap[i]['index'], + output: jsonObj.keyboard.keyMapSet[0].keyMap[i].key[j]['output'], + key: jsonObj.keyboard.keyMapSet[0].keyMap[i].key[j]['code'], + KeyName: this.mapUkeleleKeycodeToVK(Number(jsonObj.keyboard.keyMapSet[0].keyMap[i].key[j]['code'])) + })); + return null; + } + } + } + } + dataUkelele.rules = rules; + return this.reviewRuleInputData(dataUkelele); + } + + /** + * @brief member function to review data in array of rules of dataUkelele: remove duplicate rules and mark first occurance of a rule in rules + * @param dataUkelele: an object containing the name of the in/output file, an array of behaviors and an array of Rules + * @return an object containing the name of the input file, an array of behaviors and the revised array of Rule[] + */ + public reviewRuleInputData(dataUkelele: ProcessedData): ProcessedData { + + // check for duplicate C2 and C3 rules in rules (e.g. [NCAPS RALT K_8] > dk(C12) ): create a separate array of unique rules, + // then compare to rules and mark first occurrence of a rule in rules + + let uniqueCountDkB = 0; + const uniqueTextRules: string[][] = []; + + const rules: Rule[] = dataUkelele.rules; + + //------------------------------------ C2: dk ---------------------------------- + // first rule is always unique + rules[0].uniqueDeadkey = uniqueCountDkB; + rules[0].idDeadkey = uniqueCountDkB; + uniqueCountDkB++; + + for (let i = 0; i < rules.length; i++) { + + + if (((rules[i].modifierDeadkey !== undefined) && (rules[i].modifierDeadkey !== "")) + && ((rules[i].deadkey !== undefined) && (rules[i].deadkey !== ""))) { + let IsFirstUsedHereDk: boolean = true; + + // check if not used before + for (let j = 0; j < i; j++) { + if ((rules[i].modifierDeadkey === rules[j].modifierDeadkey) + && (rules[i].deadkey === rules[j].deadkey)) { + IsFirstUsedHereDk = IsFirstUsedHereDk && false; + } + } + + if (IsFirstUsedHereDk) { + rules[i].uniqueDeadkey = uniqueCountDkB; + uniqueTextRules.push([ + rules[i].modifierDeadkey, + rules[i].deadkey, + String(uniqueCountDkB)]); + uniqueCountDkB++; + } + } + } + + //----------------------------------- C3: prev-dk ---------------------------------- + let uniqueCountDkA = 0; + + // first rule is always unique + rules[0].uniquePrevDeadkey = uniqueCountDkA; + uniqueCountDkA++; + + for (let i = 0; i < rules.length; i++) { + if ((rules[i].modifierPrevDeadkey !== "") && (rules[i].prevDeadkey !== "")) { + let isFirstUsedHerePrevDk: boolean = true; + + // check if not used before + for (let j = 0; j < i; j++) { + if ((rules[i].modifierPrevDeadkey === rules[j].modifierPrevDeadkey) + && (rules[i].prevDeadkey === rules[j].prevDeadkey)) { + isFirstUsedHerePrevDk = isFirstUsedHerePrevDk && false; + } + } + + // check if first part of C3 rule contains a rule that is already defined in C2 + if (isFirstUsedHerePrevDk) { + rules[i].uniquePrevDeadkey = uniqueCountDkA; + uniqueCountDkA++; + for (let k = 0; k < uniqueTextRules.length; k++) { + if ((uniqueTextRules[k][0] === rules[i].modifierDeadkey) && (uniqueTextRules[k][1] === rules[i].deadkey)) { + rules[i].uniqueDeadkey = Number(uniqueTextRules[k][2]); + } + } + } + + if (isFirstUsedHerePrevDk) { + rules[i].uniqueDeadkey = uniqueCountDkB; + uniqueTextRules.push([ + rules[i].modifierPrevDeadkey, + rules[i].prevDeadkey, + String(uniqueCountDkB) + ]); + uniqueCountDkB++; + } + } + } + + // loop through rules and mark first occurence each rule of uniqueTextRules + for (let i = 0; i < rules.length; i++) { + for (let j = 0; j < uniqueTextRules.length; j++) { + if ((rules[i].modifierPrevDeadkey === uniqueTextRules[j][0]) && (rules[i].prevDeadkey === uniqueTextRules[j][1])) { + rules[i].idPrevDeadkey = Number(uniqueTextRules[j][2]); + } + if ((rules[i].modifierDeadkey === uniqueTextRules[j][0]) && (rules[i].deadkey === uniqueTextRules[j][1])) { + rules[i].idDeadkey = Number(uniqueTextRules[j][2]); + } + } + } + dataUkelele.rules = rules; + return dataUkelele; + } + + /** + * @brief member function to create a kmn modifier from a keylayout modifier + * @param keylayoutModifier :string - modifier used in a .keylayout file + * @param isCAPSused : boolean flag to indicate if CAPS is used in a keylayout file or not + * @return string - a modifier value suitable for use in a .kmn-file + */ + public createKmnModifier(keylayoutModifier: string, isCAPSused: boolean): string { + + const kmnModifier: string[] = []; + const modifierState = keylayoutModifier.split(" "); + + for (const modifier of modifierState) { + const modifierUppercase = modifier.toUpperCase(); + + if (isCAPSused && (keylayoutModifier).toUpperCase().indexOf("CAPS?") > 0) { + kmnModifier.push("NCAPS"); + } + if (isCAPSused && (keylayoutModifier).toUpperCase().indexOf("CAPS") === -1) { + kmnModifier.push("NCAPS"); + } + + // if we find a modifier containing a '?' e.g. SHIFT? it means the modifier is not necessary. + // If it is not necessary we don't write this modifier + if (modifierUppercase.includes('?') && modifierUppercase !== 'CAPS?') { + kmnModifier.push(""); + } + // if we find 'caps?' => caps is not necessary. + // If caps is not necessary and isCAPSused we need to write out NCAPS. + else if (isCAPSused && modifierUppercase === 'CAPS?') { + kmnModifier.push("NCAPS"); + } + else if (!isCAPSused && modifierUppercase === 'CAPS?') { + kmnModifier.push(""); + } + else if (modifierUppercase === 'CAPS' || modifierUppercase === 'CAPS?') { + kmnModifier.push("CAPS"); + } + else if (isCAPSused && (modifierUppercase === 'NCAPS')) { + kmnModifier.push("NCAPS"); + } + else if (modifierUppercase === 'ANYSHIFT' || modifierUppercase === 'SHIFT') { + kmnModifier.push("SHIFT"); + } + else if (modifierUppercase === "LEFTSHIFT" || modifierUppercase === "LSHIFT") { + kmnModifier.push("SHIFT"); + } + else if (modifierUppercase === "RIGHTSHIFT" || modifierUppercase === "RSHIFT") { + kmnModifier.push("SHIFT"); + } + else if (modifierUppercase === 'ANYCONTROL' || modifierUppercase === 'CONTROL') { + kmnModifier.push("CTRL"); + } + else if (modifierUppercase === "LEFTCONTROL" || modifierUppercase === "LCONTROL") { + kmnModifier.push("LCTRL"); + } + else if (modifierUppercase === "RIGHTCONTROL" || modifierUppercase === "RCONTROL") { + kmnModifier.push("RCTRL"); + } + else if (modifierUppercase === "LEFTOPTION" || modifierUppercase === "LOPTION") { + kmnModifier.push("LALT"); + } + else if (modifierUppercase === "RIGHTOPTION" || modifierUppercase === "ROPTION") { + kmnModifier.push("RALT"); + } + else if (modifierUppercase === 'ANYOPTION' || modifierUppercase === 'OPTION') { + kmnModifier.push("RALT"); + } + // to enable the use of other modifiers (leave upper/lowecase as in .keylayout) + // e.g. 'shift command' -> 'NCAPS SHIFT command'; 'wrongModifierName' -> 'wrongModifierName' + else { + if (!this.isAcceptableKeymanModifier(modifier)) + kmnModifier.push(modifier); + } + } + + // remove duplicate and empty entries and make sure NCAPS is at the beginning + const uniqueModifier: string[] = kmnModifier.filter(function (item, pos, self) { + return ((self.indexOf(item) === pos) && (item !== "")); + }); + + return uniqueModifier.flat().toString().replace(/,/g, " "); + } + + /** + * @brief member function to check if CAPS is used throughout a keylayout file or not + * @param keylayoutModifier the modifier string used in the .keylayout-file + * @return "caps" or undefined if "caps" is not found + */ + public checkIfCapsIsUsed(keylayoutModifier: string[][]): boolean { + if (!keylayoutModifier) + return false; + // make sure we always have a whitespace before and after each modifier( to distinguish from caps? ) + return (" " + keylayoutModifier.flat().join(" ").toUpperCase() + " ").indexOf(" CAPS ") >= 0; + } + + /** + * @brief member function to check if a modifier can be used in Keyman + * @param keylayoutModifier the modifier string used in the .keylayout-file + * @return true if the modifier can be used in keyman; false if not + */ + public isAcceptableKeymanModifier(keylayoutModifier: string): boolean { + if (keylayoutModifier === null) + return false; + const modifierSingle = keylayoutModifier.toUpperCase().split(" "); + for (const mod of modifierSingle) { + if (!mod.match(/^(NCAPS|CAPS|SHIFT|ALT|RALT|LALT|CTRL|LCTRL|RCTRL|)$/)) { + return false; + } + } + return true; + } + + /** + * @brief member function to map Ukelele keycodes to Windows Keycodes + * @param pos Ukelele (=mac) keycodes + * @return VK + */ + public mapUkeleleKeycodeToVK(pos: number): string { + const vk = [ + "K_A" /* A */, + "K_S" /* S */, + "K_D" /* D */, + "K_F" /* F */, + "K_H" /* H */, + "K_G" /* G */, + "K_Z" /* Z */, + "K_X" /* X */, + "K_C" /* C */, + "K_V" /* V */, + "K_BKQUOTE" /* ^ */, + "K_B" /* B */, + "K_Q" /* Q */, + "K_W" /* W */, + "K_E" /* E */, + "K_R" /* R */, + "K_Y" /* Y */, + "K_T" /* T */, + "K_1" /* 1 */, + "K_2" /* 2 */, + "K_3" /* 3 */, + "K_4" /* 4 */, + "K_6" /* 6 */, + "K_5" /* 5 */, + "K_EQUAL" /* ´ */, + "K_9" /* 9 */, + "K_7" /* 7 */, + "K_HYPHEN" /* ß */, + "K_8" /* 8 */, + "K_0" /* 0 */, + "K_RBRKT" /* ] */, + "K_O" /* O */, + "K_U" /* U */, + "K_LBRKT" /* [ */, + "K_I" /* I */, + "K_P" /* P */, + "K_ENTER", + "K_L" /* L */, + "K_J" /* J */, + "K_QUOTE" /* " */, + "K_K" /* K */, + "K_COLON" /* : */, + "K_BKSLASH" /* \ */, // 42 for ISO correct?? + "K_COMMA" /* , */, + "K_SLASH" /* / */, + "K_N" /* N */, + "K_M" /* M */, + "K_PERIOD" /* . */, + "K_oE2" /* \ */, // 48 for ANSI correct?? + "K_SPACE" /* \ */ + ]; + + if (!(pos >= 0 && pos <= 0x31) || (pos === null) || (pos === undefined)) { + return "" as string; + } else { + return vk[pos]; + } + } + + /** + * @brief member function to return an index for a given actionID + * @param data an object containing all data read from a .keylayout file + * @param search :string - value 'id' to be found + * @return a number specifying the index of an actionId + */ + public getActionIndexFromActionId(data: Keylayout.KeylayoutXMLSourceFile, search: string): number { + if (!data.keyboard?.actions?.action) { + return -1; + } + for (let i = 0; i < data.keyboard.actions.action.length; i++) { + if (data.keyboard.actions.action[i]['id'] === search) { + return i; + } + } + return -1; + } + + /** + * @brief member function to find the actionID of a certain state-next pair + * @param data an object containing all data read from a .keylayout file + * @param search :string value 'next' to be found + * @return a string containing the actionId of a certain state(none)-next pair + */ + public getActionIdFromActionNext(data: Keylayout.KeylayoutXMLSourceFile, search: string): string { + if (search !== "none" && data.keyboard?.actions?.action) { + for (const action of data.keyboard.actions.action) { + if (action.when) { + for (const when of action.when) { + if (when['next'] === search) { + return action['id'] as string; + } + } + } + } + } + return "" as string; + } + + /** + * @brief member function to create an array of (modifier) behaviors for a given keycode in [{keycode,modifier}] + * @param data an object containing all data read from a .keylayout file + * @param search : KeylayoutFileData[] - an array[{keycode,modifier}] to be found + * @return a string[] containing modifiers + */ + public getModifierArrayFromKeyModifierArray(data: ProcessedData["modifiers"], search: KeylayoutFileData[]): string[][] | [null] { + const returnString1D: string[][] = []; + for (let i = 0; i < search.length; i++) { + if (search[i].behavior === undefined || search[i].behavior === null) { + return [null]; + } + returnString1D.push(data[Number(search[i].behavior)]); + } + return returnString1D; + } + + /** + * @brief member function to find the output for a certain actionID for state 'none' + * @param data an object containing all data read from a .keylayout file + * @param search :string an actionId to be found + * @return a string containing the output character + */ + public getOutputFromActionIdNone(data: Keylayout.KeylayoutXMLSourceFile, search: string): string { + let OutputValue: string = ""; + + if (!data.keyboard?.actions?.action) { + return OutputValue; + } + + for (const action of data.keyboard.actions.action as Keylayout.KL_Action[]) { + if (action['id'] === search) { + for (const when of action.when as Keylayout.KL_When[]) { + if ((when['state'] === "none") && (when['output'] !== undefined)) { + OutputValue = when['output']; + } + } + } + } + return OutputValue; + } + + /** + * @brief member function to return array of [Keycode,Keyname,actionId,actionIDIndex, output] for a given actionID in of [ actionID,state,output] + * @param data an object containing all data read from a .keylayout file + * @param search :idStateOutputObject[] - array of [{ actionID,state,output }] + * @return a KeylayoutFileData[] containing [{Keycode,Keyname,actionId,actionID, output}] + */ + public getKeyActionOutputArrayFromActionStateOutputArray(data: Keylayout.KeylayoutXMLSourceFile, search: ActionStateOutput[]): KeylayoutFileData[] { + + if ((search === undefined) || (search === null)) + return []; + + const keyActionOutput = []; + + for (let k = 0; k < search.length; k++) { + for (let i = 0; i < data.keyboard.keyMapSet[0].keyMap.length; i++) { + for (let j = 0; j < data.keyboard.keyMapSet[0].keyMap[i].key.length; j++) { + if (data.keyboard.keyMapSet[0].keyMap[i].key[j]['action'] === search[k].id && + Number(data.keyboard.keyMapSet[0].keyMap[i].key[j]['code']) <= KeylayoutToKmnConverter.MAX_KEY_IDENTIFIER) { + const singleDataSet = { + keyCode: data.keyboard.keyMapSet[0].keyMap[i].key[j]['code'], + key: this.mapUkeleleKeycodeToVK(Number(data.keyboard.keyMapSet[0].keyMap[i].key[j]['code'])), + actionId: data.keyboard.keyMapSet[0].keyMap[i].key[j]['action'], + behavior: data.keyboard.keyMapSet[0].keyMap[i]['index'], + outchar: search[k].output + }; + keyActionOutput.push(singleDataSet); + } + } + } + } + return keyActionOutput; + } + + /** + * @brief member function to get an array of all actionId-output pairs for a certain state + * @param data an object containing all data read from a .keylayout file + * @param search : string a 'state' to be found + * @return an array: idStateOutputObject[] containing all [{actionId, state, output}] for a certain state + */ + public getActionStateOutputArrayFromActionState(data: Keylayout.KeylayoutXMLSourceFile, search: string): ActionStateOutput[] { + const actionStateOutput: ActionStateOutput[] = []; + if (search !== "none" && data.keyboard?.actions?.action) { + for (const action of data.keyboard.actions.action) { + if (action.when) { + for (const when of action.when) { + if ((when['state'] === search) && (when['output'] !== undefined)) { + const singleDataSet = { + id: action['id'], + state: when['state'], + output: when['output'] + }; + actionStateOutput.push(singleDataSet as ActionStateOutput); + } + } + + } + } + } + return actionStateOutput; + } + + /** + * @brief member function to create an 2D array of [KeyName,actionId,behavior,modifier,output] + * @param data an object containing all data read from a .keylayout file + * @param search : array of [{keycode,keyname,actionId,behavior,output}] to be found + * @param isCAPSused : boolean flag to indicate if CAPS is used in a keylayout file or not + * @return an array: KeylayoutFileData[] containing [{KeyName,actionId,behavior,modifier,output}] + */ + public getKeyBehaviorModOutputArrayFromKeyActionBehaviorOutputArray(data: Keylayout.KeylayoutXMLSourceFile, search: KeylayoutFileData[], isCAPSused: boolean): KeylayoutFileData[] { + const keyBehaviorModOutput = []; + if (!((search === undefined) || (search === null) || (search.length === 0))) { + for (let i = 0; i < search.length; i++) { + const behaviorIdx: number = Number(search[i].behavior); + for (let j = 0; j < data.keyboard.modifierMap[0].keyMapSelect[behaviorIdx].modifier.length; j++) { + const singleDataSet = { + actionId: search[i].actionId, + key: search[i].key, + behavior: search[i].behavior, + modifier: this.createKmnModifier(data.keyboard.modifierMap[0].keyMapSelect[behaviorIdx].modifier[j]['keys'], isCAPSused), + outchar: search[i].outchar, + }; + keyBehaviorModOutput.push(singleDataSet); + } + } + } + // remove duplicates + const uniquekeyBehaviorModOutput = keyBehaviorModOutput.reduce((unique, o) => { + if (!unique.some(obj => + obj.actionId === o.actionId && + obj.key === o.key && + obj.behavior === o.behavior && + obj.modifier === o.modifier && + obj.outchar === o.outchar + )) { + unique.push(o); + } + return unique; + }, [] as KeylayoutFileData[]); + return uniquekeyBehaviorModOutput; + } + + /** + * @brief member function to create an array of [actionID, output, behavior,keyname,modifier] for a given actionId + * @param data an object containing all data read from a .keylayout file + * @param modi an array of modifiers + * @param search : string - an actionId to be found + * @param outchar : string - the output character + * @param isCAPSused : boolean - flag to indicate if CAPS is used in a keylayout file or not + * @return an array: KeylayoutFileData[] containing [{actionID,output, behavior,keyname,modifier}] + */ + public getActionOutputBehaviorKeyModiFromActionIDStateOutput(data: Keylayout.KeylayoutXMLSourceFile, modi: string[][], search: string, outchar: string, isCapsused: boolean): KeylayoutFileData[] { + const actionOutputBehaviorKeyModi = []; + if ((!modi) || (search === "") || (search === undefined)) { + return []; + } + // loop behaviors (in ukelele it is possible to define multiple modifier combinations that behave in the same way) + for (let i = 0; i < data.keyboard.keyMapSet[0].keyMap.length; i++) { + for (let j = 0; j < data.keyboard.keyMapSet[0].keyMap[i].key.length; j++) { + if (data.keyboard.keyMapSet[0].keyMap[i].key[j]['action'] === search) { + for (let k = 0; k < modi[Number(data.keyboard.keyMapSet[0].keyMap[i]['index'])].length; k++) { + const behaviorIdx: number = Number(data.keyboard.keyMapSet[0].keyMap[i]['index']); + const singleDataSet = { + outchar: outchar, + actionId: data.keyboard.keyMapSet[0].keyMap[i].key[j]['action'], + behavior: data.keyboard.keyMapSet[0].keyMap[i]['index'], + key: this.mapUkeleleKeycodeToVK(Number(data.keyboard.keyMapSet[0].keyMap[i].key[j]['code'])), + modifier: this.createKmnModifier(modi[behaviorIdx][k], isCapsused), + }; + actionOutputBehaviorKeyModi.push(singleDataSet); + } + } + } + } + + //............................................................................. + + // remove duplicates + const uniqueactionOutputBehaviorKey = actionOutputBehaviorKeyModi.reduce((unique, o) => { + if (!unique.some(obj => + obj.outchar === o.outchar && + obj.actionId === o.actionId && + obj.behavior === o.behavior && + obj.key === o.key && + obj.modifier === o.modifier + )) { + unique.push(o); + } + return unique; + }, [] as KeylayoutFileData[]); + + return uniqueactionOutputBehaviorKey; + } + + /** + * @brief member function to create an array of [{keycode,behavior}] for a given actionId + * @param data an object containing all data read from a .keylayout file + * @param search : string - an actionId to be found + * @return an array: KeylayoutFileData[] containing [{keycode,behavior}] + */ + public getKeyModifierArrayFromActionID(data: Keylayout.KeylayoutXMLSourceFile, search: string): KeylayoutFileData[] { + const mapIndexObject1D: KeylayoutFileData[] = []; + for (let i = 0; i < data.keyboard.keyMapSet[0].keyMap.length; i++) { + for (let j = 0; j < data.keyboard.keyMapSet[0].keyMap[i].key.length; j++) { + if (data.keyboard.keyMapSet[0].keyMap[i].key[j]['action'] === search) { + const singleDataSet = { + key: data.keyboard.keyMapSet[0].keyMap[i].key[j]['code'], + behavior: String(i), + }; + mapIndexObject1D.push(singleDataSet); + } + } + } + return mapIndexObject1D; + } + + /** @internal */ + public convertBound = { + convert: this.convert.bind(this), + }; +} + diff --git a/developer/src/kmc-convert/src/kmc-convert-convert/xkb-to-kmn-converter.ts b/developer/src/kmc-convert/src/kmc-convert-convert/xkb-to-kmn-converter.ts new file mode 100644 index 00000000000..3b74af7868f --- /dev/null +++ b/developer/src/kmc-convert/src/kmc-convert-convert/xkb-to-kmn-converter.ts @@ -0,0 +1,984 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by S. Schmitt on 2025-05-12 + * + * Convert macOS/Ukelele .keylayout files to Keyman .kmn + * + */ + + + +/* + ToDo kmc-convert (list): + - check for correct function comments + - function names + - any + - return types + - squiggles + - remove comments + - remove unused functions + - add warning/errormessages + - add .filter(Boolean)[0]; to split() + + +========================================================= +key {type[Group1]="FOUR_LEVEL_PLUS_LOCK", +symbols[Group1]= [ssharp, question, backslash, questiondown, 0x1001E9E ]}; +-----> "FOUR_LEVEL_PLUS_LOCK" + ====================================================== + use split(/type\[.*\]=(.*?),symbols/); +========================================================= + + +========================================================= +key {type[Group1]="FOUR_LEVEL_PLUS_LOCK", +symbols[Group1]= [ssharp, question, backslash, questiondown, 0x1001E9E ]}; +-----> "ssharp, question, backslash, questiondown, 0x1001E9E " + ====================================================== +use split(/symbols\[.*\]\s*=(.*?)\[/) +========================================================= + + +========================================================= +key.type[Group1] = "EIGHT_LEVEL"; +-----> "EIGHT_LEVEL" + ====================================================== +use split(/key.type\[.*\]=(.*?)/); +========================================================= + + +========================================================= + key { [ +-----> "" + ====================================================== +use split(/key.type\[.*\]=(.*?)/); + split(/key\s*()\s*\{\s*\[/); + split(/key()\{\[/); +========================================================= + + + +*/ + +import { CompilerCallbacks, CompilerOptions, KeymanCompilerResult } from "@keymanapp/developer-utils"; +import { KmnFileWriter } from './../kmc-convert-write/kmn-file-writer.js'; +import { XkbFileReader } from './../kmc-convert-read/xkb-file-reader.js'; +import { ConverterMessages } from '../converter-messages.js'; +import { ConverterArtifacts, ConverterToKmnArtifacts } from "../converter-artifacts.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; +}; + +export interface ProcessedData { + /** + * Interface for all data read from a .keylayout file. Also contains all rules processed from input data. + * Data will be used for writing to a .kmn file (e.g. filename, modifier combinations, rules) + */ + keylayoutFilename: string; + kmnFilename: string; + modifiers: string[][]; + rules: Rule[]; +}; +export interface Data_xkb { + /** + * Interface for all data read from a .keylayout file. Also contains all rules processed from input data. + * Data will be used for writing to a .kmn file (e.g. filename, modifier combinations, rules) + */ + xkb_keyname: string; + xkb_keytype: string; + xkb_modifiers: string[]; + xkb_output: string[]; +}; + +/** + * @brief class for all storing a rule containing data for key, deadkey, previous deadkey, output) + */ +export class Rule { + constructor( + public readonly ruleType: string, /* C0, C1, C2, C3, or C4 */ + + public readonly modifierPrevDeadkey: string, /* first key used by C3 rules*/ + public readonly prevDeadkey: string, + public idPrevDeadkey: number, + public uniquePrevDeadkey: number, + + public readonly modifierDeadkey: string, /* second key used by C2,C3 rules*/ + public readonly deadkey: string, + public idDeadkey: number, + public uniqueDeadkey: number, + + public readonly modifierKey: string, /* third key used by C0,C1,C2,C3,C4 rules*/ + public readonly key: string, + public readonly output: Uint8Array, /* output used by C0,C1,C2,C3,C4 rules*/ + ) { } + +} + +export class XkbToKmnConverter { + static readonly INPUT_FILE_EXTENSION = '.xkb'; + static readonly OUTPUT_FILE_EXTENSION = '.kmn'; + static readonly SKIP_COMMENTED_LINES = false; + static readonly MAX_CTRL_CHARACTER = 0x20; // the hightest control character we print out as a Unicode CodePoint (U+0020) + static readonly MAX_KEY_IDENTIFIER = 49; // We only use key Nr 0 (A) -> key Nr 49 (Space) + + private options: CompilerOptions; + + constructor(private callbacks: CompilerCallbacks, options: CompilerOptions) { + this.options = { ...options }; + }; + + /** + * @brief ToDo check description + * @brief member function to run read/convert/write + * @param inputFilename the ukelele .keylayout-file to be converted + * @param outputFilename the resulting keyman .kmn-file + * @return null on success + */ + async run(inputFilename_withVariant: string, outputFilename?: string): Promise { + + const variant_name = inputFilename_withVariant.substring(inputFilename_withVariant.indexOf('(') + 1, inputFilename_withVariant.indexOf(')')); + + let inputFilename = inputFilename_withVariant; + if (variant_name) + inputFilename = inputFilename_withVariant.substring(0, inputFilename_withVariant.indexOf('(')); + + + if (!inputFilename) { + this.callbacks.reportMessage(ConverterMessages.Error_FileNotFound({ inputFilename })); + return null; + } + + const XkbReader = new XkbFileReader(this.callbacks/*, this.options*/); + + const binaryData = this.callbacks.loadFile(inputFilename); + const xkb_data = XkbReader.read(binaryData); + + if (!xkb_data) { + this.callbacks.reportMessage(ConverterMessages.Error_UnableToReadFile({ inputFilename: inputFilename })); + return null; + } + + const processedData = await this.convert(xkb_data, inputFilename_withVariant, outputFilename); + const kmnFileWriter = new KmnFileWriter(this.callbacks, this.options); + + // ToDo-kmc-convert remove writeToFile + kmnFileWriter.writeToFile(processedData as ProcessedData); + + // write to object/ConverterToKmnResult + const outputKmn = kmnFileWriter.write(processedData as ProcessedData); + const result: ConverterToKmnResult = { + artifacts: { + kmn: { data: outputKmn, filename: processedData.kmnFilename } + } + }; + return result; + } + + + private convert(xkb_data: string, inputfilename: string, outputFilename?: string): ProcessedData | null { + + if (!xkb_data) { + return null; + } + + // dataObject for all relevant data + const dataObject: ProcessedData = { + keylayoutFilename: '', + kmnFilename: '', + modifiers: [], + rules: [] + }; + + const start_brack = inputfilename.indexOf('('); + const end_brack = inputfilename.indexOf(')'); + const variant_name = inputfilename.substring(start_brack + 1, end_brack); + const inputfilenameUntilBracket = inputfilename.substring(0, start_brack); + + dataObject.keylayoutFilename = inputfilename; + if (start_brack >= 0) { + dataObject.keylayoutFilename = inputfilenameUntilBracket; + } + + // fill dataObject with filenames, behaviors and (initialized) rules + if (!outputFilename) { + if (variant_name) + dataObject.kmnFilename = dataObject.keylayoutFilename + "_" + variant_name + '.kmn'; + else + dataObject.kmnFilename = dataObject.keylayoutFilename + '.kmn'; + } + else + dataObject.kmnFilename = outputFilename; + + // fill rules into 'rules' of dataObject + const out = this.createRuleData(dataObject, xkb_data); + return out; + } + + /** + // ToDo-kmc-convert edit this function + // in the Configuration file we find the appopriate paragraph between "xkb_symbol " and the next xkb_symbol + // ++++ load xkb and return paragraph between xkb_symbols block x and xkb_symbols y + * @brief ToDo check description + * @brief member function to find the appopriate paragraph between "xkb_symbol " and the next xkb_symbol + * @param xkbData: an string containing the contents of the xkb file + * @return the paragraph of the xkb ffr the specified data + */ + public findParagraph(xkbData_in: string, dataObject: ProcessedData): string { + + // Todo-kmc-convert: recursive look through other definition files if they are included (e.g. include "de(basic)") + // Todo-kmc-convert: create paragraphname from inputfilename + // ToDo-kmc-convert remove this-it`s neccessary as we could not process the include (include "de(basic)") + const xkbData = xkbData_in.replace(/include \"/g, '\/\/include \"'); + const start_vari = dataObject.kmnFilename.lastIndexOf('_'); + const end_vari = dataObject.kmnFilename.indexOf('.'); + let variant_name = ''; + if ((start_vari !== -1) && (end_vari !== -1)) + variant_name = dataObject.kmnFilename.substring(start_vari + 1, end_vari); + + + let paragraph_name = ''; + // Todo find default keyword (default xkb_symbols "basic") and use this + if (variant_name) + paragraph_name = variant_name; + else { + paragraph_name = xkbData_in.split(/default xkb_symbols\s*\"(.*?)\"\s\{/)[1]; + } + + // remove all comments, remove whitespace, split into block for each xkb_symbol definition + const noComments = xkbData.replace(/\/\*[\s\S]*?\*\/|(?<=[^:])\/\/.*|^\/\/.*/g, ''); + const noCommentsNoWhitespace = noComments.replaceAll(/[\s,]+/g, ',').slice(0, -1); + const xkb_symbols_blocks = noCommentsNoWhitespace.split("xkb_symbols"); + + // find paragraphname in xkb_symbols, clean data and return appropriate paragraph + for (let i = 0; i < xkb_symbols_blocks.length; i++) { + if (xkb_symbols_blocks[i].indexOf(paragraph_name) >= 0) { + // text after 'xkb_symbols "xxxx" {' until '};,};' + // ToDo-kmc-convert find better solution + const start_paragraph = xkb_symbols_blocks[i].indexOf('{'); + const end_paragraph = xkb_symbols_blocks[i].indexOf('};,};'); + const paragraph = xkb_symbols_blocks[i].substring(start_paragraph + 2, end_paragraph); + return paragraph.replaceAll(/[{[,}\]]+/g, ','); + } + } + return ''; + } + + public getModifier(typename: string): string[] | null { + + // ToDo-kmc-convert read levels from file + // ToDo-kmc-convert use case + + if (typename === "ONE_LEVEL") { + return [""]; + } + + if (typename === "FOUR_LEVEL_PLUS_LOCK") { + return ["", "SHIFT", "RALT", "SHIFT RALT", "LOCK"]; + } + + if (typename === "EIGHT_LEVEL") { + return ["", "SHIFT", "ALT", "SHIFT ALT", "X", "X SHIFT", "X ALT", "X SHIFT ALT"]; + } + + if (typename === "DEFAULT_LEVEL") { + return ["", "SHIFT", "ALT", "SHIFT ALT"]; + } + return null; + } + + public get_Keyname(line: string): string { + if (line.indexOf('= 0) { + return line.split((/(\)/)).filter(Boolean)[0]; + } + return ''; + } + + public getOutput(line: string): string { + let out: string = ''; + + // e.g. if key {type[Group1]="FOUR_LEVEL_PLUS_LOCK", symbols[Group1]=[ssharp, question, backslash, questiondown, 0x1001E9E ]}; + if (line.indexOf('symbols') >= 0) { + out = line.split(/symbols.*(.*?)=,/).filter(Boolean)[1]; + } + + // e.g. if key.type[Group1] = "ONE_LEVEL"; + else if (line.indexOf('key.type') >= 0) { + out = ''; + } + + // nornal + else if (line.indexOf('= 0) { + const out1arr = line.split(/,/); + out = out1arr[1]; + } + + return out; + } + + + + public getKeytype(data_key: string, default_type: string): string { + + // kmc-konvert-better solution + const pos_type = data_key.indexOf('type'); + if (pos_type >= 0) { + const first_quote = data_key.indexOf('"'); + const longstring = data_key.substring(first_quote + 1); + const last_qoute = longstring.indexOf('"'); + const shortstring = longstring.substring(0, last_qoute); + return shortstring; + } + return default_type; + } + + public filldata_paragraph(xkbData: string, dataObject: ProcessedData, data_paragraph_arr: Data_xkb[]): Data_xkb[] { + + // in the Configuration file we find the appopriate paragraph between "xkb_symbol " and the next xkb_symbol + // then copy all rows starting with "key <" to a 1D-Vector + + // find appropriate parapraph + const paragraph = this.findParagraph(xkbData, dataObject); + if (!paragraph) + console.log('ToDo-kmc-convert add Err-msg findParagraph'); + + const lineArray = []; + + // each rule and type in a seperate line + const lines = paragraph.split('key'); + + for (let i = 0; i < lines.length; i++) { + lineArray.push(lines[i].replaceAll(/[\s{[,;}\]]+/g, ',').slice(1, -1)); + } + + let default_key_type: string = 'DEFAULT_LEVEL'; + + for (let i = 0; i < lineArray.length; i++) { + + // get Data for key_type e.g. "FOUR_LEVEL" + const key_type = this.getKeytype(lineArray[i], default_key_type); + default_key_type = key_type; + + // get Data modifier Data of keytype e.g. ["","shift","Caps"] + const modi_Data = this.getModifier(key_type); + + // get name of key e.g. '' + const keyname = this.get_Keyname(lineArray[i]); + + // get output e.g. '1,exclam,onesuperior,exclamdown' + const out = this.getOutput(lineArray[i]); + const output_elements = out.split(','); + + const singleData_paragraph: Data_xkb = { + xkb_keyname: keyname as string, + xkb_keytype: default_key_type, + xkb_modifiers: modi_Data as string[], + xkb_output: output_elements, + }; + + if (keyname) { + data_paragraph_arr.push(singleData_paragraph); + } + } + return data_paragraph_arr; + } + + + /** + * @brief ToDo check description + * @brief* @brief ToDo check description + * @brief member function to read the rules contained in a json object and add array of Rules[] to an ProcessedData + * @param dataUkelele: an object containing the name of the in/output file, an array of behaviors and an (empty) array of Rules + * @param jsonObj: json Object containing all da + * ta read from a keylayout file + * @return an object containing the name of the input file, an array of behaviors and a populated array of Rules[] + */ + public createRuleData(dataObject: ProcessedData, xkb_data: string): ProcessedData { + + // create an array of modifier combinations and store in dataObject + // ToDo-kmc-convert edit function (hard coded at present) + + + + //....................... extract complete lines to array ....................... + //....................... e.g. ',1,exclam,onesuperior,exclamdown', ....... + //............................................................................... + + const data_paragraph_arr: Data_xkb[] = []; + + // Todo-kmc-convert check data_paragraph_arr vs dataObject + this.filldata_paragraph(xkb_data, dataObject, data_paragraph_arr); + + // Todo-kmc-convert check nr(modi)= nr(output) + + //....................... create a Rule and save to ruleObj ....................... + //................................................................................. + //................................................................................. + const rules: Rule[] = []; + + for (let i = 0; i < data_paragraph_arr.length; i++) { + + for (let j = 0; j < data_paragraph_arr[i].xkb_modifiers.length; j++) { + + const ruleObj = new Rule( + /* ruleType */ "C0", + + /* modifierPrevDeadkey*/ "", + /* prevDeadkey */ "", + /* idPrevDeadkey */ 0, + /* unique A */ 0, + + /* modifierDeadkey */ "", + /* deadkey */ "", + /* dk for C2*/ 0, + /* unique B */ 0, + /* modifierKey*/ data_paragraph_arr[i].xkb_modifiers[j], + /* key */ this.get_KMVirtKC_from_keyname(data_paragraph_arr[i].xkb_keyname), + /* output */ new TextEncoder().encode(this.get_Output_FromOutputname(data_paragraph_arr[i].xkb_output[j])) + ); + rules.push(ruleObj); + } + } + dataObject.rules = rules; + return this.reviewRuleInputData(dataObject); + } + + /** + * @brief ToDo check description + * @brief convert the key name obtained from symbol file to the matching keyname e.g. name of key --> 'K_A' + * @param key_name as stated in the symbol file + * @return the equivalent keyman virtual keycode + */ + public get_KMVirtKC_from_keyname(key_name: string): string { + let out = ''; + + if (key_name == "") + out = 'K_BKQUOTE'; + else if (key_name == "") + out = 'K_1'; + else if (key_name == "") + out = 'K_2'; + else if (key_name == "") + out = 'K_3'; + else if (key_name == "") + out = 'K_4'; + else if (key_name == "") + out = 'K_5'; + else if (key_name == "") + out = 'K_6'; + else if (key_name == "") + out = 'K_7'; + else if (key_name == "") + out = 'K_8'; + else if (key_name == "") + out = 'K_9'; + else if (key_name == "") + out = 'K_0'; + else if (key_name == "") + out = 'K_HYPHEN'; + else if (key_name == "") + out = 'K_EQUAL'; + else if (key_name == "") + out = 'K_Q'; + else if (key_name == "") + out = 'K_W'; + else if (key_name == "") + out = 'K_E'; + else if (key_name == "") + out = 'K_R'; + else if (key_name == "") + out = 'K_T'; + else if (key_name == "") + out = 'K_Y'; + else if (key_name == "") + out = 'K_U'; + else if (key_name == "") + out = 'K_I'; + else if (key_name == "") + out = 'K_O'; + else if (key_name == "") + out = 'K_P'; + else if (key_name == "") + out = 'K_LBRKT'; + else if (key_name == "") + out = 'K_RBRKT'; + + else if (key_name == "") + out = 'K_A'; + else if (key_name == "") + out = 'K_S'; + else if (key_name == "") + out = 'K_D'; + else if (key_name == "") + out = 'K_F'; + else if (key_name == "") + out = 'K_G'; + else if (key_name == "") + out = 'K_H'; + else if (key_name == "") + out = 'K_J'; + else if (key_name == "") + out = 'K_K'; + else if (key_name == "") + out = 'K_L'; + else if (key_name == "") + out = 'K_COLON'; + else if (key_name == "") + out = 'K_QUOTE'; + + else if (key_name == "") + out = 'K_Z'; + else if (key_name == "") + out = 'K_X'; + else if (key_name == "") + out = 'K_C'; + else if (key_name == "") + out = 'K_V'; + else if (key_name == "") + out = 'K_B'; + else if (key_name == "") + out = 'K_N'; + else if (key_name == "") + out = 'K_M'; + else if (key_name == "") + out = 'K_COMMA'; + else if (key_name == "") + out = 'K_PERIOD'; + else if (key_name == "") + out = 'K_SLASH'; + else if (key_name == "") + out = 'K_BKSLASH'; + else if (key_name == "") + out = 'K_RIGHTSHIFT'; + else if (key_name == "") + out = 'K_SPACE'; + + return String(out); + } + + + /** + * @brief ToDo check description + * @brief convert a keyval name to an output character + more on https://manpages.ubuntu.com/manpages/jammy/man3/keysyms.3tk.html + * @param outchar the name of the output + * @return the input character if input is a character,an empty string if "NoSymbol" is found, + * the output character if found in map, a Warning if not + */ + public get_Output_FromOutputname(outchar: string): string { + + if ((!outchar) || (outchar === "NoSymbol")) + return (''); + // it is already a character + if (outchar.length === 1) + return (outchar); + + if (outchar.slice(0, 2) === "0x") { + console.log(" ToDo-kmc-convert keysym name starting with 0x not found!"); + return ('******* keysym name not found! - with 0x :' + outchar); + } + + const m_uni = /^U([0-9a-f]{1,6})$/i.exec(outchar); + if (m_uni) { + console.log(" ToDo-kmc-convert keysym name starting with Uxxxx not found!"); + return ('******* keysym name not found! - with Uxxxx :' + outchar); + } + + // ToDo-kmc-convert find a function to lookup values + const key_values = new Map(); + key_values.set("ampersand", 38); + key_values.set("apostrophe", 39); + key_values.set("asciicircum", 136); + key_values.set("asciitilde", 126); + key_values.set("asterisk", 42); + key_values.set("at", 64); + key_values.set("backslash", 92); + key_values.set("BackSpace", 65288); + key_values.set("bar", 124); + key_values.set("braceleft", 123); + key_values.set("braceright", 125); + key_values.set("bracketleft", 91); + key_values.set("bracketright", 93); + key_values.set("colon", 58); + key_values.set("comma", 44); + key_values.set("diaeresis", 168); + key_values.set("dollar", 36); + key_values.set("equal", 61); + key_values.set("exclam", 33); + key_values.set("grave", 96); + key_values.set("greater", 62); + key_values.set("less", 60); + key_values.set("minus", 45); + key_values.set("numbersign", 35); + key_values.set("parenleft", 40); + key_values.set("parenright", 41); + key_values.set("percent", 37); + key_values.set("period", 46); + key_values.set("plus", 43); + key_values.set("question", 63); + key_values.set("quotedbl", 34); + key_values.set("semicolon", 59); + key_values.set("slash", 47); + key_values.set("space", 32); + key_values.set("ssharp", 223); + key_values.set("underscore", 95); + key_values.set("nobreakspace", 160); + key_values.set("exclamdown", 161); + key_values.set("cent", 162); + key_values.set("sterling", 163); + key_values.set("currency", 164); + key_values.set("yen", 165); + key_values.set("brokenbar", 166); + key_values.set("section", 167); + key_values.set("copyright", 169); + key_values.set("ordfeminine", 170); + key_values.set("guillemotleft", 171); + key_values.set("notsign", 172); + key_values.set("hyphen", 173); + key_values.set("registered", 174); + key_values.set("macron", 175); + key_values.set("degree", 176); + key_values.set("plusminus", 177); + key_values.set("twosuperior", 178); + key_values.set("threesuperior", 179); + key_values.set("acute", 180); + key_values.set("mu", 181); + key_values.set("paragraph", 182); + key_values.set("periodcentered", 183); + key_values.set("cedilla", 184); + key_values.set("onesuperior", 185); + key_values.set("masculine", 186); + key_values.set("guillemotright", 187); + key_values.set("onequarter", 188); + key_values.set("onehalf", 189); + key_values.set("threequarters", 190); + key_values.set("questiondown", 191); + key_values.set("Agrave", 192); + key_values.set("Aacute", 193); + key_values.set("Acircumflex", 194); + key_values.set("Atilde", 195); + key_values.set("Adiaeresis", 196); + key_values.set("Aring", 197); + key_values.set("AE", 198); + key_values.set("Ccedilla", 199); + key_values.set("Egrave", 200); + key_values.set("Eacute", 201); + key_values.set("Ecircumflex", 202); + key_values.set("Ediaeresis", 203); + key_values.set("Igrave", 204); + key_values.set("Iacute", 205); + key_values.set("Icircumflex", 206); + key_values.set("Idiaeresis", 207); + key_values.set("ETH", 208); + key_values.set("Ntilde", 209); + key_values.set("Ograve", 210); + key_values.set("Oacute", 211); + key_values.set("Ocircumflex", 212); + key_values.set("Otilde", 213); + key_values.set("Odiaeresis", 214); + key_values.set("multiply", 215); + key_values.set("Oslash", 216); + key_values.set("Ugrave", 217); + key_values.set("Uacute", 218); + key_values.set("Ucircumflex", 219); + key_values.set("Udiaeresis", 220); + key_values.set("Yacute", 221); + key_values.set("THORN", 222); + key_values.set("agrave", 224); + key_values.set("aacute", 225); + key_values.set("acircumflex", 226); + key_values.set("atilde", 227); + key_values.set("adiaeresis", 228); + key_values.set("aring", 229); + key_values.set("ae", 230); + key_values.set("ccedilla", 231); + key_values.set("egrave", 232); + key_values.set("eacute", 233); + key_values.set("ecircumflex", 234); + key_values.set("ediaeresis", 235); + key_values.set("igrave", 236); + key_values.set("iacute", 237); + key_values.set("icircumflex", 238); + key_values.set("idiaeresis", 239); + key_values.set("eth", 240); + key_values.set("ntilde", 241); + key_values.set("ograve", 242); + key_values.set("oacute", 243); + key_values.set("ocircumflex", 244); + key_values.set("otilde", 245); + key_values.set("odiaeresis", 246); + key_values.set("division", 247); + key_values.set("oslash", 248); + key_values.set("ugrave", 249); + key_values.set("uacute", 250); + key_values.set("ucircumflex", 251); + key_values.set("udiaeresis", 252); + key_values.set("yacute", 253); + key_values.set("thorn", 254); + key_values.set("ydiaeresis", 255); + key_values.set("Aogonek", 417); + key_values.set("breve", 418); + key_values.set("Lstroke", 419); + key_values.set("Lcaron", 421); + key_values.set("Sacute", 422); + key_values.set("Scaron", 425); + key_values.set("Scedilla", 426); + key_values.set("Tcaron", 427); + key_values.set("Zacute", 428); + key_values.set("Zcaron", 430); + key_values.set("Zabovedot", 431); + key_values.set("aogonek", 433); + key_values.set("ogonek", 434); + key_values.set("lstroke", 435); + key_values.set("lcaron", 437); + key_values.set("sacute", 438); + key_values.set("caron", 439); + key_values.set("scaron", 441); + key_values.set("scedilla", 442); + key_values.set("tcaron", 443); + key_values.set("zacute", 444); + key_values.set("doubleacute", 445); + key_values.set("zcaron", 446); + key_values.set("zabovedot", 447); + key_values.set("Racute", 448); + key_values.set("Abreve", 451); + key_values.set("Lacute", 453); + key_values.set("Cacute", 454); + key_values.set("Ccaron", 456); + key_values.set("Eogonek", 458); + key_values.set("Ecaron", 460); + key_values.set("Dcaron", 463); + key_values.set("Dstroke", 464); + key_values.set("Nacute", 465); + key_values.set("Ncaron", 466); + key_values.set("Odoubleacute", 469); + key_values.set("Rcaron", 472); + key_values.set("Uring", 473); + key_values.set("Udoubleacute", 475); + key_values.set("Tcedilla", 478); + key_values.set("racute", 480); + key_values.set("abreve", 483); + key_values.set("lacute", 485); + key_values.set("cacute", 486); + key_values.set("ccaron", 488); + key_values.set("eogonek", 490); + key_values.set("ecaron", 492); + key_values.set("dcaron", 495); + key_values.set("dstroke", 496); + key_values.set("nacute", 497); + key_values.set("ncaron", 498); + key_values.set("odoubleacute", 501); + key_values.set("rcaron", 504); + key_values.set("uring", 505); + key_values.set("udoubleacute", 507); + key_values.set("tcedilla", 510); + key_values.set("abovedot", 511); + key_values.set("Hstroke", 673); + key_values.set("Hcircumflex", 678); + key_values.set("Iabovedot", 681); + key_values.set("Gbreve", 683); + key_values.set("Jcircumflex", 684); + key_values.set("hstroke", 689); + key_values.set("hcircumflex", 694); + key_values.set("idotless", 697); + key_values.set("gbreve", 699); + key_values.set("jcircumflex", 700); + key_values.set("Cabovedot", 709); + key_values.set("Ccircumflex", 710); + key_values.set("Gabovedot", 725); + key_values.set("Gcircumflex", 728); + key_values.set("Ubreve", 733); + key_values.set("Scircumflex", 734); + key_values.set("cabovedot", 741); + key_values.set("ccircumflex", 742); + key_values.set("gabovedot", 757); + key_values.set("gcircumflex", 760); + key_values.set("ubreve", 765); + key_values.set("scircumflex", 766); + key_values.set("kra", 930); + key_values.set("Rcedilla", 931); + key_values.set("Itilde", 933); + key_values.set("Lcedilla", 934); + key_values.set("Emacron", 938); + key_values.set("Gcedilla", 939); + key_values.set("Tslash", 940); + key_values.set("rcedilla", 947); + key_values.set("itilde", 949); + key_values.set("lcedilla", 950); + key_values.set("emacron", 954); + key_values.set("gcedilla", 955); + key_values.set("tslash", 956); + key_values.set("ENG", 957); + key_values.set("eng", 959); + key_values.set("Amacron", 960); + key_values.set("Iogonek", 967); + key_values.set("Eabovedot", 972); + key_values.set("Imacron", 975); + key_values.set("Ncedilla", 977); + key_values.set("Omacron", 978); + key_values.set("Kcedilla", 979); + key_values.set("Uogonek", 985); + key_values.set("Utilde", 989); + key_values.set("Umacron", 990); + key_values.set("amacron", 992); + key_values.set("iogonek", 999); + key_values.set("eabovedot", 1004); + key_values.set("imacron", 1007); + key_values.set("ncedilla", 1009); + key_values.set("omacron", 1010); + key_values.set("kcedilla", 1011); + key_values.set("uogonek", 1017); + key_values.set("utilde", 1021); + key_values.set("umacron", 1022); + key_values.set("overline", 1150); + key_values.set("dead_abovedot", 729); + key_values.set("dead_abovering", 730); + key_values.set("dead_acute", 180); + key_values.set("dead_breve", 728); + key_values.set("dead_caron", 711); + key_values.set("dead_cedilla", 184); + key_values.set("dead_circumflex", 94); + key_values.set("dead_diaeresis", 168); + key_values.set("dead_doubleacute", 733); + key_values.set("dead_grave", 96); + key_values.set("dead_ogonek", 731); + key_values.set("dead_perispomeni", 126); + key_values.set("dead_tilde", 126); + key_values.set("acute accent", 0xB4); + + const result = key_values.get(outchar); + + if (!result) { + console.log(" ToDo-kmc-convert keysymname not found!"); + return ('******* keysym name not found!' + outchar); + } + // Todo-kmc-convert better names + const codePoint_u = parseInt(result, 10); + const outconv = String.fromCodePoint(codePoint_u); + return outconv; + } + + /** + * @brief ToDo check description + * @brief member function to review data in array of rules of dataUkelele: remove duplicate rules and mark first occurance of a rule in rules + * @param dataUkelele: an object containing the name of the in/output file, an array of behaviors and an array of Rules + * @return an object containing the name of the input file, an array of behaviors and the revised array of Rule[] + */ + public reviewRuleInputData(dataUkelele: ProcessedData): ProcessedData { + + // check for duplicate C2 and C3 rules in rules (e.g. [NCAPS RALT K_8] > dk(C12) ): create a separate array of unique rules, + // then compare to rules and mark first occurrence of a rule in rules + + let uniqueCountDkB = 0; + const uniqueTextRules: string[][] = []; + + const rules: Rule[] = dataUkelele.rules; + + //------------------------------------ C2: dk ---------------------------------- + // first rule is always unique + rules[0].uniqueDeadkey = uniqueCountDkB; + rules[0].idDeadkey = uniqueCountDkB; + uniqueCountDkB++; + + for (let i = 0; i < rules.length; i++) { + + + if (((rules[i].modifierDeadkey !== undefined) && (rules[i].modifierDeadkey !== "")) + && ((rules[i].deadkey !== undefined) && (rules[i].deadkey !== ""))) { + let IsFirstUsedHereDk: boolean = true; + + // check if not used before + for (let j = 0; j < i; j++) { + if ((rules[i].modifierDeadkey === rules[j].modifierDeadkey) + && (rules[i].deadkey === rules[j].deadkey)) { + IsFirstUsedHereDk = IsFirstUsedHereDk && false; + } + } + + if (IsFirstUsedHereDk) { + rules[i].uniqueDeadkey = uniqueCountDkB; + uniqueTextRules.push([ + rules[i].modifierDeadkey, + rules[i].deadkey, + String(uniqueCountDkB)]); + uniqueCountDkB++; + } + } + } + + //----------------------------------- C3: prev-dk ---------------------------------- + let uniqueCountDkA = 0; + + // first rule is always unique + rules[0].uniquePrevDeadkey = uniqueCountDkA; + uniqueCountDkA++; + + for (let i = 0; i < rules.length; i++) { + if ((rules[i].modifierPrevDeadkey !== "") && (rules[i].prevDeadkey !== "")) { + let isFirstUsedHerePrevDk: boolean = true; + + // check if not used before + for (let j = 0; j < i; j++) { + if ((rules[i].modifierPrevDeadkey === rules[j].modifierPrevDeadkey) + && (rules[i].prevDeadkey === rules[j].prevDeadkey)) { + isFirstUsedHerePrevDk = isFirstUsedHerePrevDk && false; + } + } + + // check if first part of C3 rule contains a rule that is already defined in C2 + if (isFirstUsedHerePrevDk) { + rules[i].uniquePrevDeadkey = uniqueCountDkA; + uniqueCountDkA++; + for (let k = 0; k < uniqueTextRules.length; k++) { + if ((uniqueTextRules[k][0] === rules[i].modifierDeadkey) && (uniqueTextRules[k][1] === rules[i].deadkey)) { + rules[i].uniqueDeadkey = Number(uniqueTextRules[k][2]); + } + } + } + + if (isFirstUsedHerePrevDk) { + rules[i].uniqueDeadkey = uniqueCountDkB; + uniqueTextRules.push([ + rules[i].modifierPrevDeadkey, + rules[i].prevDeadkey, + String(uniqueCountDkB) + ]); + uniqueCountDkB++; + } + } + } + + // loop through rules and mark first occurence each rule of uniqueTextRules + for (let i = 0; i < rules.length; i++) { + for (let j = 0; j < uniqueTextRules.length; j++) { + if ((rules[i].modifierPrevDeadkey === uniqueTextRules[j][0]) && (rules[i].prevDeadkey === uniqueTextRules[j][1])) { + rules[i].idPrevDeadkey = Number(uniqueTextRules[j][2]); + } + if ((rules[i].modifierDeadkey === uniqueTextRules[j][0]) && (rules[i].deadkey === uniqueTextRules[j][1])) { + rules[i].idDeadkey = Number(uniqueTextRules[j][2]); + } + } + } + dataUkelele.rules = rules; + return dataUkelele; + } + + /** @internal */ + public convertBound = { + convert: this.convert.bind(this), + }; +} + diff --git a/developer/src/kmc-convert/src/kmc-convert-read/keylayout-file-reader.ts b/developer/src/kmc-convert/src/kmc-convert-read/keylayout-file-reader.ts new file mode 100644 index 00000000000..46b4e055b31 --- /dev/null +++ b/developer/src/kmc-convert/src/kmc-convert-read/keylayout-file-reader.ts @@ -0,0 +1,169 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by S. Schmitt on 2025-05-12 + * + * Read macOS/Ukelele .keylayout files + * + */ + +import { CompilerCallbacks, DeveloperUtilsMessages, Keylayout, KeymanXMLReader } from "@keymanapp/developer-utils"; +import { util, SchemaValidators } from '@keymanapp/common-types'; +import { ConverterMessages } from '../converter-messages.js'; +import boxXmlArray = util.boxXmlArray; +import { KL_KeyMapSelect, KL_KeyMap } from "../../../common/web/utils/src/types/keylayout/keylayout-xml.js"; + +export class KeylayoutFileReader { + + constructor(private callbacks: CompilerCallbacks /*,private options: CompilerOptions*/) { }; + + + /** + * @brief helper function to find a specific keyMap index in a keyMapSet + * @param jsonObj the read keylayout data to be checked + * @param keyMapSelect the keyMapSelect element to find in keyMapSet + * @return true if the keyMapSet element is found, false if not + */ + public findMapIndexinKeymap(jsonObj: Keylayout.KeylayoutXMLSourceFile, keyMapSelect: KL_KeyMapSelect): boolean { + for (const keyMapSet of jsonObj.keyboard.keyMapSet) { + for (const keyMap of keyMapSet.keyMap) { + if (keyMap['index'] === keyMapSelect.mapIndex) { + return true; + } + } + } + return false; + } + + /** + * @brief helper function to find a specific keyMapSelect index in a modifierMap + * @param jsonObj the read keylayout data to be checked + * @param keyMap the keyMap element to find in modifierMap + * @return true if the keyMap element is found, false if not + */ + public findIndexinKeymapSelect(jsonObj: Keylayout.KeylayoutXMLSourceFile, keyMap: KL_KeyMap): boolean { + for (const modifierMap of jsonObj.keyboard.modifierMap) { + for (const keyMapSelect of modifierMap.keyMapSelect) { + if (keyMapSelect['mapIndex'] === keyMap.index) { + return true; + } + } + } + return false; + } + + /** + * @brief member function checking if all keyMapSelect elements have a corresponding keyMap + * element in the .keylayout file (if not, the .keylayout file is invalid and will not be converted) + * see TN2056 (https://developer.apple.com/library/archive/technotes/tn2056/_index.html#//apple_ref/doc/uid/DTS10003085-CH1-SUBSECTION7) + * @param jsonObj the read keylayout data to be checked + * @return true if all keyMapSelect elements have a corresponding keyMap element, false if not + */ + public checkForCorrespondingElements(jsonObj: Keylayout.KeylayoutXMLSourceFile): boolean { + let available = true; + + // check if all keyMapSelect elements have a corresponding keyMap element in the .keylayout file + for (const modifierMap of jsonObj.keyboard.modifierMap) { + for (const keyMapSelect of modifierMap.keyMapSelect) { + available = available && this.findMapIndexinKeymap(jsonObj, keyMapSelect); + } + } + // check if all keyMap elements have a corresponding keyMapSelect element in the .keylayout file + for (const keyMapSet of jsonObj.keyboard.keyMapSet) { + for (const keyMap of keyMapSet.keyMap) { + available = available && this.findIndexinKeymapSelect(jsonObj, keyMap); + } + } + return available; + } + + + /** + * @returns true if valid, false if invalid + */ + public validate(source: Keylayout.KeylayoutXMLSourceFile, inputFilename: string): boolean { + if (!SchemaValidators.default.keylayout(source)) { + for (const err of (SchemaValidators.default.keylayout).errors) { + this.callbacks.reportMessage(DeveloperUtilsMessages.Error_InvalidXml({ + e: err.instancePath + })); + } + return false; + } + + // check if all keyMapSelect elements have a corresponding keyMap element in the .keylayout file + if (!this.checkForCorrespondingElements(source)) { + this.callbacks.reportMessage(ConverterMessages.Error_InvalidFile({ errorText: inputFilename })); + return false; + } + return true; + } + + /** + * @brief member function to box single-entry objects into arrays + * @param source the object to be changed + * @return object that contain only boxed arrays + */ + public boxArray(source: any) { + + boxXmlArray(source, 'modifierMap'); + + boxXmlArray(source, 'keyMapSet'); + + if (source.layouts) { + boxXmlArray(source.layouts, 'layout'); + } + + if (source.modifierMap) { + for (const modifierMap of source.modifierMap) { + boxXmlArray(modifierMap, 'keyMapSelect'); + if (modifierMap.keyMapSelect) { + for (const keyMapSelect of modifierMap.keyMapSelect) { + boxXmlArray(keyMapSelect, 'modifier'); + } + } + } + } + + for (const keyMapSet of source.keyMapSet) { + boxXmlArray(keyMapSet, 'keyMap'); + for (const keyMap of keyMapSet.keyMap) { + boxXmlArray(keyMap, 'key'); + } + } + + if (source.actions) { + boxXmlArray(source.actions, 'action'); + for (const action of source.actions.action) { + boxXmlArray(action, 'when'); + } + } + + boxXmlArray(source?.terminators, 'when'); + } + + /** + * @brief member function to parse data from a .keylayout-file and store in a json object + * we need to be able to ignore an output character of "", process an output character of " " (space) and allow surrounding whitespace in #text (which will be removed later) + * @param inputFilename the ukelele .keylayout-file to be parsed + * @return in case of success: json object containing data of the .keylayout file; else null + */ + public read(source: Uint8Array): Keylayout.KeylayoutXMLSourceFile { + + try { + const data = new TextDecoder().decode(source); + const jsonObj = new KeymanXMLReader('keylayout').parse(data) as Keylayout.KeylayoutXMLSourceFile; + + if (!jsonObj?.keyboard) { + this.callbacks.reportMessage(ConverterMessages.Error_UnableToParse()); + return null; + } + this.boxArray(jsonObj.keyboard); // jsonObj now contains arrays; no single fields + return jsonObj; + } + catch (err) { + this.callbacks.reportMessage(ConverterMessages.Error_UnableToRead()); + return null; + } + } +} diff --git a/developer/src/kmc-convert/src/kmc-convert-read/xkb-file-reader.ts b/developer/src/kmc-convert/src/kmc-convert-read/xkb-file-reader.ts new file mode 100644 index 00000000000..33d5531bfb3 --- /dev/null +++ b/developer/src/kmc-convert/src/kmc-convert-read/xkb-file-reader.ts @@ -0,0 +1,41 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by S. Schmitt on 2025-05-12 + * + * Read macOS/Ukelele .keylayout files + * + */ + +import { CompilerCallbacks/*, DeveloperUtilsMessages, Keylayout*/ } from "@keymanapp/developer-utils"; +import { ConverterMessages } from '../converter-messages.js'; + +export class XkbFileReader { + + constructor(private callbacks: CompilerCallbacks /*,private options: CompilerOptions*/) { }; + + /** + * @brief member function to parse data from a .keylayout-file and store in a json object + * we need to be able to ignore an output character of "", process an output character of " " (space) and allow surrounding whitespace in #text (which will be removed later) + * @param inputFilename the ukelele .keylayout-file to be parsed + * @return in case of success: json object containing data of the .keylayout file; else null + */ + public read(source: Uint8Array): string |null{ + + try { + const data = new TextDecoder().decode(source); + + // ToDo-kmc-convert double msg? + + if (!data) { + this.callbacks.reportMessage(ConverterMessages.Error_UnableToRead()); + return null; + } + return data; + } + catch (err) { + this.callbacks.reportMessage(ConverterMessages.Error_UnableToRead()); + return null; + } + } +} diff --git a/developer/src/kmc-convert/src/kmc-convert-write/kmn-file-writer.ts b/developer/src/kmc-convert/src/kmc-convert-write/kmn-file-writer.ts new file mode 100644 index 00000000000..e556e0f5e1b --- /dev/null +++ b/developer/src/kmc-convert/src/kmc-convert-write/kmn-file-writer.ts @@ -0,0 +1,1096 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by S. Schmitt on 2025-05-12 + * + * Write Keyman .kmn files from an in-memory representation generated + * + */ + +import { CompilerCallbacks, CompilerOptions } from "@keymanapp/developer-utils"; +import { KeylayoutToKmnConverter, ProcessedData, Rule } from './../kmc-convert-convert/keylayout-to-kmn-converter.js'; +import { ConverterMessages } from '../converter-messages.js'; +import KEYMAN_VERSION from "@keymanapp/keyman-version"; + +export interface messageCharacter { + message: string; + character: string; +}; + +export class KmnFileWriter { + + constructor(private callbacks: CompilerCallbacks, private options: CompilerOptions) { }; + + // TODO-kmc-convert remove + public writeToFile(dataUkelele: ProcessedData): boolean | null { + + let data: string = "\n"; + + // add top part of kmn file: STORES + data += this.writeKmnFileHeader(dataUkelele); + + // add bottom part of kmn file: RULES + data += this.writeDataRules(dataUkelele); + + try { + this.callbacks.fs.writeFileSync(dataUkelele.kmnFilename, new TextEncoder().encode(data)); + return true; + } catch (err: any) { + this.callbacks.reportMessage(ConverterMessages.Error_UnableToWrite({ outputFilename: dataUkelele.kmnFilename, errorText: err })); + return false; + } + } + /** + * @brief member function to write data from object to a Uint8Array + * @param dataUkelele the array holding all keyboard data + * @return a Uint8Array holding data + */ + public write(dataUkelele: ProcessedData): Uint8Array|null { + let data: string = "\n"; + + // top part of kmn file: STORES + const dataStores = this.writeKmnFileHeader(dataUkelele); + + // bottom part of kmn file: RULES + const dataRules = this.writeDataRules(dataUkelele); + + if (dataRules) + data += dataStores + dataRules; + + try { + return new TextEncoder().encode(data); + } catch (err:any) { + this.callbacks.reportMessage(ConverterMessages.Error_UnableToWrite({ outputFilename: dataUkelele.kmnFilename, errorText: err })); + return null; + } + } + + /** + * @brief member function to create data for the header (stores) that will be printed to the resulting kmn file + * @param dataUkelele an object containing all data read from a .keylayout file + * @return string - all stores to be printed + */ + public writeKmnFileHeader(dataUkelele: ProcessedData): string { + + let data: string = ""; + + data += "c ..................................................................................................................\n"; + data += "c ..................................................................................................................\n"; + data += "c Keyman keyboard generated by kmn-convert version: " + KEYMAN_VERSION.VERSION + "\n"; + data += "c from Ukelele file: " + dataUkelele.keylayoutFilename + "\n"; + data += "c ..................................................................................................................\n"; + data += "c ..................................................................................................................\n"; + data += "\n"; + + data += "store(&TARGETS) \'desktop\'\n"; + + data += "\n"; + data += "begin Unicode > use(main)\n\n"; + data += "group(main) using keys\n\n"; + + data += "\n"; + return data; + } + + /** + * @brief member function to create data from rules that will be printed to the resulting kmn file + * @param dataUkelele an object containing all data read from a .keylayout file + * @return string - all rules to be printed + */ + public writeDataRules(dataUkelele: ProcessedData): string { + + const keylayoutKmnConverter = new KeylayoutToKmnConverter(this.callbacks, this.options); + 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 uniqueDataRules: Rule[] = dataUkelele.rules.filter((curr) => { + return (!(curr.output.length === 0 || curr.output === undefined) + && (curr.key !== "") + && ((curr.ruleType === "C0") + || (curr.ruleType === "C1") + || (curr.ruleType === "C2" && (curr.deadkey !== "")) + || (curr.ruleType === "C3" && (curr.deadkey !== "") && (curr.prevDeadkey !== ""))) + ); + }).reduce((unique, o) => { + if (!unique.some((obj: Rule) => + new TextDecoder().decode(obj.output) === new TextDecoder().decode(o.output) + + && obj.ruleType === o.ruleType + && obj.modifierKey === o.modifierKey + && obj.key === o.key + + && obj.modifierDeadkey === o.modifierDeadkey + && obj.deadkey === o.deadkey + + && obj.modifierPrevDeadkey === o.modifierPrevDeadkey + && obj.prevDeadkey === o.prevDeadkey) + ) { + unique.push(o); + } + return unique; + }, []); + + //................................................ C0 C1 ................................................................ + + for (let k = 0; k < uniqueDataRules.length; k++) { + + if ((uniqueDataRules[k].ruleType === "C0") || (uniqueDataRules[k].ruleType === "C1")) { + + // lookup key nr of the key which is being processed + let keyNr: number = 0; + for (let j = 0; j <= KeylayoutToKmnConverter.MAX_KEY_IDENTIFIER; j++) { + if (keylayoutKmnConverter.mapUkeleleKeycodeToVK(j) === uniqueDataRules[k].key) { + keyNr = j; + break; + } + } + + // skip keyNr 48 (K_TAB) and 36 (K_ENTER) + if ((keyNr === 48) || (keyNr === 36)) { + continue; + } + + // add a line after rules of each key + if ((k > 1) && (uniqueDataRules[k - 1].key !== uniqueDataRules[k].key) && (uniqueDataRules[k - 1].ruleType === uniqueDataRules[k].ruleType)) { + data += '\n'; + } + + // use of Unicode Character vs Unicode Codepoint; + // If it`s a ctrl character we print out the Unicode Codepoint else we print out the Unicode Character + let versionOutputCharacter; + const warnText = this.reviewRules(uniqueDataRules, k); + + const outputCharacter = new TextDecoder().decode(uniqueDataRules[k].output); + // TODO-kmc-convert: after merge of PR 14569 use functions from util instead of the ones in this class + // const outputUnicodeCharacter = util.convertToUnicodeCharacter(outputCharacter); + // const outputUnicodeCodePoint = util.convertToUnicodeCodePoint(outputCharacter); + + if ((outputCharacter !== undefined) || (outputCharacter !== "")) { + const characterMessage = this.writeCharacterOrUnicode(outputCharacter, warnText[2]); + versionOutputCharacter = characterMessage.character; + warnText[2] = characterMessage.message; + } + + // add a warning in front of rules in case unavailable modifiers or ambiguous rules are used + // if warning contains duplicate rules we do not write out the entire rule + // (even if there are other warnings for the same rule) since that rule had been written before + if (warnText[2].indexOf("duplicate") < 0) { + + let warningTextToWrite = ""; + if (!KeylayoutToKmnConverter.SKIP_COMMENTED_LINES && (warnText[2].length > 0)) { + warningTextToWrite = warnText[2]; + } + + if (!((warnText[2].length > 0) && KeylayoutToKmnConverter.SKIP_COMMENTED_LINES)) { + if (versionOutputCharacter === "'") { + data += warningTextToWrite + + "+ [" + + (uniqueDataRules[k].modifierKey + ' ' + uniqueDataRules[k].key).trim() + + `] > \"` + + versionOutputCharacter + + '\"\n'; + } + else { + data += warningTextToWrite + + "+ [" + + (uniqueDataRules[k].modifierKey + ' ' + uniqueDataRules[k].key).trim() + + `] > \'` + + versionOutputCharacter + + '\'\n'; + } + } + } + } + } + + //................................................ C2 ................................................................... + for (let k = 0; k < uniqueDataRules.length; k++) { + + if (uniqueDataRules[k].ruleType === "C2") { + + // use of Unicode Character vs Unicode Codepoint; + // If it`s a ctrl character we print out the Unicode Codepoint else we print out the Unicode Character + let versionOutputCharacter; + const warnText = this.reviewRules(uniqueDataRules, k); + + const outputCharacter = new TextDecoder().decode(uniqueDataRules[k].output); + // TODO-kmc-convert: after merge of PR 14569 use functions from util instead of the ones in this class + // const outputUnicodeCharacter = util.convertToUnicodeCharacter(outputCharacter); + // const outputUnicodeCodePoint = util.convertToUnicodeCodePoint(outputCharacter); + + if ((outputCharacter !== undefined) || (outputCharacter !== "")) { + const characterMessage = this.writeCharacterOrUnicode(outputCharacter, warnText[2]); + versionOutputCharacter = characterMessage.character; + warnText[2] = characterMessage.message; + } + + // add a warning in front of rules in case unavailable modifiers or ambiguous rules are used + // if warning contains duplicate rules we do not write out the entire rule + // (even if there are other warnings for the same rule) since that rule had been written before + if (warnText[1].indexOf("duplicate") < 0) { + + let warningTextToWrite = ""; + if (!KeylayoutToKmnConverter.SKIP_COMMENTED_LINES && (warnText[1].length > 0)) { + warningTextToWrite = warnText[1]; + } + + if (!((warnText[1].length > 0) && KeylayoutToKmnConverter.SKIP_COMMENTED_LINES)) { + data += warningTextToWrite + + "+ [" + (uniqueDataRules[k].modifierDeadkey + " " + + uniqueDataRules[k].deadkey).trim() + + "] > dk(A" + String(uniqueDataRules[k].idDeadkey) + + ")\n"; + } + } + + if ((warnText[2].indexOf("duplicate") < 0)) { + + let warningTextToWrite = ""; + if (!KeylayoutToKmnConverter.SKIP_COMMENTED_LINES && (warnText[2].length > 0)) { + warningTextToWrite = warnText[2]; + } + + if (!((warnText[2].length > 0) && KeylayoutToKmnConverter.SKIP_COMMENTED_LINES)) { + if (versionOutputCharacter === "'") { + data += warningTextToWrite + + "dk(A" + + (String(uniqueDataRules[k].idDeadkey) + ") + [" + + uniqueDataRules[k].modifierKey).trim() + + " " + + uniqueDataRules[k].key + '] > \"' + + versionOutputCharacter + + '\"\n'; + } + else { + data += warningTextToWrite + + "dk(A" + + (String(uniqueDataRules[k].idDeadkey) + ") + [" + + uniqueDataRules[k].modifierKey).trim() + + " " + + uniqueDataRules[k].key + "] > \'" + + versionOutputCharacter + + "\'\n"; + } + + + } + data += "\n"; + } + } + } + + //................................................ C3 ................................................................... + + for (let k = 0; k < uniqueDataRules.length; k++) { + if (uniqueDataRules[k].ruleType === "C3") { + + // use of Unicode Character vs Unicode Codepoint; + // If it`s a ctrl character we print out the Unicode Codepoint else we print out the Unicode Character + let versionOutputCharacter; + + const warnText = this.reviewRules(uniqueDataRules, k); + const outputCharacter = new TextDecoder().decode(uniqueDataRules[k].output); + // TODO-kmc-convert: after merge of PR 14569 use functions from util instead of the ones in this class + + if ((outputCharacter !== undefined) || (outputCharacter !== "")) { + const characterMessage = this.writeCharacterOrUnicode(outputCharacter, warnText[2]); + versionOutputCharacter = characterMessage.character; + warnText[2] = characterMessage.message; + } + + // add a warning in front of rules in case unavailable modifiers or ambiguous rules are used + // if warning contains duplicate rules we do not write out the entire rule + // (even if there are other warnings for the same rule) since that rule had been written before + if (warnText[0].indexOf("duplicate") < 0) { + + let warningTextToWrite = ""; + + if (!KeylayoutToKmnConverter.SKIP_COMMENTED_LINES && (warnText[0].length > 0)) { + warningTextToWrite = warnText[0]; + } + + if (!((warnText[0].length > 0) && KeylayoutToKmnConverter.SKIP_COMMENTED_LINES)) { + data += warningTextToWrite + + "+ [" + + (uniqueDataRules[k].modifierPrevDeadkey + " " + + uniqueDataRules[k].prevDeadkey).trim() + + "] > dk(A" + + String(uniqueDataRules[k].idPrevDeadkey) + ")\n"; + } + } + + if (warnText[1].indexOf("duplicate") < 0) { + + let warningTextToWrite = ""; + if (!KeylayoutToKmnConverter.SKIP_COMMENTED_LINES && (warnText[1].length > 0)) { + warningTextToWrite = warnText[1]; + } + + if (!((warnText[1].length > 0) && KeylayoutToKmnConverter.SKIP_COMMENTED_LINES)) { + data += warningTextToWrite + + "dk(A" + (String(uniqueDataRules[k].idPrevDeadkey) + ") + [" + + uniqueDataRules[k].modifierDeadkey).trim() + + " " + + uniqueDataRules[k].deadkey + + "] > dk(B" + + String(uniqueDataRules[k].idDeadkey) + + ")\n"; + } + } + + if (warnText[2].indexOf("duplicate") < 0) { + + let warningTextToWrite = ""; + if (!KeylayoutToKmnConverter.SKIP_COMMENTED_LINES && (warnText[2].length > 0)) { + warningTextToWrite = warnText[2]; + } + + if (!((warnText[2].length > 0) && KeylayoutToKmnConverter.SKIP_COMMENTED_LINES)) { + data += warningTextToWrite + "dk(B" + + (String(uniqueDataRules[k].idDeadkey) + + ") + [" + + uniqueDataRules[k].modifierKey).trim() + + " " + + uniqueDataRules[k].key + + "] > \'" + + versionOutputCharacter + + "\'\n"; + } + } + + if ((warnText[0].indexOf("duplicate") < 0) || (warnText[1].indexOf("duplicate") < 0) || (warnText[2].indexOf("duplicate") < 0)) { + data += "\n"; + } + } + } + return data; + } + + /** + * @brief member function to review rules for acceptable modifiers, duplicate or ambiguous rules and return an array containing possible warnings. + * 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 Rule[] + * @return a string[] containing possible warnings for a rule + */ + public reviewRules(rule: Rule[], index: number): string[] { + + const keylayoutKmnConverter = new KeylayoutToKmnConverter(this.callbacks, this.options); + const warningText: string[] = Array(3).fill(""); + + // ------------------------- check unavailable modifiers ------------------------- + + if ((rule[index].ruleType === "C0") || (rule[index].ruleType === "C1")) { + if (!keylayoutKmnConverter.isAcceptableKeymanModifier(rule[index].modifierKey)) { + warningText[2] = "unavailable modifier : "; + } + } + + else if (rule[index].ruleType === "C2") { + if (!keylayoutKmnConverter.isAcceptableKeymanModifier(rule[index].modifierDeadkey)) { + warningText[1] = "unavailable modifier : "; + warningText[2] = "unavailable superior rule ( [" + + rule[index].modifierDeadkey + " " + + rule[index].deadkey + + "] > dk(A" + + rule[index].idDeadkey + + ") ) : "; + } + if (!keylayoutKmnConverter.isAcceptableKeymanModifier(rule[index].modifierKey)) { + warningText[2] = "unavailable modifier : "; + } + } + + else if (rule[index].ruleType === "C3") { + + if (!keylayoutKmnConverter.isAcceptableKeymanModifier(rule[index].modifierPrevDeadkey)) { + warningText[0] = "unavailable modifier : "; + warningText[1] = "unavailable superior rule ( [" + + rule[index].modifierPrevDeadkey + " " + + rule[index].prevDeadkey + + "] > dk(A" + + rule[index].idPrevDeadkey + + ") ) : "; + } + + if (!keylayoutKmnConverter.isAcceptableKeymanModifier(rule[index].modifierDeadkey)) { + warningText[1] = "unavailable modifier : "; + warningText[2] = "unavailable superior rule ( [" + + rule[index].modifierDeadkey + " " + + rule[index].deadkey + + "] > dk(B" + + rule[index].idDeadkey + + ") ) : "; + } + + if (!keylayoutKmnConverter.isAcceptableKeymanModifier(rule[index].modifierKey)) { + warningText[2] = "unavailable modifier : "; + } + } + + // ------------------------- check ambiguous/duplicate rules ------------------------- + + if ((rule[index].ruleType === "C0") || (rule[index].ruleType === "C1")) { + + // 1-1: + [CAPS K_N] > 'N' <-> + [CAPS K_N] > 'A' + const amb_1_1 = rule.filter((curr, idx) => + (curr.ruleType === "C0" || curr.ruleType === "C1") + && curr.modifierPrevDeadkey === "" + && curr.prevDeadkey === "" + && curr.modifierDeadkey === "" + && curr.deadkey === "" + && curr.modifierKey === rule[index].modifierKey + && curr.key === rule[index].key + && new TextDecoder().decode(curr.output) !== new TextDecoder().decode(rule[index].output) + && idx < index + ); + // 1-1: + [CAPS K_N] > 'N' <-> + [CAPS K_N] > 'N' + const dup_1_1 = rule.filter((curr, idx) => + (curr.ruleType === "C0" || curr.ruleType === "C1") + && curr.modifierPrevDeadkey === "" + && curr.prevDeadkey === "" + && curr.modifierDeadkey === "" + && curr.deadkey === "" + && curr.modifierKey === rule[index].modifierKey + && curr.key === rule[index].key + && new TextDecoder().decode(curr.output) === new TextDecoder().decode(rule[index].output) + && idx < index + ); + + // 4-1: + [CAPS K_N] > dk(C11) <-> + [CAPS K_N] > 'Ñ' + const amb_4_1 = rule.filter((curr, idx) => + ((curr.ruleType === "C3")) + && curr.modifierPrevDeadkey === rule[index].modifierKey + && curr.prevDeadkey === rule[index].key + ); + + // 2-1: + [CAPS K_N] > dk(C11) <-> + [CAPS K_N] > 'Ñ' + const amb_2_1 = rule.filter((curr, idx) => + ((curr.ruleType === "C2")) + && curr.modifierDeadkey === rule[index].modifierKey + && curr.deadkey === rule[index].key + ); + + if (amb_4_1.length > 0) { + warningText[2] = warningText[2] + + ("ambiguous rule: later: [" + + amb_4_1[0].modifierPrevDeadkey + + " " + + amb_4_1[0].prevDeadkey + + "] > dk(C" + + amb_2_1[0].idDeadkey + + ") "); + } + + if (amb_2_1.length > 0) { + warningText[2] = warningText[2] + + ("ambiguous rule: later: [" + + amb_2_1[0].modifierDeadkey + + " " + + amb_2_1[0].deadkey + + "] > dk(A" + + amb_2_1[0].idDeadkey + + ") "); + } + + if (amb_1_1.length > 0) { + warningText[2] = warningText[2] + + ("ambiguous rule: earlier: [" + + amb_1_1[0].modifierKey + + " " + + amb_1_1[0].key + + "] > \'" + + this.writeCharacterOrUnicode(new TextDecoder().decode(amb_1_1[0].output)).character + + "\' "); + } + + if (dup_1_1.length > 0) { + warningText[2] = warningText[2] + + ("duplicate rule: earlier: [" + + dup_1_1[0].modifierKey + + " " + + dup_1_1[0].key + + "] > \'" + + this.writeCharacterOrUnicode(new TextDecoder().decode(dup_1_1[0].output)).character + + "\' "); + } + } + + if (rule[index].ruleType === "C2") { + + // 2-2: + [CAPS K_N] > dk(C11) <-> + [CAPS K_N] > dk(C3) + const amb_2_2 = rule.filter((curr, idx) => + curr.ruleType === "C2" + && curr.modifierDeadkey === rule[index].modifierDeadkey + && curr.deadkey === rule[index].deadkey + && curr.idDeadkey !== rule[index].idDeadkey + && idx < index + ); + + // 2-2: + [CAPS K_N] > dk(C11) <-> + [CAPS K_N] > dk(C11) + const dup_2_2 = rule.filter((curr, idx) => + curr.ruleType === "C2" + && curr.modifierDeadkey === rule[index].modifierDeadkey + && curr.deadkey === rule[index].deadkey + && curr.idDeadkey === rule[index].idDeadkey + && idx < index + ); + + //3-3: dk(C11) + [SHIFT CAPS K_A] > 'Ã' <-> dk(C11) + [SHIFT CAPS K_A] > 'B' + const amb_3_3 = rule.filter((curr, idx) => + (curr.ruleType === "C2") + && curr.idDeadkey === rule[index].idDeadkey + && curr.modifierKey === rule[index].modifierKey + && curr.key === rule[index].key + && new TextDecoder().decode(curr.output) !== new TextDecoder().decode(rule[index].output) + && idx < index + ); + + //3-3: dk(C11) + [SHIFT CAPS K_A] > 'Ã' <-> dk(C11) + [SHIFT CAPS K_A] > 'Ã' + const dup_3_3 = rule.filter((curr, idx) => + (curr.ruleType === "C2") + && curr.idDeadkey === rule[index].idDeadkey + && rule[index].uniqueDeadkey === 0 + && curr.modifierKey === rule[index].modifierKey + && curr.key === rule[index].key + && new TextDecoder().decode(curr.output) === new TextDecoder().decode(rule[index].output) + && idx < index + ); + + // 4-2: + [CAPS K_N] > dk(C11) <-> + [CAPS K_N] > dk(B11) + const amb_4_2 = rule.filter((curr, idx) => + ((curr.ruleType === "C3")) + && curr.modifierPrevDeadkey === rule[index].modifierDeadkey + && curr.prevDeadkey === rule[index].deadkey + && curr.idPrevDeadkey === rule[index].idDeadkey + ); + + if (amb_2_2.length > 0) { + warningText[1] = warningText[1] + + ("ambiguous rule: earlier: [" + + amb_2_2[0].modifierDeadkey + + " " + + amb_2_2[0].deadkey + + "] > dk(C" + + amb_2_2[0].idDeadkey + + ") "); + } + + if (dup_2_2.length > 0) { + warningText[1] = warningText[1] + + ("duplicate rule: earlier: [" + + dup_2_2[0].modifierDeadkey + + " " + + dup_2_2[0].deadkey + + "] > dk(C" + + dup_2_2[0].idDeadkey + + ") "); + } + + if (amb_3_3.length > 0) { + warningText[2] = warningText[2] + + ("ambiguous rule: earlier: dk(A" + + amb_3_3[0].idDeadkey + + ") + [" + + amb_3_3[0].modifierKey + + " " + + amb_3_3[0].key + + "] > \'" + + this.writeCharacterOrUnicode(new TextDecoder().decode(amb_3_3[0].output)).character + + "\' "); + } + + if (dup_3_3.length > 0) { + warningText[2] = warningText[2] + + ("duplicate rule: earlier: dk(A" + + dup_3_3[0].idDeadkey + + ") + [" + + dup_3_3[0].modifierKey + + " " + + dup_3_3[0].key + + "] > \'" + + this.writeCharacterOrUnicode(new TextDecoder().decode(dup_3_3[0].output)).character + + "\' "); + } + + if (amb_4_2.length > 0) { + warningText[0] = warningText[0] + + ("ambiguous rule: later: [" + + amb_4_2[0].modifierPrevDeadkey + + " " + + amb_4_2[0].prevDeadkey + + "] > dk(C" + + amb_4_2[0].idPrevDeadkey + + ") "); + } + } + + if (rule[index].ruleType === "C3") { + + // 2-4 + [CAPS K_N] > dk(C11) <-> + [CAPS K_N] > dk(B11) + const amb_2_4 = rule.filter((curr, idx) => + ((curr.ruleType === "C2")) + && curr.modifierDeadkey === rule[index].modifierPrevDeadkey + && curr.deadkey === rule[index].prevDeadkey + && curr.idDeadkey === rule[index].idPrevDeadkey + ); + + // 6-3 dk(C11) + [SHIFT CAPS K_A] > 'Ã' <-> dk(C11) + [SHIFT CAPS K_A] > 'B' + const amb_6_3 = rule.filter((curr, idx) => + (curr.ruleType === "C2") + && curr.idPrevDeadkey === rule[index].idPrevDeadkey + && curr.modifierKey === rule[index].modifierKey + && curr.key === rule[index].key + && (new TextDecoder().decode(curr.output) !== new TextDecoder().decode(rule[index].output)) + ); + + // 6-3 dk(C11) + [SHIFT CAPS K_A] > 'Ã' <-> dk(C11) + [SHIFT CAPS K_A] > 'Ã' + const dup_6_3 = rule.filter((curr, idx) => + (curr.ruleType === "C2") + && curr.idPrevDeadkey === rule[index].idPrevDeadkey + && curr.modifierKey === rule[index].modifierKey + && curr.key === rule[index].key + && new TextDecoder().decode(curr.output) === new TextDecoder().decode(rule[index].output) + ); + + // 4-4 + [CAPS K_N] > dk(C11) <-> + [CAPS K_N] > dk(C1) + const amb_4_4 = rule.filter((curr, idx) => + curr.ruleType === "C3" + && curr.modifierPrevDeadkey === rule[index].modifierPrevDeadkey + && curr.idPrevDeadkey !== rule[index].idPrevDeadkey + && curr.prevDeadkey === rule[index].prevDeadkey + && rule[index].uniquePrevDeadkey !== 0 + && idx < index + ); + + // 4-4 + [CAPS K_N] > dk(C11) <-> + [CAPS K_N] > dk(C11) + const dup_4_4 = rule.filter((curr, idx) => + curr.ruleType === "C3" + && curr.modifierPrevDeadkey === rule[index].modifierPrevDeadkey + && curr.prevDeadkey === rule[index].prevDeadkey + && curr.idPrevDeadkey === rule[index].idPrevDeadkey + && idx < index + ); + + // 5-5 dk(C1) + [SHIFT CAPS K_A] > dk(C2) <-> dk(C1) + [SHIFT CAPS K_A] > dk(C3) + const amb_5_5 = rule.filter((curr, idx) => ( + (curr.ruleType === "C3") + && curr.idPrevDeadkey === rule[index].idPrevDeadkey + && curr.modifierDeadkey === rule[index].modifierDeadkey + && curr.deadkey === rule[index].deadkey + && curr.idDeadkey === rule[index].idDeadkey) + && idx < index + && (rule[index].uniqueDeadkey !== 0 || rule[index].uniquePrevDeadkey !== 0) + ); + + // 5-5 dk(C1) + [SHIFT CAPS K_A] > dk(C2) <-> dk(C1) + [SHIFT CAPS K_A] > dk(C2) + const dup_5_5 = rule.filter((curr, idx) => + (curr.ruleType === "C3") + && curr.idPrevDeadkey === rule[index].idPrevDeadkey + && curr.modifierPrevDeadkey === rule[index].modifierPrevDeadkey + && curr.prevDeadkey === rule[index].prevDeadkey + && curr.modifierDeadkey === rule[index].modifierDeadkey + && curr.deadkey === rule[index].deadkey + && curr.idDeadkey === rule[index].idDeadkey + && rule[index].uniqueDeadkey === 0 + && idx < index + ); + + // 6-6 dk(C11) + [SHIFT CAPS K_A] > 'Ã' <-> dk(C11) + [SHIFT CAPS K_A] > 'B' + const amb_6_6 = rule.filter((curr, idx) => + (curr.ruleType === "C3") + && curr.idPrevDeadkey === rule[index].idPrevDeadkey + && curr.modifierKey === rule[index].modifierKey + && curr.key === rule[index].key + && (new TextDecoder().decode(curr.output) !== new TextDecoder().decode(rule[index].output)) + && idx < index + ); + + // 6-6 dk(C11) + [SHIFT CAPS K_A] > 'Ã' <-> dk(C11) + [SHIFT CAPS K_A] > 'Ã' + const dup_6_6 = + rule.filter((curr, idx) => + (curr.ruleType === "C3") + && curr.idDeadkey === rule[index].idDeadkey + && curr.modifierKey === rule[index].modifierKey + && curr.key === rule[index].key + && (new TextDecoder().decode(curr.output) === new TextDecoder().decode(rule[index].output)) + && idx < index + ); + + if (amb_2_4.length > 0) { + warningText[0] = warningText[0] + + ("ambiguous rule: earlier: [" + + amb_2_4[0].modifierDeadkey + + " " + + amb_2_4[0].deadkey + + "] > dk(A" + + amb_2_4[0].idDeadkey + + ") "); + } + + if (amb_6_3.length > 0) { + warningText[1] = warningText[1] + + ("ambiguous rule: earlier: dk(C" + + amb_6_3[0].idDeadkey + + ") + [" + + amb_6_3[0].modifierKey + + " " + + amb_6_3[0].key + + "] > \'" + + this.writeCharacterOrUnicode(new TextDecoder().decode(amb_6_3[0].output)).character + + "\' "); + } + + if (dup_6_3.length > 0) { + warningText[1] = warningText[1] + + ("duplicate rule: earlier: dk(C" + + dup_6_3[0].idDeadkey + + ") + [" + + dup_6_3[0].modifierKey + + " " + + dup_6_3[0].key + + "] > \'" + + this.writeCharacterOrUnicode(new TextDecoder().decode(dup_6_3[0].output)).character + + "\' "); + } + + if (amb_4_4.length > 0) { + warningText[0] = warningText[0] + + ("ambiguous rule: earlier: [" + + amb_4_4[0].modifierPrevDeadkey + + " " + + amb_4_4[0].prevDeadkey + + "] > dk(C" + + amb_4_4[0].idPrevDeadkey + + ") "); + } + + if (dup_4_4.length > 0) { + warningText[0] = warningText[0] + + ("duplicate rule: earlier: [" + + dup_4_4[0].modifierPrevDeadkey + + " " + + dup_4_4[0].prevDeadkey + + "] > dk(C" + + dup_4_4[0].idPrevDeadkey + + ") "); + } + + if (amb_5_5.length > 0) { + warningText[1] = warningText[1] + + ("ambiguous rule: earlier: dk(B" + + amb_5_5[0].idPrevDeadkey + + ") + [" + + amb_5_5[0].modifierDeadkey + + " " + + amb_5_5[0].deadkey + + "] > dk(B" + + amb_5_5[0].idDeadkey + + ") "); + } + + if (dup_5_5.length > 0) { + warningText[1] = warningText[1] + + ("duplicate rule: earlier: dk(B" + + dup_5_5[0].idPrevDeadkey + + ") + [" + + dup_5_5[0].modifierDeadkey + + " " + + dup_5_5[0].deadkey + + "] > dk(B" + + dup_5_5[0].idDeadkey + + ") "); + } + + if (amb_6_6.length > 0) { + warningText[2] = warningText[2] + + ("ambiguous rule: earlier: dk(B" + + amb_6_6[0].idDeadkey + + ") + [" + + amb_6_6[0].modifierKey + + " " + + amb_6_6[0].key + + "] > \'" + + this.writeCharacterOrUnicode(new TextDecoder().decode(amb_6_6[0].output)).character + + "\' "); + } + + if (dup_6_6.length > 0) { + warningText[2] = warningText[2] + + ("duplicate rule: earlier: dk(B" + + dup_6_6[0].idDeadkey + + ") + [" + + dup_6_6[0].modifierKey + + " " + + dup_6_6[0].key + + "] > \'" + + this.writeCharacterOrUnicode(new TextDecoder().decode(dup_6_6[0].output)).character + + "\' "); + } + } + // In rare cases a rule might not be written out therefore we need to inform the user: + // usually we write the first occurance of an ambiguous C0/C1 rule and comment out the later + // assuming that if several C0/C1 rules are ambiguous the user prefers to use the first C0/C1 rule + // for C2/C3 rules we write the last occurance of an ambiguous rule and comment out the earlier + // assuming that if a C0/C1 and a C2/C3 rule is ambiguous the user prefers to use the C2/C3 rule over the C0/C1 rule + // if both happens, nothing would be written, therefore this messsage + + const extraWarning = "PLEASE CHECK THE FOLLOWING RULE AS IT WILL NOT BE WRITTEN ! "; + + if (warningText[0] !== "") { + warningText[0] = "c WARNING: " + warningText[0] + "here: "; + + if ((warningText[0].indexOf("earlier:") > 0) && (warningText[0].indexOf("later:") > 0)) { + warningText[0] = warningText[0] + extraWarning; + } + } + if (warningText[1] !== "") { + warningText[1] = "c WARNING: " + warningText[1] + "here: "; + + if ((warningText[1].indexOf("earlier:") > 0) && (warningText[1].indexOf("later:") > 0)) { + warningText[1] = warningText[1] + extraWarning; + } + } + + if (warningText[2] !== "") { + warningText[2] = "c WARNING: " + warningText[2] + "here: "; + + if ((warningText[2].indexOf("earlier:") > 0) && (warningText[2].indexOf("later:") > 0)) { + warningText[2] = warningText[2] + extraWarning; + } + } + return warningText; + } + + /** + * @brief member function to write a character as Unicode Character or Unicode Codepoint depending on the character that is to be written + * @param ctr : string - the character to be written + * @return a string containing the Unicode representation of the control character. + * A control character will be written as unicode (U+0004), + * a non-control character will be written as itself ( 'A', '1', '፩', '😎') + * null in case of an empty string or null or undefined input + */ + public writeCharacterOrUnicode(ctr: string, msg: string = ""): messageCharacter { + + if ((ctr === null) || (ctr === undefined) || (ctr.length === 0)) { + return null; + } + + let versionOutputCharacter; + const out: messageCharacter = { + message: msg, + character: ctr + }; + + const m_uni = /^U\+([0-9a-f]{1,6})$/i.exec(ctr); + const m_hex = /^&#x([0-9a-f]{1,6});$/i.exec(ctr); + const m_dec = /^&#([0-9]{1,7});$/.exec(ctr); + + // find the value of output character which may be specified in unicode, html hex or html dec format ( e.g. U+1234 -> 1234; ሴ -> 1234; ሴ -> 1234) + const ctr_val = ((m_uni || m_hex || m_dec) ? + m_uni ? parseInt(m_uni[1], 16) : m_hex ? parseInt(m_hex[1], 16) : parseInt(m_dec[1], 10) : KeylayoutToKmnConverter.MAX_CTRL_CHARACTER + ); + + // for control charactersin 'U+...', '&#x...' or '&#...' format as well as in "" format + if ((ctr_val < KeylayoutToKmnConverter.MAX_CTRL_CHARACTER) || (ctr.charCodeAt(0) < KeylayoutToKmnConverter.MAX_CTRL_CHARACTER)) { + + // for control characters in 'U+...', '&#x...' or '&#...' format + if (ctr_val < KeylayoutToKmnConverter.MAX_CTRL_CHARACTER) { + versionOutputCharacter = "U+" + ctr_val.toString(16).toUpperCase().padStart(4, '0'); + } + // for control characters in "" format + if (ctr.charCodeAt(0) < KeylayoutToKmnConverter.MAX_CTRL_CHARACTER) { + versionOutputCharacter = "U+" + ctr.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0'); + } + if (versionOutputCharacter) + out.character = versionOutputCharacter; + + // add a warning message + if (msg == "") { + out.message = "c WARNING: use of a control character "; + } + else { + out.message = msg + "; Use of a control character "; + } + } + else { + out.character = this.convertToUnicodeCharacter(ctr);; + } + return out; + } + + // TODO: move to util in PR 14569 + /** + * @brief function to convert a numeric character reference or a unicode value to a unicode character e.g. c -> c; U+1F60E -> 😎 + * @param inputString the value that will converted + * @return a unicode character like 'c', 'ሴ', '😎' or undefined if inputString is not recognized + */ + public convertToUnicodeCharacter_old(inputString: string): string { + + + // null, undefined will later be refused for conversion + if (inputString == null || inputString == undefined) { + return undefined; + } + + // U+ followed by 1.-6. hex digits will later be used for conversion + const m_uni = /^U\+([0-9a-f]{1,6})$/i.exec(inputString); + + // invalid U+ ( U+ followed by anything) will later be refused for conversion + const m_uni_inv = /^(U\+)+(.?)+$/i.exec(inputString); + + // &#x followed by 1.-6. hex digits will later be used for conversion + const m_hex = /^&#x([0-9a-f]{1,6});$/i.exec(inputString); + + // &# followed by 1.-6. decimal digits will later be used for conversion + const m_dec = /^&#([0-9]{1,7});$/.exec(inputString); + + // & followed by gt, lt, quot, amp, apos will later be used for conversion + const m_nam = /^&(gt|lt|quot|amp|apos);$/i.exec(inputString); + + // &# followed by anything will later be refused for conversion + const m_html_inv = /^(&#)+(.?)+$/i.exec(inputString); + + // one or more characters except starting with U+ or & will later be used for conversion + const m_chr = /^(?!U\+|&).+$/i.exec(inputString); + + // '&', '&#','&#x', or 'U+' with or without ; will later be refused for conversion + const m_chr_inv = /^((&;?)+|(&#;?)+|(&#x;?)+|(U\+)+;?)$|^$/i.exec(inputString); + + // valid 'U+xxxx' + if (m_uni) { + const codePoint_u = parseInt(m_uni[1], 16); + // Reject surrogates and invalid codepoints + if ((codePoint_u >= 0xD800 && codePoint_u <= 0xDFFF) || codePoint_u > 0x10FFFF) { + return undefined; + } + return String.fromCodePoint(codePoint_u); + } + + // invalid 'U+xxxx' + else if (m_uni_inv) { + return undefined; + } + + // valid '&#x...' + else if (m_hex) { + const codePoint_h = parseInt(m_hex[1], 16); + // Reject surrogates and invalid codepoints + if ((codePoint_h >= 0xD800 && codePoint_h <= 0xDFFF) || codePoint_h > 0x10FFFF) { + return undefined; + } + return String.fromCodePoint(codePoint_h); + } + // valid '&#...' + else if (m_dec) { + const codePoint_d = parseInt(m_dec[1], 10); + // Reject surrogates and invalid codepoints + if ((codePoint_d >= 0xD800 && codePoint_d <= 0xDFFF) || codePoint_d > 0x10FFFF) { + return undefined; + } + return String.fromCodePoint(codePoint_d); + } + // valid '>', '<',.. + else if (m_nam) { + switch (m_nam[1].toLowerCase()) { + case 'gt': return '>'; + case 'lt': return '<'; + case 'quot': return '"'; + case 'amp': return '&'; + case 'apos': return "'"; + default: return undefined; + } + } + // invalid '&...' + else if (m_html_inv) { + return undefined; + } + + // single 'U+', '&', '' + else if (m_chr_inv) { + return inputString; + } + + // if no matches so far, check for one or more characters ('a','ab', 'ẘ','😎', '😎😎', ) + else if (m_chr) { + return inputString; + } + return undefined; + } + public convertToUnicodeCharacter(inputString: string): string { + + + // null, undefined will later be refused for conversion + if (inputString == null || inputString == undefined) { + return undefined; + } + + // &#x followed by 1.-6. hex digits will later be used for conversion + const m_hex = /^&#x([0-9a-f]{1,6});$/i.exec(inputString); + + // &# followed by 1.-6. decimal digits will later be used for conversion + const m_dec = /^&#([0-9]{1,7});$/.exec(inputString); + + // & followed by gt, lt, quot, amp, apos will later be used for conversion + const m_nam = /^&(gt|lt|quot|amp|apos);$/i.exec(inputString); + + // &# followed by anything will later be refused for conversion + const m_html_inv = /^(&#)+(.?)+$/i.exec(inputString); + + // one or more characters except starting with & will later be used for conversion + const m_chr = /^(?!&).+$/i.exec(inputString); + + // '&', '&#','&#x' with or without ; will later be refused for conversion + const m_chr_inv = /^((&;?)+|(&#;?)+|(&#x;?)+;?)$|^$/i.exec(inputString); + + // valid '&#x...' + if (m_hex) { + const codePoint_h = parseInt(m_hex[1], 16); + // Reject surrogates and invalid codepoints + if ((codePoint_h >= 0xD800 && codePoint_h <= 0xDFFF) || codePoint_h > 0x10FFFF) { + return undefined; + } + return String.fromCodePoint(codePoint_h); + } + // valid '&#...' + else if (m_dec) { + const codePoint_d = parseInt(m_dec[1], 10); + // Reject surrogates and invalid codepoints + if ((codePoint_d >= 0xD800 && codePoint_d <= 0xDFFF) || codePoint_d > 0x10FFFF) { + return undefined; + } + return String.fromCodePoint(codePoint_d); + } + // valid '>', '<',.. + else if (m_nam) { + switch (m_nam[1].toLowerCase()) { + case 'gt': return '>'; + case 'lt': return '<'; + case 'quot': return '"'; + case 'amp': return '&'; + case 'apos': return "'"; + default: return undefined; + } + } + // invalid '&...' + else if (m_html_inv) { + return undefined; + } + + // single '&', '' + else if (m_chr_inv) { + return inputString; + } + + // if no matches so far, check for one or more characters ('a','ab', 'ẘ','😎', '😎😎', ) + else if (m_chr) { + return inputString; + } + return undefined; + } +} diff --git a/developer/src/kmc-convert/src/main.ts b/developer/src/kmc-convert/src/main.ts index 4719c0a6c90..11b56fccc9c 100644 --- a/developer/src/kmc-convert/src/main.ts +++ b/developer/src/kmc-convert/src/main.ts @@ -1,5 +1,5 @@ /* - * Keyman is copyright (C) SIL International. MIT License. + * Keyman is copyright (C) SIL Global. MIT License. * * Keyboard source file conversion tools */ diff --git a/developer/src/kmc-convert/test/test-converter-messages.ts b/developer/src/kmc-convert/test/converter-messages.tests.ts similarity index 88% rename from developer/src/kmc-convert/test/test-converter-messages.ts rename to developer/src/kmc-convert/test/converter-messages.tests.ts index 3519ddbb10a..94c2ca0dee4 100644 --- a/developer/src/kmc-convert/test/test-converter-messages.ts +++ b/developer/src/kmc-convert/test/converter-messages.tests.ts @@ -1,5 +1,5 @@ /* - * Keyman is copyright (C) SIL International. MIT License. + * Keyman is copyright (C) SIL Global. MIT License. */ import 'mocha'; import { ConverterMessages } from '../src/converter-messages.js'; diff --git a/developer/src/kmc-convert/test/converter.tests.ts b/developer/src/kmc-convert/test/converter.tests.ts new file mode 100644 index 00000000000..a16c1767394 --- /dev/null +++ b/developer/src/kmc-convert/test/converter.tests.ts @@ -0,0 +1,25 @@ +import 'mocha'; +import { assert } from 'chai'; +import { Converter } from '../src/main.js'; +import { CompilerOptions, } from "@keymanapp/developer-utils"; +import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; + +describe('converter class', function () { + it('should throw on failure', async function () { + const converter = new Converter(); + try { + await converter.init(null, null); + assert.fail('Expected exception'); + } catch (e) { + assert.ok(e); + } + }); + + it('should start', async function () { + const converter = new Converter(); + const callbacks = new TestCompilerCallbacks(); + const options: CompilerOptions = { saveDebug: true, shouldAddCompilerVersion: false }; + assert(await converter.init(callbacks, options)); + }); + +}); diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test copy.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test copy.keylayout new file mode 100644 index 00000000000..76fd91b4db6 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test copy.keylayout @@ -0,0 +1,596 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test.keylayout new file mode 100644 index 00000000000..76fd91b4db6 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test.keylayout @@ -0,0 +1,596 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test.xkb b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test.xkb new file mode 100644 index 00000000000..76fd91b4db6 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test.xkb @@ -0,0 +1,596 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_C0.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_C0.keylayout new file mode 100644 index 00000000000..d8c49ea62ae --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_C0.keylayout @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_C0_C1_C2_C3.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_C0_C1_C2_C3.keylayout new file mode 100644 index 00000000000..f1f3ebab1e5 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_C0_C1_C2_C3.keylayout @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_C1.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_C1.keylayout new file mode 100644 index 00000000000..e785e58ca18 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_C1.keylayout @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_C2.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_C2.keylayout new file mode 100644 index 00000000000..aa6b60997bc --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_C2.keylayout @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_C2_several.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_C2_several.keylayout new file mode 100644 index 00000000000..d51369c934c --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_C2_several.keylayout @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_C3.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_C3.keylayout new file mode 100644 index 00000000000..6275a230bbf --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_C3.keylayout @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_C3_several.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_C3_several.keylayout new file mode 100644 index 00000000000..7fff380f2cc --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_C3_several.keylayout @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_ExtraWarning.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_ExtraWarning.keylayout new file mode 100644 index 00000000000..0d36b2a5fea --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_ExtraWarning.keylayout @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_ambiguous_keys.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_ambiguous_keys.keylayout new file mode 100644 index 00000000000..97778e876ee --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_ambiguous_keys.keylayout @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_characters.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_characters.keylayout new file mode 100644 index 00000000000..2aae8f1bb46 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_characters.keylayout @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_differentAmountOfKeysInBehaviours.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_differentAmountOfKeysInBehaviours.keylayout new file mode 100644 index 00000000000..4b770866cd6 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_differentAmountOfKeysInBehaviours.keylayout @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_differentEncodings.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_differentEncodings.keylayout new file mode 100644 index 00000000000..c526f8c2af8 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_differentEncodings.keylayout @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_duplicate_keys.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_duplicate_keys.keylayout new file mode 100644 index 00000000000..a5cb09069bf --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_duplicate_keys.keylayout @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_duplicate_missing_keycode.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_duplicate_missing_keycode.keylayout new file mode 100644 index 00000000000..39da3300f4f --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_duplicate_missing_keycode.keylayout @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_duplicate_missing_keys.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_duplicate_missing_keys.keylayout new file mode 100644 index 00000000000..b3670561432 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_duplicate_missing_keys.keylayout @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_maxKeyCode.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_maxKeyCode.keylayout new file mode 100644 index 00000000000..a5f9e178b99 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_maxKeyCode.keylayout @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_messages.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_messages.keylayout new file mode 100644 index 00000000000..5d8d1b86820 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_messages.keylayout @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_messages_controlCharacter.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_messages_controlCharacter.keylayout new file mode 100644 index 00000000000..f97abb984d2 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_messages_controlCharacter.keylayout @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_messages_superior_C2.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_messages_superior_C2.keylayout new file mode 100644 index 00000000000..3416bb5e8ff --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_messages_superior_C2.keylayout @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_messages_superior_C3.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_messages_superior_C3.keylayout new file mode 100644 index 00000000000..f95afe40db5 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_messages_superior_C3.keylayout @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_modifier.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_modifier.keylayout new file mode 100644 index 00000000000..67ccf4db63e --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_modifier.keylayout @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_modifierNoCaps.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_modifierNoCaps.keylayout new file mode 100644 index 00000000000..9656413d7e6 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_modifierNoCaps.keylayout @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_moreKeyMapThanKeyMapselectAndJisERROR.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_moreKeyMapThanKeyMapselectAndJisERROR.keylayout new file mode 100644 index 00000000000..13be153d756 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_moreKeyMapThanKeyMapselectAndJisERROR.keylayout @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_moreKeyMapThanKeyMapselectERROR.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_moreKeyMapThanKeyMapselectERROR.keylayout new file mode 100644 index 00000000000..3bd9b5ba491 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_moreKeyMapThanKeyMapselectERROR.keylayout @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_moreKeymapSelectThanKeymapERROR.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_moreKeymapSelectThanKeymapERROR.keylayout new file mode 100644 index 00000000000..25d178237cb --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_moreKeymapSelectThanKeymapERROR.keylayout @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_nr_elements.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_nr_elements.keylayout new file mode 100644 index 00000000000..7a32f4b3c25 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_nr_elements.keylayout @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_onlyOneKeymap.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_onlyOneKeymap.keylayout new file mode 100644 index 00000000000..8f173994576 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_onlyOneKeymap.keylayout @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_undefinedAction.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_undefinedAction.keylayout new file mode 100644 index 00000000000..4e6f0ce8023 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_undefinedAction.keylayout @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_unsupportedCharacters.keylayout b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_unsupportedCharacters.keylayout new file mode 100644 index 00000000000..d393b332ee6 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/Test_unsupportedCharacters.keylayout @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-keylayout-kmn/keylayout.dtd b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/keylayout.dtd new file mode 100644 index 00000000000..a5c75267811 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-keylayout-kmn/keylayout.dtd @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-xkb-kmn/Test.keylayout b/developer/src/kmc-convert/test/data/tests-xkb-kmn/Test.keylayout new file mode 100644 index 00000000000..76fd91b4db6 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-xkb-kmn/Test.keylayout @@ -0,0 +1,596 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-xkb-kmn/Test.xkb b/developer/src/kmc-convert/test/data/tests-xkb-kmn/Test.xkb new file mode 100644 index 00000000000..76fd91b4db6 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-xkb-kmn/Test.xkb @@ -0,0 +1,596 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-convert/test/data/tests-xkb-kmn/de b/developer/src/kmc-convert/test/data/tests-xkb-kmn/de new file mode 100644 index 00000000000..0c69ecc0764 --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-xkb-kmn/de @@ -0,0 +1,1202 @@ +default xkb_symbols "basic" { + + include "latin(type4)" + + name[Group1]="German"; + + key { [ 2, quotedbl, twosuperior, oneeighth ] }; + key { [ 3, section, threesuperior, sterling ] }; + key { [ 4, dollar, onequarter, currency ] }; + + key {type[Group1]="FOUR_LEVEL_PLUS_LOCK", symbols[Group1]= + [ssharp, question, backslash, questiondown, 0x1001E9E ]}; + key { [dead_acute, dead_grave, dead_cedilla, dead_ogonek ] }; + + key { [ e, E, EuroSign, EuroSign ] }; + key { [ z, Z, leftarrow, yen ] }; + key { [udiaeresis, Udiaeresis, dead_diaeresis, dead_abovering ] }; + key { [ plus, asterisk, asciitilde, macron ] }; + + key { [ s, S, U017F, U1E9E ] }; + key { [ j, J, dead_belowdot, dead_abovedot ] }; + key { [odiaeresis, Odiaeresis, dead_doubleacute, dead_belowdot ] }; + key { [adiaeresis, Adiaeresis, dead_circumflex, dead_caron ] }; + key { [dead_circumflex, degree, U2032, U2033 ] }; + + key { [numbersign, apostrophe, rightsinglequotemark, dead_breve ] }; + key { [ y, Y, guillemotright, U203A ] }; + key { [ x, X, guillemotleft, U2039 ] }; + key { [ v, V, doublelowquotemark, singlelowquotemark ] }; + key { [ b, B, leftdoublequotemark, leftsinglequotemark ] }; + key { [ n, N, rightdoublequotemark, rightsinglequotemark ] }; + key { [ comma, semicolon, periodcentered, multiply ] }; + key { [ period, colon, U2026, division ] }; + key { [ minus, underscore, endash, emdash ] }; + key { [ less, greater, bar, dead_belowmacron ] }; + + include "kpdl(comma)" + + include "level3(ralt_switch)" +}; + +partial alphanumeric_keys +xkb_symbols "deadtilde" { + // previous standard German layout with tilde as dead key + + include "de(basic)" + name[Group1]="German (dead tilde)"; + + key { [ plus, asterisk, dead_tilde, dead_macron ] }; +}; + +partial alphanumeric_keys +xkb_symbols "nodeadkeys" { + + // modify the basic German layout to not have any dead keys + + include "de(basic)" + name[Group1]="German (no dead keys)"; + + key { [asciicircum, degree, notsign, notsign ] }; + key { [ acute, grave, cedilla, cedilla ] }; + key { [ udiaeresis, Udiaeresis, diaeresis, diaeresis ] }; + key { [ plus, asterisk, asciitilde, macron ] }; + key { [ odiaeresis, Odiaeresis, doubleacute, doubleacute ] }; + key { [ adiaeresis, Adiaeresis, asciicircum, asciicircum ] }; + key { [ numbersign, apostrophe, rightsinglequotemark, grave ] }; +}; + +partial alphanumeric_keys +xkb_symbols "deadgraveacute" { + // modify the basic German layout to have only acute and grave + // as dead keys (tilde and circumflex are needed as spacing characters + // in many programming languages) + + include "de(basic)" + name[Group1]="German (dead grave acute)"; + + key { [asciicircum, degree, notsign, notsign ] }; + key { [ plus, asterisk, asciitilde, dead_macron ] }; + key { [ numbersign, apostrophe, rightsinglequotemark, grave ] }; +}; + +partial alphanumeric_keys +xkb_symbols "deadacute" { + // modify the basic German layout to have only acute as + // dead keys (ASCII grave, tilde and circumflex are needed as + // spacing characters in many programming languages and text formatters) + + include "de(deadgraveacute)" + + name[Group1]="German (dead acute)"; + + key { [dead_acute, grave, dead_cedilla, dead_ogonek ] }; + key { [numbersign, apostrophe, rightsinglequotemark, dead_grave ] }; +}; + +partial alphanumeric_keys +xkb_symbols "e1" { + // German extended layout E1 based on DIN 2137-1:2020-11 + // Designed for a 105-key keyboard + // https://de.wikipedia.org/wiki/Tastaturbelegung + + name[Group1]="German (E1)"; + + // first row + key.type[Group1] = "EIGHT_LEVEL"; + key { [ dead_circumflex, degree, multiply, NoSymbol, NoSymbol, NoSymbol, NoSymbol, NoSymbol ] }; + key { [ 1, exclam, rightsinglequotemark, NoSymbol, onequarter, U25CA, NoSymbol, NoSymbol ] }; + key { [ 2, quotedbl, twosuperior, NoSymbol, onehalf, U00A6, NoSymbol, NoSymbol ] }; + key { [ 3, section, threesuperior, NoSymbol, threequarters, U00B6, NoSymbol, NoSymbol ] }; + key { [ 4, dollar, emdash, NoSymbol, currency, U2133, NoSymbol, NoSymbol ] }; + key { [ 5, percent, exclamdown, NoSymbol, U2030, U20B0, NoSymbol, NoSymbol ] }; + key { [ 6, ampersand, questiondown, NoSymbol, U2044, U204A, NoSymbol, NoSymbol ] }; + key { [ 7, slash, braceleft, NoSymbol, U2300, U2116, NoSymbol, NoSymbol ] }; + key { [ 8, parenleft, bracketleft, NoSymbol, U27E8, U27EA, NoSymbol, NoSymbol ] }; + key { [ 9, parenright, bracketright, NoSymbol, U27E9, U27EB, NoSymbol, NoSymbol ] }; + key { [ 0, equal, braceright, NoSymbol, division, U2205, NoSymbol, NoSymbol ] }; + key { [ ssharp, question, backslash, NoSymbol, notequal, U00AC, NoSymbol, NoSymbol ] }; + key { [ dead_acute, dead_grave, dead_abovedot, NoSymbol, sterling, U035C, NoSymbol, NoSymbol ] }; + + // second row + key.type[Group1] = "EIGHT_LEVEL_ALPHABETIC"; + key { [ q, Q, at, NoSymbol, masculine, U2642, NoSymbol, NoSymbol ] }; + key { [ w, W, dead_macron, NoSymbol, ordfeminine, U2640, NoSymbol, NoSymbol ] }; + key { [ e, E, EuroSign, NoSymbol, schwa, SCHWA, NoSymbol, NoSymbol ] }; + key { [ r, R, dead_doubleacute, NoSymbol, trademark, registered, NoSymbol, NoSymbol ] }; + key { [ t, T, dead_caron, NoSymbol, thorn, THORN, NoSymbol, NoSymbol ] }; + key { [ z, Z, dead_diaeresis, NoSymbol, U0292, U01B7, NoSymbol, NoSymbol ] }; + key { [ u, U, dead_breve, NoSymbol, rightarrow, leftarrow, NoSymbol, NoSymbol ] }; + key { [ i, I, dead_tilde, NoSymbol, idotless, U26A5, NoSymbol, NoSymbol ] }; + key { [ o, O, dead_abovering, NoSymbol, oslash, Oslash, NoSymbol, NoSymbol ] }; + key { [ p, P, dead_hook, NoSymbol, downarrow, uparrow, NoSymbol, NoSymbol ] }; + key { [ udiaeresis, Udiaeresis, dead_horn, NoSymbol, U2198, U2197, NoSymbol, NoSymbol ] }; + key.type[Group1] = "EIGHT_LEVEL"; + key { [ plus, asterisk, asciitilde, NoSymbol, plusminus, U2052, NoSymbol, NoSymbol ] }; + + // third row + key.type[Group1] = "EIGHT_LEVEL_ALPHABETIC"; + // Per DIN 2137-1:2018-12, p. 11-12, (Alt)Gr+a can either invoke + // a selection possibility for emojis or special characters, or + // output the U+263A smiley. + key { [ a, A, Multi_key, NoSymbol, NoSymbol, NoSymbol, NoSymbol, NoSymbol ] }; + key { [ s, S, seconds, NoSymbol, U017F, U2211, NoSymbol, NoSymbol ] }; + key { [ d, D, minutes, NoSymbol, eth, ETH, NoSymbol, NoSymbol ] }; + key { [ f, F, ISO_Level5_Latch, NoSymbol, NoSymbol, NoSymbol, NoSymbol, NoSymbol ] }; + key { [ g, G, U1E9E, NoSymbol, U02BF, U261B, NoSymbol, NoSymbol ] }; + key { [ h, H, dead_belowmacron, NoSymbol, U02BE, U261A, NoSymbol, NoSymbol ] }; + key { [ j, J, dead_cedilla, NoSymbol, U02B9, U02BA, NoSymbol, NoSymbol ] }; + key { [ k, K, dead_belowcomma, NoSymbol, NoSymbol, NoSymbol, NoSymbol, NoSymbol ] }; + key { [ l, L, dead_ogonek, NoSymbol, lstroke, Lstroke, NoSymbol, NoSymbol ] }; + key { [ odiaeresis, Odiaeresis, dead_belowdot, NoSymbol, oe, OE, NoSymbol, NoSymbol ] }; + key { [ adiaeresis, Adiaeresis, dead_stroke, NoSymbol, ae, AE, NoSymbol, NoSymbol ] }; + key.type[Group1] = "EIGHT_LEVEL"; + key { [ numbersign, apostrophe, U2212, NoSymbol, U2020, U2021, NoSymbol, NoSymbol ] }; + + // fourth row + key.type[Group1] = "EIGHT_LEVEL_ALPHABETIC"; + key { [ y, Y, U203A, NoSymbol, U2423, U23D1, NoSymbol, NoSymbol ] }; + key { [ x, X, guillemotright, NoSymbol, doublelowquotemark, singlelowquotemark, NoSymbol, NoSymbol ] }; + key { [ c, C, U202F, NoSymbol, cent, copyright, NoSymbol, NoSymbol ] }; + key { [ v, V, guillemotleft, NoSymbol, leftdoublequotemark, leftsinglequotemark, NoSymbol, NoSymbol ] }; + key { [ b, B, U2039, NoSymbol, rightdoublequotemark, rightsinglequotemark, NoSymbol, NoSymbol ] }; + key { [ n, N, endash, NoSymbol, eng, ENG, NoSymbol, NoSymbol ] }; + // Per DIN 2137-1:2018-12, p. 12, U+2217 should be replaced by the + // 'middle asterisk' character as soon as it has been added to + // Unicode (see Unicode proposal L2/17-152). + key { [ m, M, mu, NoSymbol, U200C, U2217, NoSymbol, NoSymbol ] }; + key.type[Group1] = "EIGHT_LEVEL"; + key { [ comma, semicolon, U2011, NoSymbol, U02BB, U2661, NoSymbol, NoSymbol ] }; + key { [ period, colon, periodcentered, NoSymbol, ellipsis, U2713, NoSymbol, NoSymbol ] }; + key { [ minus, underscore, hyphen, NoSymbol, U2022, U25E6, NoSymbol, NoSymbol ] }; + + // fifth row + key.type[Group1] = "EIGHT_LEVEL"; + key { [ space, space, nobreakspace, NoSymbol, U200A, U2009, NoSymbol, NoSymbol ] }; + key.type[Group1] = "ONE_LEVEL"; + key { [ Shift_L ] }; + key { [ Shift_R ] }; + key { [ ISO_Level3_Shift ] }; + + // key exists only on the 105-key keyboard + key.type[Group1] = "EIGHT_LEVEL"; + key { [ less, greater, bar, NoSymbol, lessthanequal, greaterthanequal, NoSymbol, NoSymbol ] }; + + include "kpdl(comma)" + include "level3(modifier_mapping)" + include "level5(modifier_mapping)" + }; + +partial alphanumeric_keys +xkb_symbols "e2" { + // German extended layout E2 based on DIN 2137-1:2020-11 + // Designed for a 104-key keyboard + // https://de.wikipedia.org/wiki/Tastaturbelegung + + include "de(e1)" + name[Group1]="German (E2)"; + + // one key less: assign bar, less and greater to other keys + key.type[Group1] = "EIGHT_LEVEL"; + key { [ dead_circumflex, degree, bar, NoSymbol, NoSymbol, NoSymbol, NoSymbol, NoSymbol ] }; + key { [ 2, quotedbl, less, NoSymbol, onehalf, U00A6, NoSymbol, NoSymbol ] }; + key { [ 3, section, greater, NoSymbol, threequarters, U00B6, NoSymbol, NoSymbol ] }; + key { [ plus, asterisk, asciitilde, NoSymbol, multiply, U2052, NoSymbol, NoSymbol ] }; + + // if E2 is used on a 105-key keyboard + key.type[Group1] = "ONE_LEVEL"; + key { [ ISO_Level3_Shift ] }; +}; + +partial alphanumeric_keys +xkb_symbols "T3" { + // German extended layout T3 based on DIN 2137-1:2012-06 + // Now obsolete, use de(e1) or de(e2) + + name[Group1]="German (T3)"; + + key.type[Group1] = "EIGHT_LEVEL"; + key { [ dead_circumflex, degree, multiply, NoSymbol, U204A, hyphen, bar, NoSymbol ] }; + key { [ 1, exclam, rightsinglequotemark, NoSymbol, onesuperior, exclamdown, U02B9, NoSymbol ] }; + key { [ 2, quotedbl, twosuperior, NoSymbol, twosuperior, currency, U02BA, NoSymbol ] }; + key { [ 3, section, threesuperior, NoSymbol, threesuperior, sterling, U02BF, NoSymbol ] }; + key { [ 4, dollar, emdash, NoSymbol, onequarter, 0x20AC, U02BE, NoSymbol ] }; + key { [ 5, percent, exclamdown, NoSymbol, onehalf, uparrow, U02C1, NoSymbol ] }; + key { [ 6, ampersand, questiondown, NoSymbol, threequarters, downarrow, U02C0, NoSymbol ] }; + key { [ 7, slash, braceleft, NoSymbol, oneeighth, leftarrow, braceleft, NoSymbol ] }; + key { [ 8, parenleft, bracketleft, NoSymbol, threeeighths, rightarrow, braceright, NoSymbol ] }; + key { [ 9, parenright, bracketright, NoSymbol, fiveeighths, plusminus, bracketleft, NoSymbol ] }; + key { [ 0, equal, braceright, NoSymbol, seveneighths, trademark, bracketright, NoSymbol ] }; + key { [ ssharp, question, backslash, NoSymbol, backslash, questiondown, U02BB, NoSymbol ] }; + key { [ dead_acute, dead_grave, dead_abovedot, NoSymbol, dead_cedilla, dead_ogonek, notsign, NoSymbol ] }; + + key.type[Group1] = "EIGHT_LEVEL_ALPHABETIC"; + key { [ q, Q, at, NoSymbol, U0242, U0241, U030D, NoSymbol ] }; + key { [ w, W, dead_caron, NoSymbol, U02B7, U2126, dead_abovedot, NoSymbol ] }; + key { [ e, E, EuroSign, NoSymbol, oe, OE, dead_breve, NoSymbol ] }; + key { [ r, R, dead_diaeresis, NoSymbol, paragraph, registered, dead_circumflex, NoSymbol ] }; + key { [ t, T, dead_macron, NoSymbol, UA78C, UA78B, dead_diaeresis, NoSymbol ] }; + key { [ z, Z, dead_doubleacute, NoSymbol, U027C, yen, dead_invertedbreve, NoSymbol ] }; + key { [ u, U, dead_breve, NoSymbol, U0223, U0222, dead_caron, NoSymbol ] }; + key { [ i, I, dead_tilde, NoSymbol, idotless, U214D, dead_abovecomma, NoSymbol ] }; + key { [ o, O, dead_abovering, NoSymbol, oslash, Oslash, dead_horn, NoSymbol ] }; + key { [ p, P, dead_hook, NoSymbol, thorn, THORN, dead_hook, NoSymbol ] }; + key { [ udiaeresis, Udiaeresis, dead_horn, NoSymbol, U017F, dead_abovering, dead_grave, NoSymbol ] }; + key.type[Group1] = "EIGHT_LEVEL"; + key { [ plus, asterisk, asciitilde, NoSymbol, dead_tilde, dead_macron, at, NoSymbol ] }; + + key.type[Group1] = "ONE_LEVEL"; + key { [ Caps_Lock ] }; + key.type[Group1] = "EIGHT_LEVEL_ALPHABETIC"; + key { [ a, A, lessthanequal, NoSymbol, ae, AE, U0329, NoSymbol ] }; + key { [ s, S, greaterthanequal, NoSymbol, ssharp, section, dead_belowdot, NoSymbol ] }; + key { [ d, D, U2300, NoSymbol, eth, ETH, dead_belowbreve, NoSymbol ] }; + key { [ f, F, minutes, NoSymbol, U0294, ordfeminine, dead_belowcircumflex, NoSymbol ] }; + key { [ g, G, seconds, NoSymbol, eng, ENG, dead_belowmacron, NoSymbol ] }; + key { [ h, H, U1E9E, NoSymbol, U0272, U019D, U0332, NoSymbol ] }; + key { [ j, J, dead_cedilla, NoSymbol, U0133, U0132, dead_belowring, NoSymbol ] }; + key { [ k, K, dead_belowcomma, NoSymbol, kra, dead_belowcomma, dead_stroke, NoSymbol ] }; + key { [ l, L, dead_ogonek, NoSymbol, lstroke, Lstroke, U0338, NoSymbol ] }; + key { [ odiaeresis, Odiaeresis, dead_belowdot, NoSymbol, dead_acute, dead_doubleacute, degree, NoSymbol ] }; + key { [ adiaeresis, Adiaeresis, dead_stroke, NoSymbol, U019B, U1E9E, minutes, NoSymbol ] }; + key.type[Group1] = "EIGHT_LEVEL"; + key { [ numbersign, apostrophe, registered, NoSymbol, schwa, SCHWA, seconds, NoSymbol ] }; + + key { [ less, greater, bar, NoSymbol, U0149, brokenbar, U266A, NoSymbol ] }; + key.type[Group1] = "EIGHT_LEVEL_ALPHABETIC"; + key { [ y, Y, U203A, NoSymbol, U0292, U01B7, guillemotleft, NoSymbol ] }; + key { [ x, X, guillemotright, NoSymbol, doublelowquotemark, singlelowquotemark, guillemotright, NoSymbol ] }; + key { [ c, C, copyright, NoSymbol, cent, copyright, Greek_horizbar, NoSymbol ] }; + key { [ v, V, guillemotleft, NoSymbol, leftdoublequotemark, leftsinglequotemark, U2039, NoSymbol ] }; + key { [ b, B, U2039, NoSymbol, rightdoublequotemark, rightsinglequotemark, U203A, NoSymbol ] }; + key { [ n, N, endash, NoSymbol, U019E, U0220, endash, NoSymbol ] }; + key { [ m, M, mu, NoSymbol, mu, masculine, emdash, NoSymbol ] }; + key.type[Group1] = "EIGHT_LEVEL"; + key { [ comma, semicolon, U02BB, NoSymbol, ellipsis, multiply, dollar, NoSymbol ] }; + key { [ period, colon, U200C, NoSymbol, periodcentered, division, numbersign, NoSymbol ] }; + key { [ minus, underscore, hyphen, NoSymbol, U0140, U013F, U2011, NoSymbol ] }; + + key { [ space, space, nobreakspace, NoSymbol, U202F, U200C, nobreakspace, NoSymbol ] }; + + include "kpdl(comma)" + + include "level5(modifier_mapping)" + include "level3(modifier_mapping)" + key.type[Group1] = "THREE_LEVEL"; + key { [ Shift_L, Shift_L, ISO_Level5_Latch ] }; + key { [ Shift_R, Shift_R, ISO_Level5_Latch ] }; + key { [ ISO_Level3_Shift, ISO_Level5_Latch, ISO_Level5_Latch ] }; + }; + +partial alphanumeric_keys +xkb_symbols "ro" { + // Adds Romanian-specific letters to the German basic layout. + // Romanian symbols are accessible by combining and + // 'a', 's', 't', 'i', 'ä (ä)' (+ for capital letters). + + include "de(basic)" + + name[Group1]="Romanian (Germany)"; + + key { [ t, T, U021b, U021a ] }; + key { [ i, I, icircumflex, Icircumflex ] }; + key { [ a, A, acircumflex, Acircumflex ] }; + key { [ s, S, U0219, U0218 ] }; + key { [ adiaeresis, Adiaeresis, abreve, Abreve ] }; +}; + +partial alphanumeric_keys +xkb_symbols "ro_nodeadkeys" { + // Adds Romanian-specific letters to the German nodeadkeys layout. + // Read the comment for de_ro ! + + include "de(nodeadkeys)" + name[Group1]="Romanian (Germany, no dead keys)"; + + key { [ t, T, U021b, U021a ] }; + key { [ i, I, icircumflex, Icircumflex ] }; + key { [ a, A, acircumflex, Acircumflex ] }; + key { [ s, S, U0219, U0218 ] }; + key { [ adiaeresis, Adiaeresis, abreve, Abreve ] }; +}; + +// German Dvorak keymap by Thorsten Staerk (www.staerk.de/thorsten) +// Have acute and grave as dead keys, tilde and circumflex alive as they are needed +// in many programming languages. +// to use this keymap, use a 105-key-keyboard and the command setxkbmap -model pc105 -layout dvorak -variant de +// source: http://www-lehre.informatik.uni-osnabrueck.de/~rfreund/dvorak.php +partial alphanumeric_keys +xkb_symbols "dvorak" { + include "us(dvorak)" + + name[Group1]="German (Dvorak)"; + + key { [ asciicircum, degree ] }; + + key { [ 1, exclam, onesuperior ] }; + key { [ 2, quotedbl, twosuperior ] }; + key { [ 3, section, threesuperior ] }; + key { [ 4, dollar, bar ] }; + key { [ 5, percent, bar ] }; + key { [ 6, ampersand, brokenbar ] }; + key { [ 7, slash, braceleft ] }; + key { [ 8, parenleft, bracketleft ] }; + key { [ 9, parenright, bracketright ] }; + key { [ 0, equal, braceright ] }; + key { [ plus, asterisk, asciitilde ] }; + key { [ less, greater, dead_grave ] }; + + key { [ udiaeresis, Udiaeresis, at ] }; + key { [ comma, semicolon, dead_diaeresis ] }; + key { [ period, colon ] }; + key { [ c, C, copyright, Cacute ] }; + key { [ t, T, trademark ] }; + key { [ z, Z, zabovedot, Zabovedot ] }; + key { [ question, ssharp ] }; + key { [ slash, backslash, dead_acute ] }; + + key { [ a, A, at, aogonek ] }; + key { [ o, O, oacute, Oacute ] }; + key { [ e, E, EuroSign, eogonek ] }; + key { [ i, I ] }; + key { [ u, U ] }; + key { [ h, H ] }; + key { [ d, D ] }; + key { [ r, R, registered ] }; + key { [ n, N, nacute, Nacute ] }; + key { [ s, S, sacute, Sacute] }; + key { [ l, L, lstroke, Lstroke ] }; + + key { [ odiaeresis, Odiaeresis ] }; + key { [ q, Q, at ] }; + key { [ m, M, mu ] }; + key { [ numbersign, apostrophe ] }; + + key { [ minus, underscore, hyphen, diaeresis] }; + + key { [ adiaeresis, Adiaeresis, bar ] }; + + include "level3(ralt_switch)" +}; + + +// German Neo-Layout Version 2 +// adopted 2004 by Hanno Behrens +// inspired by Dvorak/de-ergo http://www.goebel-consult.de/de-ergo/ +// +// Authors: +// Stephan Hilb +// +// Benjamin Kellermann +// Erik Streb +// and many other contributors +// +// http://www.neo-layout.org +// +// $Revision$, $Date$ + +partial alphanumeric_keys modifier_keys keypad_keys +xkb_symbols "neo_base" { + + // Levels in Neo jargon + // -------------------------------------------------------------- + // Ebene 1: normal + // Ebene 2: Shift + // Ebene 3: Mod3 + // Ebene 4: Mod4 (for marking something use Shift + Mod4) + // Ebene 5: Shift + Mod3 + // Ebene 6: Mod3 + Mod4 + // Compose (not a level): Mod3 + Tab + // Feststelltaste (Capslock): Shift + Shift + // Mod4-Lock: Mod4 + Mod4 + // Mod4-Lock: Shift + Mod3 + Tab + + // Legend + // =============== + // Levels in Xkbmap jargon to be found here in the definitions. + // These are the levels used, and Xorg's translations: + // -------------------------------------------------------------- + // Xorg: Level1 Level2 Level3 Level4 Level5 Level6 Level7 Level8 + // Neo: Ebene1 Ebene2 Ebene3 Ebene5 Ebene4 Pseudo-Ebene Ebene6 ??? + // Keys (Neo): None Shift Mod3 Mod3 + Shift Mod4 Mod4 + Shift Mod3 + Mod4 Mod3 + Mod4 + Shift + + + // Alphanumeric-keys + // =============== + key.type[Group1] = "EIGHT_LEVEL_LEVEL_FIVE_LOCK"; + + // Tab as Multi_key (Compose) + // -------------------------------------------------------------- + key { [ Tab, ISO_Left_Tab, Multi_key, ISO_Level5_Lock, NoSymbol, NoSymbol, NoSymbol, ISO_Level5_Lock ] }; + + + // Number row + // -------------------------------------------------------------- + key { [ dead_circumflex, dead_caron, U21BB, U02DE, dead_abovedot, Pointer_EnableKeys, dead_belowdot, NoSymbol ] }; + + key { [ 1, degree, onesuperior, onesubscript, ordfeminine, NoSymbol, notsign, NoSymbol ] }; + key { [ 2, section, twosuperior, twosubscript, masculine, NoSymbol, logicalor, NoSymbol ] }; + key { [ 3, U2113, threesuperior, threesubscript, numerosign, NoSymbol, logicaland, NoSymbol ] }; + key { [ 4, guillemotright, U203A, femalesymbol, NoSymbol, NoSymbol, U22A5, NoSymbol ] }; + key { [ 5, guillemotleft, U2039, malesymbol, periodcentered, NoSymbol, U2221, NoSymbol ] }; + key { [ 6, dollar, cent, U26A5, sterling, NoSymbol, U2225, NoSymbol ] }; + + key { [ 7, EuroSign, yen, U03F0, currency, NoSymbol, rightarrow, NoSymbol ] }; + key { [ 8, doublelowquotemark, singlelowquotemark, U27E8, Tab, ISO_Left_Tab, U221E, NoSymbol ] }; + key { [ 9, leftdoublequotemark, leftsinglequotemark, U27E9, KP_Divide, KP_Divide, variation, NoSymbol ] }; + key { [ 0, rightdoublequotemark, rightsinglequotemark, zerosubscript, KP_Multiply, KP_Multiply, emptyset, NoSymbol ] }; + + key { [ minus, emdash, NoSymbol, U2011, KP_Subtract, KP_Subtract, hyphen, NoSymbol ] }; + key { [ dead_grave, dead_cedilla, dead_abovering, dead_dasia, dead_diaeresis, NoSymbol, dead_macron, NoSymbol ] }; + + // Top row + // -------------------------------------------------------------- + key.type[Group1] = "EIGHT_LEVEL_ALPHABETIC_LEVEL_FIVE_LOCK"; + key { [ x, X, ellipsis, Greek_xi, Prior, Prior, Greek_XI, NoSymbol ] }; + key { [ v, V, underscore, NoSymbol, BackSpace, BackSpace, radical, NoSymbol ] }; + key { [ l, L, bracketleft, Greek_lambda, Up, Up, Greek_LAMBDA, NoSymbol ] }; + key { [ c, C, bracketright, Greek_chi, Delete, Delete, U2102, NoSymbol ] }; + key { [ w, W, asciicircum, Greek_omega, Next, Next, Greek_OMEGA, NoSymbol ] }; + + key { [ k, K, exclam, Greek_kappa, exclamdown, NoSymbol, multiply, NoSymbol ] }; + key { [ h, H, less, Greek_psi, KP_7, KP_7, Greek_PSI, NoSymbol ] }; + key { [ g, G, greater, Greek_gamma, KP_8, KP_8, Greek_GAMMA, NoSymbol ] }; + key { [ f, F, equal, Greek_phi, KP_9, KP_9, Greek_PHI, NoSymbol ] }; + key { [ q, Q, ampersand, U03D5, KP_Add, KP_Add, U211A, NoSymbol ] }; + + key { [ ssharp, U1E9E, U017F, Greek_finalsmallsigma, U2212, NoSymbol, jot, NoSymbol ] }; + + key.type[Group1] = "EIGHT_LEVEL_LEVEL_FIVE_LOCK"; + key { [ dead_acute, dead_tilde, dead_stroke, dead_psili, dead_doubleacute, NoSymbol, dead_breve, NoSymbol ] }; + + // Middle row + // -------------------------------------------------------------- + key.type[Group1] = "EIGHT_LEVEL_ALPHABETIC_LEVEL_FIVE_LOCK"; + key { [ u, U, backslash, NoSymbol, Home, Home, includedin, NoSymbol ] }; + key { [ i, I, slash, Greek_iota, Left, Left, integral, NoSymbol ] }; + key { [ a, A, braceleft, Greek_alpha, Down, Down, U2200, NoSymbol ] }; + key { [ e, E, braceright, Greek_epsilon, Right, Right, U2203, NoSymbol ] }; + key { [ o, O, asterisk, Greek_omicron, End, End, elementof, NoSymbol ] }; + + key { [ s, S, question, Greek_sigma, questiondown, NoSymbol, Greek_SIGMA, NoSymbol ] }; + key { [ n, N, parenleft, Greek_nu, KP_4, KP_4, U2115, NoSymbol ] }; + key { [ r, R, parenright, Greek_rho, KP_5, KP_5, U211D, NoSymbol ] }; + key { [ t, T, minus, Greek_tau, KP_6, KP_6, partialderivative, NoSymbol ] }; + key { [ d, D, colon, Greek_delta, KP_Separator, comma, Greek_DELTA, NoSymbol ] }; + + key { [ y, Y, at, Greek_upsilon, period, KP_Decimal, nabla, NoSymbol ] }; + + // Bottom row + // -------------------------------------------------------------- + key { [ udiaeresis, Udiaeresis, numbersign, NoSymbol, Escape, Escape, union, NoSymbol ] }; + key { [ odiaeresis, Odiaeresis, dollar, U03F5, Tab, Tab, intersection, NoSymbol ] }; + key { [ adiaeresis, Adiaeresis, bar, Greek_eta, Insert, Insert, U2135, NoSymbol ] }; + key { [ p, P, asciitilde, Greek_pi, Return, Return, Greek_PI, NoSymbol ] }; + key { [ z, Z, grave, Greek_zeta, Undo, Redo, U2124, NoSymbol ] }; + + key { [ b, B, plus, Greek_beta, colon, NoSymbol, U21D0, NoSymbol ] }; + key { [ m, M, percent, Greek_mu, KP_1, KP_1, ifonlyif, NoSymbol ] }; + key.type[Group1] = "EIGHT_LEVEL_LEVEL_FIVE_LOCK"; + key { [ comma, endash, quotedbl, U03F1, KP_2, KP_2, U21D2, NoSymbol ] }; + key { [ period, enfilledcircbullet, apostrophe, U03D1, KP_3, KP_3, U21A6, NoSymbol ] }; + key.type[Group1] = "EIGHT_LEVEL_ALPHABETIC_LEVEL_FIVE_LOCK"; + key { [ j, J, semicolon, Greek_theta, semicolon, NoSymbol, Greek_THETA, NoSymbol ] }; + key.type[Group1] = "EIGHT_LEVEL_LEVEL_FIVE_LOCK"; + + // Space key + // -------------------------------------------------------------- + key { [ space, space, space, nobreakspace, KP_0, KP_0, U202F, NoSymbol ] }; + + + // Keypad-keys + // =============== + + // The former Numlock key: + key { [ Tab, ISO_Left_Tab, equal, approxeq, notequal, Pointer_EnableKeys, identical, NoSymbol ] }; + + // Topmost row + // -------------------------------------------------------------- + key { [ KP_Divide, KP_Divide, division, U2300, U2044, NoSymbol, U2223, NoSymbol ] }; + key { [ KP_Multiply, KP_Multiply, U22C5, U2299, multiply, NoSymbol, U2297, NoSymbol ] }; + key { [ KP_Subtract, KP_Subtract, U2212, U2296, U2216, NoSymbol, U2238, NoSymbol ] }; + + // Top row + // -------------------------------------------------------------- + key { [ KP_7, U2714, U2195, U226A, KP_Home, KP_Home, upstile, NoSymbol ] }; + key { [ KP_8, U2718, uparrow, intersection, KP_Up, KP_Up, U22C2, NoSymbol ] }; + key { [ KP_9, dagger, U20D7, U226B, KP_Prior, KP_Prior, U2309, NoSymbol ] }; + key { [ KP_Add, KP_Add, plusminus, U2295, U2213, NoSymbol, U2214, NoSymbol ] }; + + // Middle row + // -------------------------------------------------------------- + key { [ KP_4, club, leftarrow, includedin, KP_Left, KP_Left, U2286, NoSymbol ] }; + key { [ KP_5, EuroSign, colon, U22B6, KP_Begin, KP_Begin, U22B7, NoSymbol ] }; + key { [ KP_6, U2023, rightarrow, includes, KP_Right, KP_Right, U2287, NoSymbol ] }; + + // Bottom row + // -------------------------------------------------------------- + key { [ KP_1, diamond, U2194, lessthanequal, KP_End, KP_End, downstile, NoSymbol ] }; + key { [ KP_2, heart, downarrow, union, KP_Down, KP_Down, U22C3, NoSymbol ] }; + key { [ KP_3, U2660, U21CC, greaterthanequal, KP_Next, KP_Next, U230B, NoSymbol ] }; + key { [ KP_Enter, KP_Enter, KP_Enter, KP_Enter, KP_Enter, KP_Enter, KP_Enter, NoSymbol ] }; + key { [ KP_Equal, NoSymbol, NoSymbol, NoSymbol, NoSymbol, NoSymbol, NoSymbol, NoSymbol ] }; + + // Bottommost row + // -------------------------------------------------------------- + key { [ KP_0, U2423, percent, U2030, KP_Insert, KP_Insert, U25A1, NoSymbol ] }; + key { [ KP_Separator, period, comma, minutes, KP_Delete, KP_Delete, seconds, NoSymbol ] }; +}; + +partial alphanumeric_keys modifier_keys keypad_keys +xkb_symbols "neo" { + + include "de(neo_base)" + + name[Group1]= "German (Neo 2)"; + + include "shift(both_capslock)" + include "level3(caps_switch)" + include "level3(bksl_switch)" + include "level5(lsgt_switch_lock)" + include "level5(ralt_switch_lock)" +}; + +// Copied from macintosh_vndr/de +// olh@suse.de very close to MacOS map + +partial alphanumeric_keys +xkb_symbols "mac" { + + include "de" + name[Group1]= "German (Macintosh)"; + + key { [ 1, exclam, exclamdown, at ] }; + key { [ 5, percent, bracketleft ] }; + key { [ 6, ampersand, bracketright ] }; + key { [ 7, slash, bar, backslash ] }; + key { [ 8, parenleft, braceleft, asciitilde ] }; + key { [ 9, parenright, braceright ] }; + key { [ q, Q, guillemotleft, guillemotright ] }; + key { [ r, R, registered ] }; + key { [ u, U, diaeresis, Aacute ] }; + key { [ i, I, slash, Ucircumflex ] }; + key { [ udiaeresis, Udiaeresis, periodcentered, degree ] }; + key { [ plus, asterisk, asciitilde ] }; + key { [ a, A, aring, Aring ] }; + key { [ g, G, copyright ] }; + key { [ h, H, ordfeminine ] }; + key { [ l, L, at ] }; + key { [ odiaeresis, Odiaeresis, dead_acute ] }; + key { [ n, N, asciitilde ] }; +}; + +partial alphanumeric_keys +xkb_symbols "mac_nodeadkeys" { + // modify the standard German mac layout to not have any dead keys + include "de(mac)" + name[Group1]= "German (Macintosh, no dead keys)"; + + key { [ asciicircum, degree, notsign ] }; + key { [ 4, dollar, onequarter, currency ] }; + key { [ acute, grave, cedilla ] }; + key { [ udiaeresis, Udiaeresis, diaeresis ] }; + key { [ plus, asterisk, asciitilde, macron ] }; + key { [ odiaeresis, Odiaeresis, acute ] }; + key { [ adiaeresis, Adiaeresis, asciicircum ] }; + + key { [ numbersign, apostrophe, rightsinglequotemark ] }; +}; + +partial alphanumeric_keys +xkb_symbols "dsb" +{ + include "latin(basic)" + name[Group1] = "Lower Sorbian"; + key { [ z, Z, zcaron, Zcaron ] }; + key { [ x, X, zacute, Zacute ] }; + key { [ c, C, cacute, Cacute ] }; + key { [ v, V, ccaron, Ccaron ] }; + key { [ n, N, nacute, Nacute ] }; + key { [ s, S, sacute, Sacute ] }; + key { [ d, D, scaron, Scaron ] }; + key { [ f, F ] }; + key { [ q, Q ] }; + key { [ w, W ] }; + key { [ e, E, ecaron, Ecaron ] }; + key { [ r, R, racute, Racute ] }; + key { [ t, T, U20B5, EuroSign ] }; + key { [ o, O, oacute, Oacute ] }; + include "kpdl(comma)" + include "level3(ralt_switch)" +}; + +partial alphanumeric_keys +xkb_symbols "dsb_qwertz" +{ + include "latin(basic)" + name[Group1] = "Lower Sorbian (QWERTZ)"; + key { [ y, Y ] }; + key { [ x, X ] }; + key { [ c, C, cacute, Cacute ] }; + key { [ v, V, ccaron, Ccaron ] }; + key { [ n, N, nacute, Nacute ] }; + key { [ s, S, sacute, Sacute ] }; + key { [ d, D, scaron, Scaron ] }; + key { [ f, F ] }; + key { [ q, Q ] }; + key { [ w, W ] }; + key { [ e, E, ecaron, Ecaron ] }; + key { [ r, R, racute, Racute ] }; + key { [ t, T, U20B5, EuroSign ] }; + key { [ z, Z, zcaron, Zcaron ] }; + key { [ u, U, zacute, Zacute ] }; + key { [ o, O, oacute, Oacute ] }; + include "kpdl(comma)" + include "level3(ralt_switch)" +}; + +partial alphanumeric_keys +xkb_symbols "qwerty" { + + // This layout should work exactly as a de with the exception + // of 'Z' and 'Y' keys, which are in the qwerty style (ie. swapped). + // 2008 by Matej Košík + + include "de(basic)" + + name[Group1] = "German (QWERTY)"; + + key { [ z, Z, leftarrow, yen ] }; + key { [ y, Y, guillemotleft, less ] }; +}; + +// layout for Russian letters on an german keyboard +// based on US-RU layout by Ivan Popov 2005-07-17 +// adopted for german layout by Alexey Fisher 2010-08-19 + +partial alphanumeric_keys +xkb_symbols "ru" { + + include "de(basic)" + + name[Group1]= "Russian (Germany, phonetic)"; + + key { [ Cyrillic_a, Cyrillic_A ] }; + key { [ Cyrillic_be, Cyrillic_BE ] }; + key { [ Cyrillic_ve, Cyrillic_VE ] }; + key { [ Cyrillic_ghe, Cyrillic_GHE ] }; + key { [ Cyrillic_de, Cyrillic_DE ] }; + key { [ Cyrillic_ie, Cyrillic_IE ] }; + key { [ Cyrillic_io, Cyrillic_IO, asciitilde ] }; + key { [ Cyrillic_zhe, Cyrillic_ZHE ] }; + key { [ Cyrillic_ze, Cyrillic_ZE ] }; + key { [ Cyrillic_i, Cyrillic_I ] }; + key { [ Cyrillic_shorti, Cyrillic_SHORTI ] }; + key { [ Cyrillic_ka, Cyrillic_KA ] }; + key { [ Cyrillic_el, Cyrillic_EL ] }; + key { [ Cyrillic_em, Cyrillic_EM ] }; + key { [ Cyrillic_en, Cyrillic_EN ] }; + key { [ Cyrillic_o, Cyrillic_O ] }; + key { [ Cyrillic_pe, Cyrillic_PE ] }; + key { [ Cyrillic_er, Cyrillic_ER ] }; + key { [ Cyrillic_es, Cyrillic_ES ] }; + key { [ Cyrillic_te, Cyrillic_TE ] }; + key { [ Cyrillic_u, Cyrillic_U ] }; + key { [ Cyrillic_ef, Cyrillic_EF ] }; + key { [ Cyrillic_ha, Cyrillic_HA ] }; + key { [ Cyrillic_tse, Cyrillic_TSE ] }; + key { [ Cyrillic_che, Cyrillic_CHE ] }; + key { [ Cyrillic_sha, Cyrillic_SHA ] }; + key { [ Cyrillic_shcha, Cyrillic_SHCHA, plus, asterisk ] }; + key { [ Cyrillic_hardsign, Cyrillic_HARDSIGN ] }; + key { [ Cyrillic_yeru, Cyrillic_YERU ] }; + key { [ Cyrillic_softsign, Cyrillic_SOFTSIGN ] }; + key { [ Cyrillic_e, Cyrillic_E ] }; + key { [ Cyrillic_yu, Cyrillic_YU, numbersign, apostrophe ] }; + key { [ Cyrillic_ya, Cyrillic_YA ] }; + + include "level3(ralt_switch)" +}; + +// layout for Russian (recommended) letters on a german keyboard +// based on "Russisch für Deutsche, empfohlen" by B. Bendixen und H. Rothe http://russisch.urz.uni-leipzig.de/key2000.htm 2016-02-01 +// adapted for Linux by Niko Krause 2016-06-09 + +partial alphanumeric_keys +xkb_symbols "ru-recom" { + + include "de(basic)" + + name[Group1]= "Russian (Germany, recommended)"; + + key { [ Cyrillic_a, Cyrillic_A ] }; + key { [ Cyrillic_be, Cyrillic_BE ] }; + key { [ Cyrillic_ve, Cyrillic_VE ] }; + key { [ Cyrillic_ghe, Cyrillic_GHE, Ukrainian_ghe_with_upturn, Ukrainian_GHE_WITH_UPTURN ] }; + key { [ Cyrillic_de, Cyrillic_DE ] }; + key { [ Cyrillic_ie, Cyrillic_IE ] }; + key { [ Cyrillic_ya, Cyrillic_YA, asciicircum, degree ] }; + key { [ Cyrillic_ha, Cyrillic_HA ] }; + key { [ Cyrillic_tse, Cyrillic_TSE ] }; + key { [ Cyrillic_i, Cyrillic_I, Ukrainian_i, Ukrainian_I ] }; + key { [ Cyrillic_shorti, Cyrillic_SHORTI, Ukrainian_yi, Ukrainian_YI ] }; + key { [ Cyrillic_ka, Cyrillic_KA ] }; + key { [ Cyrillic_el, Cyrillic_EL ] }; + key { [ Cyrillic_em, Cyrillic_EM ] }; + key { [ Cyrillic_en, Cyrillic_EN ] }; + key { [ Cyrillic_o, Cyrillic_O ] }; + key { [ Cyrillic_pe, Cyrillic_PE ] }; + key { [ Cyrillic_er, Cyrillic_ER ] }; + key { [ Cyrillic_es, Cyrillic_ES, Cyrillic_ze, Cyrillic_ZE ] }; + key { [ Cyrillic_te, Cyrillic_TE ] }; + key { [ Cyrillic_u, Cyrillic_U ] }; + key { [ Cyrillic_ef, Cyrillic_EF ] }; + key { [ Cyrillic_zhe, Cyrillic_ZHE ] }; + key { [ Cyrillic_che, Cyrillic_CHE ] }; + key { [ Cyrillic_io, Cyrillic_IO ] }; + key { [ Cyrillic_yu, Cyrillic_YU ] }; + key { [ Cyrillic_sha, Cyrillic_SHA, plus, asterisk ] }; + key { [ Cyrillic_ze, Cyrillic_ZE ] }; + key { [ Cyrillic_yeru, Cyrillic_YERU ] }; + key { [ Cyrillic_softsign, Cyrillic_SOFTSIGN ] }; + key { [ Cyrillic_e, Cyrillic_E, Ukrainian_ie, Ukrainian_IE ] }; + key { [ Cyrillic_hardsign, Cyrillic_HARDSIGN, numbersign, apostrophe ] }; + key { [ Cyrillic_shcha, Cyrillic_SHCHA ] }; + + key { [ asciitilde, question, backslash, questiondown ] }; + key { [ U0301, U0300, U0323, U0307 ] }; + + include "level3(ralt_switch)" +}; + +// layout for Russian (transliteration) letters on a german keyboard +// based on "Russisch für Deutsche, Transliteration" by B. Bendixen und H. Rothe http://russisch.urz.uni-leipzig.de/key2000.htm 2016-02-01 +// adapted for Linux by Niko Krause 2016-06-09 + +partial alphanumeric_keys +xkb_symbols "ru-translit" { + + include "de(basic)" + + name[Group1]= "Russian (Germany, transliteration)"; + + key { [ Cyrillic_a, Cyrillic_A ] }; + key { [ Cyrillic_be, Cyrillic_BE ] }; + key { [ Cyrillic_sha, Cyrillic_SHA ] }; + key { [ Cyrillic_ghe, Cyrillic_GHE, Ukrainian_ghe_with_upturn, Ukrainian_GHE_WITH_UPTURN ] }; + key { [ Cyrillic_de, Cyrillic_DE ] }; + key { [ Cyrillic_ie, Cyrillic_IE ] }; + key { [ Cyrillic_ya, Cyrillic_YA, asciicircum, degree ] }; + key { [ Cyrillic_ha, Cyrillic_HA ] }; + key { [ Cyrillic_ze, Cyrillic_ZE ] }; + key { [ Cyrillic_i, Cyrillic_I, Ukrainian_i, Ukrainian_I ] }; + key { [ Cyrillic_shorti, Cyrillic_SHORTI, Ukrainian_yi, Ukrainian_YI ] }; + key { [ Cyrillic_ka, Cyrillic_KA ] }; + key { [ Cyrillic_el, Cyrillic_EL ] }; + key { [ Cyrillic_em, Cyrillic_EM ] }; + key { [ Cyrillic_en, Cyrillic_EN ] }; + key { [ Cyrillic_o, Cyrillic_O ] }; + key { [ Cyrillic_pe, Cyrillic_PE ] }; + key { [ Cyrillic_er, Cyrillic_ER ] }; + key { [ Cyrillic_es, Cyrillic_ES, Cyrillic_che, Cyrillic_CHE ] }; + key { [ Cyrillic_te, Cyrillic_TE ] }; + key { [ Cyrillic_u, Cyrillic_U ] }; + key { [ Cyrillic_ef, Cyrillic_EF ] }; + key { [ Cyrillic_zhe, Cyrillic_ZHE ] }; + key { [ Cyrillic_tse, Cyrillic_TSE ] }; + key { [ Cyrillic_io, Cyrillic_IO ] }; + key { [ Cyrillic_yu, Cyrillic_YU ] }; + key { [ Cyrillic_hardsign, Cyrillic_HARDSIGN, plus, asterisk ] }; + key { [ Cyrillic_che, Cyrillic_CHE ] }; + key { [ Cyrillic_yeru, Cyrillic_YERU ] }; + key { [ Cyrillic_ve, Cyrillic_VE ] }; + key { [ Cyrillic_e, Cyrillic_E, Ukrainian_ie, Ukrainian_IE ] }; + key { [ Cyrillic_softsign, Cyrillic_SOFTSIGN, numbersign, apostrophe ] }; + key { [ Cyrillic_shcha, Cyrillic_SHCHA ] }; + + key { [ asciitilde, question, backslash, questiondown ] }; + key { [ U0301, U0300, U0323, U0307 ] }; + + include "level3(ralt_switch)" +}; + +partial alphanumeric_keys +xkb_symbols "pl" { + + // Combined layout for entering both German and Polish symbols on a German physical + // keyboard. Based on German (no dead keys) and Polish (basic). Polish diacritics + // on AltGr+"acelnosxz". EuroSign moved to AE04 (AltGr+dollar key) to avoid conflict + // with Polish eogonek. + // + // https://github.com/kontextify/xkeyboard-config + + include "latin(type4)" + include "de(nodeadkeys)" + + name[Group1]= "Polish (Germany, no dead keys)"; + + key { [ 4, dollar, EuroSign, currency ] }; + + key { [ q, Q ] }; + key { [ w, W ] }; + key { [ e, E, eogonek, Eogonek ] }; + key { [ o, O, oacute, Oacute ] }; + key { [ a, A, aogonek, Aogonek ] }; + key { [ s, S, sacute, Sacute ] }; + key { [ f, F ] }; + key { [ z, Z, zabovedot, Zabovedot ] }; + key { [ x, X, zacute, Zacute ] }; + key { [ c, C, cacute, Cacute ] }; + key { [ n, N, nacute, Nacute ] }; + + include "kpdl(comma)" + + include "level3(ralt_switch)" +}; + +partial alphanumeric_keys +xkb_symbols "tr" { + + // add turkish-specific letters to the basic German layout. + // Turkish symbols are accessible with combination of and + // 'i', 's', 'g', 'c'' (+ for capital letters). + + include "de(basic)" + + name[Group1]="Turkish (Germany)"; + + key { [ i, I, U0131, U0130 ] }; + key { [ s, S, U015F, U015E ] }; + key { [ g, G, U011F, U011E ] }; + key { [ c, C, U0E7, U0C7 ] }; +}; + +partial alphanumeric_keys +xkb_symbols "us" { + include "us" + + name[Group1]="German (US)"; + + key { [ 3, numbersign, section, degree ] }; + key { [ minus, underscore, ssharp, U1E9E ] }; + + key { [ e, E, EuroSign, cent ] }; + key { [ u, U, udiaeresis, Udiaeresis ] }; + key { [ o, O, odiaeresis, Odiaeresis ] }; + key { [ bracketleft, braceleft, udiaeresis, Udiaeresis ] }; + + key { [ a, A, adiaeresis, Adiaeresis ] }; + key { [ s, S, ssharp, U1E9E ] }; + key { [ semicolon, colon, odiaeresis, Odiaeresis ] }; + key { [ apostrophe, quotedbl, adiaeresis, Adiaeresis ] }; + + key { [ c, C, Multi_key, Multi_key ] }; + key { [ m, M, dead_greek, Menu ] }; + + include "level3(ralt_switch)" +}; + +// EXTRAS: + +partial alphanumeric_keys +xkb_symbols "hu" { + + // modify the basic German layout to not have any dead keys and add Hungarian letters + + include "de(basic)" + name[Group1]="German (with Hungarian letters, no dead keys)"; + + key { [ y, Y, guillemotleft, less ] }; + key { [odiaeresis, Odiaeresis, eacute, Eacute ] }; + key { [adiaeresis, Adiaeresis, aacute, Aacute] }; + key { [ e, E, EuroSign, EuroSign ] }; + key { [ z, Z, leftarrow, yen ] }; + key { [ u, U, uacute, Uacute ] }; + key { [ i, I, iacute, Iacute ] }; + key { [ o, O, odoubleacute, Odoubleacute ] }; + key { [udiaeresis, Udiaeresis, udoubleacute, Udoubleacute ] }; + key { [ plus, asterisk, asciitilde, macron ] }; + key { [ acute, grave, oacute, Oacute ] }; + key { [numbersign, apostrophe, rightsinglequotemark, grave ] }; + key { [asciicircum, degree, notsign, notsign ] }; +}; + +partial alphanumeric_keys + xkb_symbols "sun_type6" { + include "sun_vndr/de(sun_type6)" +}; + +partial alphanumeric_keys +xkb_symbols "adnw_base" { + include "de(neo_base)" + + key.type[Group1] = "EIGHT_LEVEL_LEVEL_FIVE_LOCK"; + key { [ period, enfilledcircbullet, NoSymbol, U03D1, NoSymbol, NoSymbol, U21A6, NoSymbol ] }; + key { [ comma, endash, NoSymbol, U03F1, NoSymbol, NoSymbol, U21D2, NoSymbol ] }; + + key.type[Group1] = "EIGHT_LEVEL_ALPHABETIC_LEVEL_FIVE_LOCK"; + key { [ k, K, NoSymbol, Greek_kappa, NoSymbol, NoSymbol, multiply, NoSymbol ] }; + key { [ u, U, NoSymbol, NoSymbol, NoSymbol, NoSymbol, includedin, NoSymbol ] }; + key { [ udiaeresis, Udiaeresis, NoSymbol, NoSymbol, NoSymbol, NoSymbol, union, NoSymbol ] }; + key { [ adiaeresis, Adiaeresis, NoSymbol, Greek_eta, NoSymbol, NoSymbol, U2135, NoSymbol ] }; + key { [ v, V, NoSymbol, NoSymbol, NoSymbol, NoSymbol, radical, NoSymbol ] }; + key { [ g, G, NoSymbol, Greek_gamma, NoSymbol, NoSymbol, Greek_GAMMA, NoSymbol ] }; + key { [ c, C, NoSymbol, Greek_chi, NoSymbol, NoSymbol, U2102, NoSymbol ] }; + key { [ l, L, NoSymbol, Greek_lambda, NoSymbol, NoSymbol, Greek_LAMBDA, NoSymbol ] }; + key { [ j, J, NoSymbol, Greek_theta, NoSymbol, NoSymbol, Greek_THETA, NoSymbol ] }; + key { [ f, F, NoSymbol, Greek_phi, NoSymbol, NoSymbol, Greek_PHI, NoSymbol ] }; + key { [ h, H, NoSymbol, Greek_psi, NoSymbol, NoSymbol, Greek_PSI, NoSymbol ] }; + key { [ i, I, NoSymbol, Greek_iota, NoSymbol, NoSymbol, integral, NoSymbol ] }; + key { [ e, E, NoSymbol, Greek_epsilon, NoSymbol, NoSymbol, U2203, NoSymbol ] }; + key { [ a, A, NoSymbol, Greek_alpha, NoSymbol, NoSymbol, U2200, NoSymbol ] }; + key { [ o, O, NoSymbol, Greek_omicron, NoSymbol, NoSymbol, elementof, NoSymbol ] }; + key { [ d, D, NoSymbol, Greek_delta, NoSymbol, NoSymbol, Greek_DELTA, NoSymbol ] }; + key { [ t, T, NoSymbol, Greek_tau, NoSymbol, NoSymbol, partialderivative, NoSymbol ] }; + key { [ r, R, NoSymbol, Greek_rho, NoSymbol, NoSymbol, U211D, NoSymbol ] }; + key { [ n, N, NoSymbol, Greek_nu, NoSymbol, NoSymbol, U2115, NoSymbol ] }; + key { [ s, S, NoSymbol, Greek_sigma, NoSymbol, NoSymbol, Greek_SIGMA, NoSymbol ] }; + key { [ ssharp, U1E9E, NoSymbol, Greek_finalsmallsigma, NoSymbol, NoSymbol, jot, NoSymbol ] }; + key { [ x, X, NoSymbol, Greek_xi, NoSymbol, NoSymbol, Greek_XI, NoSymbol ] }; + key { [ y, Y, NoSymbol, Greek_upsilon, NoSymbol, NoSymbol, nabla, NoSymbol ] }; + key { [ odiaeresis, Odiaeresis, NoSymbol, U03F5, NoSymbol, NoSymbol, intersection, NoSymbol ] }; + key { [ q, Q, NoSymbol, U03D5, NoSymbol, NoSymbol, U211A, NoSymbol ] }; + key { [ b, B, NoSymbol, Greek_beta, NoSymbol, NoSymbol, U21D0, NoSymbol ] }; + key { [ p, P, NoSymbol, Greek_pi, NoSymbol, NoSymbol, Greek_PI, NoSymbol ] }; + key { [ w, W, NoSymbol, Greek_omega, NoSymbol, NoSymbol, Greek_OMEGA, NoSymbol ] }; + key { [ m, M, NoSymbol, Greek_mu, NoSymbol, NoSymbol, ifonlyif, NoSymbol ] }; + key { [ z, Z, NoSymbol, Greek_zeta, NoSymbol, NoSymbol, U2124, NoSymbol ] }; +}; + +partial alphanumeric_keys modifier_keys keypad_keys +xkb_symbols "adnw" { + + include "de(adnw_base)" + + name[Group1]= "German (Aus der Neo-Welt)"; + + include "shift(both_capslock)" + include "level3(caps_switch)" + include "level3(bksl_switch)" + include "level5(lsgt_switch_lock)" + include "level5(ralt_switch_lock)" +}; + +partial alphanumeric_keys +xkb_symbols "koy_base" { + include "de(neo_base)" + + key.type[Group1] = "EIGHT_LEVEL_LEVEL_FIVE_LOCK"; + key { [ period, enfilledcircbullet, NoSymbol, U03D1, NoSymbol, NoSymbol, U21A6, NoSymbol ] }; + key { [ comma, endash, NoSymbol, U03F1, NoSymbol, NoSymbol, U21D2, NoSymbol ] }; + + key.type[Group1] = "EIGHT_LEVEL_ALPHABETIC_LEVEL_FIVE_LOCK"; + key { [ k, K, NoSymbol, Greek_kappa, NoSymbol, NoSymbol, multiply, NoSymbol ] }; + key { [ o, O, NoSymbol, Greek_omicron, NoSymbol, NoSymbol, elementof, NoSymbol ] }; + key { [ y, Y, NoSymbol, Greek_upsilon, NoSymbol, NoSymbol, nabla, NoSymbol ] }; + key { [ v, V, NoSymbol, NoSymbol, NoSymbol, NoSymbol, radical, NoSymbol ] }; + key { [ g, G, NoSymbol, Greek_gamma, NoSymbol, NoSymbol, Greek_GAMMA, NoSymbol ] }; + key { [ c, C, NoSymbol, Greek_chi, NoSymbol, NoSymbol, U2102, NoSymbol ] }; + key { [ l, L, NoSymbol, Greek_lambda, NoSymbol, NoSymbol, Greek_LAMBDA, NoSymbol ] }; + key { [ ssharp, U1E9E, NoSymbol, Greek_finalsmallsigma, NoSymbol, NoSymbol, jot, NoSymbol ] }; + key { [ z, Z, NoSymbol, Greek_zeta, NoSymbol, NoSymbol, U2124, NoSymbol ] }; + key { [ h, H, NoSymbol, Greek_psi, NoSymbol, NoSymbol, Greek_PSI, NoSymbol ] }; + key { [ a, A, NoSymbol, Greek_alpha, NoSymbol, NoSymbol, U2200, NoSymbol ] }; + key { [ e, E, NoSymbol, Greek_epsilon, NoSymbol, NoSymbol, U2203, NoSymbol ] }; + key { [ i, I, NoSymbol, Greek_iota, NoSymbol, NoSymbol, integral, NoSymbol ] }; + key { [ u, U, NoSymbol, NoSymbol, NoSymbol, NoSymbol, includedin, NoSymbol ] }; + key { [ d, D, NoSymbol, Greek_delta, NoSymbol, NoSymbol, Greek_DELTA, NoSymbol ] }; + key { [ t, T, NoSymbol, Greek_tau, NoSymbol, NoSymbol, partialderivative, NoSymbol ] }; + key { [ r, R, NoSymbol, Greek_rho, NoSymbol, NoSymbol, U211D, NoSymbol ] }; + key { [ n, N, NoSymbol, Greek_nu, NoSymbol, NoSymbol, U2115, NoSymbol ] }; + key { [ s, S, NoSymbol, Greek_sigma, NoSymbol, NoSymbol, Greek_SIGMA, NoSymbol ] }; + key { [ f, F, NoSymbol, Greek_phi, NoSymbol, NoSymbol, Greek_PHI, NoSymbol ] }; + key { [ x, X, NoSymbol, Greek_xi, NoSymbol, NoSymbol, Greek_XI, NoSymbol ] }; + key { [ q, Q, NoSymbol, U03D5, NoSymbol, NoSymbol, U211A, NoSymbol ] }; + key { [ adiaeresis, Adiaeresis, NoSymbol, Greek_eta, NoSymbol, NoSymbol, U2135, NoSymbol ] }; + key { [ udiaeresis, Udiaeresis, NoSymbol, NoSymbol, NoSymbol, NoSymbol, union, NoSymbol ] }; + key { [ odiaeresis, Odiaeresis, NoSymbol, U03F5, NoSymbol, NoSymbol, intersection, NoSymbol ] }; + key { [ b, B, NoSymbol, Greek_beta, NoSymbol, NoSymbol, U21D0, NoSymbol ] }; + key { [ p, P, NoSymbol, Greek_pi, NoSymbol, NoSymbol, Greek_PI, NoSymbol ] }; + key { [ w, W, NoSymbol, Greek_omega, NoSymbol, NoSymbol, Greek_OMEGA, NoSymbol ] }; + key { [ m, M, NoSymbol, Greek_mu, NoSymbol, NoSymbol, ifonlyif, NoSymbol ] }; + key { [ j, J, NoSymbol, Greek_theta, NoSymbol, NoSymbol, Greek_THETA, NoSymbol ] }; +}; + +partial alphanumeric_keys modifier_keys keypad_keys +xkb_symbols "koy" { + + include "de(koy_base)" + + name[Group1]= "German (KOY)"; + + include "shift(both_capslock)" + include "level3(caps_switch)" + include "level3(bksl_switch)" + include "level5(lsgt_switch_lock)" + include "level5(ralt_switch_lock)" +}; + +partial alphanumeric_keys +xkb_symbols "bone_base" { + include "de(neo_base)" + + key.type[Group1] = "EIGHT_LEVEL_LEVEL_FIVE_LOCK"; + key { [ comma, endash, NoSymbol, U03F1, NoSymbol, NoSymbol, U21D2, NoSymbol ] }; + key { [ period, enfilledcircbullet, NoSymbol, U03D1, NoSymbol, NoSymbol, U21A6, NoSymbol ] }; + + key.type[Group1] = "EIGHT_LEVEL_ALPHABETIC_LEVEL_FIVE_LOCK"; + key { [ j, J, NoSymbol, Greek_theta, NoSymbol, NoSymbol, Greek_THETA, NoSymbol ] }; + key { [ d, D, NoSymbol, Greek_delta, NoSymbol, NoSymbol, Greek_DELTA, NoSymbol ] }; + key { [ u, U, NoSymbol, NoSymbol, NoSymbol, NoSymbol, includedin, NoSymbol ] }; + key { [ a, A, NoSymbol, Greek_alpha, NoSymbol, NoSymbol, U2200, NoSymbol ] }; + key { [ x, X, NoSymbol, Greek_xi, NoSymbol, NoSymbol, Greek_XI, NoSymbol ] }; + key { [ p, P, NoSymbol, Greek_pi, NoSymbol, NoSymbol, Greek_PI, NoSymbol ] }; + key { [ h, H, NoSymbol, Greek_psi, NoSymbol, NoSymbol, Greek_PSI, NoSymbol ] }; + key { [ l, L, NoSymbol, Greek_lambda, NoSymbol, NoSymbol, Greek_LAMBDA, NoSymbol ] }; + key { [ m, M, NoSymbol, Greek_mu, NoSymbol, NoSymbol, ifonlyif, NoSymbol ] }; + key { [ w, W, NoSymbol, Greek_omega, NoSymbol, NoSymbol, Greek_OMEGA, NoSymbol ] }; + key { [ ssharp, U1E9E, NoSymbol, Greek_finalsmallsigma, NoSymbol, NoSymbol, jot, NoSymbol ] }; + key { [ c, C, NoSymbol, Greek_chi, NoSymbol, NoSymbol, U2102, NoSymbol ] }; + key { [ t, T, NoSymbol, Greek_tau, NoSymbol, NoSymbol, partialderivative, NoSymbol ] }; + key { [ i, I, NoSymbol, Greek_iota, NoSymbol, NoSymbol, integral, NoSymbol ] }; + key { [ e, E, NoSymbol, Greek_epsilon, NoSymbol, NoSymbol, U2203, NoSymbol ] }; + key { [ o, O, NoSymbol, Greek_omicron, NoSymbol, NoSymbol, elementof, NoSymbol ] }; + key { [ b, B, NoSymbol, Greek_beta, NoSymbol, NoSymbol, U21D0, NoSymbol ] }; + key { [ n, N, NoSymbol, Greek_nu, NoSymbol, NoSymbol, U2115, NoSymbol ] }; + key { [ r, R, NoSymbol, Greek_rho, NoSymbol, NoSymbol, U211D, NoSymbol ] }; + key { [ s, S, NoSymbol, Greek_sigma, NoSymbol, NoSymbol, Greek_SIGMA, NoSymbol ] }; + key { [ g, G, NoSymbol, Greek_gamma, NoSymbol, NoSymbol, Greek_GAMMA, NoSymbol ] }; + key { [ q, Q, NoSymbol, U03D5, NoSymbol, NoSymbol, U211A, NoSymbol ] }; + key { [ f, F, NoSymbol, Greek_phi, NoSymbol, NoSymbol, Greek_PHI, NoSymbol ] }; + key { [ v, V, NoSymbol, NoSymbol, NoSymbol, NoSymbol, radical, NoSymbol ] }; + key { [ udiaeresis, Udiaeresis, NoSymbol, NoSymbol, NoSymbol, NoSymbol, union, NoSymbol ] }; + key { [ adiaeresis, Adiaeresis, NoSymbol, Greek_eta, NoSymbol, NoSymbol, U2135, NoSymbol ] }; + key { [ odiaeresis, Odiaeresis, NoSymbol, U03F5, NoSymbol, NoSymbol, intersection, NoSymbol ] }; + key { [ y, Y, NoSymbol, Greek_upsilon, NoSymbol, NoSymbol, nabla, NoSymbol ] }; + key { [ z, Z, NoSymbol, Greek_zeta, NoSymbol, NoSymbol, U2124, NoSymbol ] }; + key { [ k, K, NoSymbol, Greek_kappa, NoSymbol, NoSymbol, multiply, NoSymbol ] }; +}; + +partial alphanumeric_keys modifier_keys keypad_keys +xkb_symbols "bone" { + + include "de(bone_base)" + + name[Group1]= "German (Bone)"; + + include "shift(both_capslock)" + include "level3(caps_switch)" + include "level3(bksl_switch)" + include "level5(lsgt_switch_lock)" + include "level5(ralt_switch_lock)" +}; + +partial alphanumeric_keys +xkb_symbols "bone_eszett_home_base" { + include "de(bone_base)" + + key.type[Group1] = "EIGHT_LEVEL_ALPHABETIC_LEVEL_FIVE_LOCK"; + key { [ q, Q, NoSymbol, U03D5, NoSymbol, NoSymbol, U211A, NoSymbol ] }; + key { [ ssharp, U1E9E, NoSymbol, Greek_finalsmallsigma, NoSymbol, NoSymbol, jot, NoSymbol ] }; +}; + +partial alphanumeric_keys modifier_keys keypad_keys +xkb_symbols "bone_eszett_home" { + + include "de(bone_eszett_home_base)" + + name[Group1]= "German (Bone, eszett in the home row)"; + + include "shift(both_capslock)" + include "level3(caps_switch)" + include "level3(bksl_switch)" + include "level5(lsgt_switch_lock)" + include "level5(ralt_switch_lock)" +}; + +partial alphanumeric_keys +xkb_symbols "neo_qwertz_base" { + include "de(neo_base)" + + key.type[Group1] = "EIGHT_LEVEL_LEVEL_FIVE_LOCK"; + key { [ comma, endash, NoSymbol, U03F1, NoSymbol, NoSymbol, U21D2, NoSymbol ] }; + key { [ period, enfilledcircbullet, NoSymbol, U03D1, NoSymbol, NoSymbol, U21A6, NoSymbol ] }; + key { [ minus, emdash, NoSymbol, U2011, NoSymbol, NoSymbol, hyphen, NoSymbol ] }; + + key.type[Group1] = "EIGHT_LEVEL_ALPHABETIC_LEVEL_FIVE_LOCK"; + key { [ ssharp, U1E9E, NoSymbol, Greek_finalsmallsigma, NoSymbol, NoSymbol, jot, NoSymbol ] }; + key { [ q, Q, NoSymbol, U03D5, NoSymbol, NoSymbol, U211A, NoSymbol ] }; + key { [ w, W, NoSymbol, Greek_omega, NoSymbol, NoSymbol, Greek_OMEGA, NoSymbol ] }; + key { [ e, E, NoSymbol, Greek_epsilon, NoSymbol, NoSymbol, U2203, NoSymbol ] }; + key { [ r, R, NoSymbol, Greek_rho, NoSymbol, NoSymbol, U211D, NoSymbol ] }; + key { [ t, T, NoSymbol, Greek_tau, NoSymbol, NoSymbol, partialderivative, NoSymbol ] }; + key { [ z, Z, NoSymbol, Greek_zeta, NoSymbol, NoSymbol, U2124, NoSymbol ] }; + key { [ u, U, NoSymbol, NoSymbol, NoSymbol, NoSymbol, includedin, NoSymbol ] }; + key { [ i, I, NoSymbol, Greek_iota, NoSymbol, NoSymbol, integral, NoSymbol ] }; + key { [ o, O, NoSymbol, Greek_omicron, NoSymbol, NoSymbol, elementof, NoSymbol ] }; + key { [ p, P, NoSymbol, Greek_pi, NoSymbol, NoSymbol, Greek_PI, NoSymbol ] }; + key { [ udiaeresis, Udiaeresis, NoSymbol, NoSymbol, NoSymbol, NoSymbol, union, NoSymbol ] }; + key { [ a, A, NoSymbol, Greek_alpha, NoSymbol, NoSymbol, U2200, NoSymbol ] }; + key { [ s, S, NoSymbol, Greek_sigma, NoSymbol, NoSymbol, Greek_SIGMA, NoSymbol ] }; + key { [ d, D, NoSymbol, Greek_delta, NoSymbol, NoSymbol, Greek_DELTA, NoSymbol ] }; + key { [ f, F, NoSymbol, Greek_phi, NoSymbol, NoSymbol, Greek_PHI, NoSymbol ] }; + key { [ g, G, NoSymbol, Greek_gamma, NoSymbol, NoSymbol, Greek_GAMMA, NoSymbol ] }; + key { [ h, H, NoSymbol, Greek_psi, NoSymbol, NoSymbol, Greek_PSI, NoSymbol ] }; + key { [ j, J, NoSymbol, Greek_theta, NoSymbol, NoSymbol, Greek_THETA, NoSymbol ] }; + key { [ k, K, NoSymbol, Greek_kappa, NoSymbol, NoSymbol, multiply, NoSymbol ] }; + key { [ l, L, NoSymbol, Greek_lambda, NoSymbol, NoSymbol, Greek_LAMBDA, NoSymbol ] }; + key { [ odiaeresis, Odiaeresis, NoSymbol, U03F5, NoSymbol, NoSymbol, intersection, NoSymbol ] }; + key { [ adiaeresis, Adiaeresis, NoSymbol, Greek_eta, NoSymbol, NoSymbol, U2135, NoSymbol ] }; + key { [ y, Y, NoSymbol, Greek_upsilon, NoSymbol, NoSymbol, nabla, NoSymbol ] }; + key { [ x, X, NoSymbol, Greek_xi, NoSymbol, NoSymbol, Greek_XI, NoSymbol ] }; + key { [ c, C, NoSymbol, Greek_chi, NoSymbol, NoSymbol, U2102, NoSymbol ] }; + key { [ v, V, NoSymbol, NoSymbol, NoSymbol, NoSymbol, radical, NoSymbol ] }; + key { [ b, B, NoSymbol, Greek_beta, NoSymbol, NoSymbol, U21D0, NoSymbol ] }; + key { [ n, N, NoSymbol, Greek_nu, NoSymbol, NoSymbol, U2115, NoSymbol ] }; + key { [ m, M, NoSymbol, Greek_mu, NoSymbol, NoSymbol, ifonlyif, NoSymbol ] }; +}; + +partial alphanumeric_keys modifier_keys keypad_keys +xkb_symbols "neo_qwertz" { + + include "de(neo_qwertz_base)" + + name[Group1]= "German (Neo, QWERTZ)"; + + include "shift(both_capslock)" + include "level3(caps_switch)" + include "level3(bksl_switch)" + include "level5(lsgt_switch_lock)" + include "level5(ralt_switch_lock)" +}; + +partial alphanumeric_keys +xkb_symbols "neo_qwerty_base" { + include "de(neo_qwertz_base)" + + key.type[Group1] = "EIGHT_LEVEL_ALPHABETIC_LEVEL_FIVE_LOCK"; + key { [ y, Y, NoSymbol, Greek_upsilon, NoSymbol, NoSymbol, nabla, NoSymbol ] }; + key { [ z, Z, NoSymbol, Greek_zeta, NoSymbol, NoSymbol, U2124, NoSymbol ] }; +}; + +partial alphanumeric_keys modifier_keys keypad_keys +xkb_symbols "neo_qwerty" { + + include "de(neo_qwerty_base)" + + name[Group1]= "German (Neo, QWERTY)"; + + include "shift(both_capslock)" + include "level3(caps_switch)" + include "level3(bksl_switch)" + include "level5(lsgt_switch_lock)" + include "level5(ralt_switch_lock)" +}; + +partial alphanumeric_keys +xkb_symbols "lld" { + include "de(basic)" + name[Group1] = "German (Ladin)"; + + key { [ p, P, ediaeresis, Ediaeresis ] }; +}; diff --git a/developer/src/kmc-convert/test/data/tests-xkb-kmn/de-complex b/developer/src/kmc-convert/test/data/tests-xkb-kmn/de-complex new file mode 100644 index 00000000000..32ebbf4813c --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-xkb-kmn/de-complex @@ -0,0 +1,47 @@ +// Common Latin alphabet layout +// 0x1001E9E on key key , key , key added for testing purposes by SAB +// can now process +// comments ( // and /* */) +// different key.type +// defined xkb_symbols block +// read variant from variable +default xkb_symbols "basic" { + + key { [ /*some comment in the middle*/ 1, exclam, onesuperior, exclamdown ] }; + key { [ 2, quotedbl, twosuperior, oneeighth ] }; // some comment at the end + key { [ 3, section, threesuperior, sterling ] }; + key { [ r, /*a comment in the middle*/ R, paragraph, registered ] }; // plus some comment at the end + + key {type[Group1]="FOUR_LEVEL_PLUS_LOCK", symbols[Group1]= + [ssharp, question, backslash, questiondown, B ]}; + + + key.type[Group1] = "EIGHT_LEVEL"; + // t,T,ŧ ,Ŧ ,ṭ,ṫ,ţ,ᵗ + key { [ t, T, tslash,Tslash ,U1E6D ,U1E6B ,U0163 ,U1D57] }; + + key.type[Group1] = "ONE_LEVEL"; + key { [Z] }; + key { [U] }; + key { [I] }; + // key { [ z, Z, leftarrow, yen ] }; + // key { [ u, U, downarrow, uparrow ] }; + // key { [ i, I, rightarrow, idotless ] }; + + key.type[Group1] = "DEFAULT_LEVEL"; + key { [ o, O, oslash, Ooblique ] }; + key { [ p, P, thorn, THORN ] }; + +}; + + +partial alphanumeric_keys +xkb_symbols "deadtilde" { + // previous standard German layout with tilde as dead key + + include "de(basic)" + name[Group1]="German (dead tilde)"; + + key { [ plus, asterisk, dead_tilde, dead_macron ] }; +}; + diff --git a/developer/src/kmc-convert/test/data/tests-xkb-kmn/de-simple b/developer/src/kmc-convert/test/data/tests-xkb-kmn/de-simple new file mode 100644 index 00000000000..0fdb26b6eca --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-xkb-kmn/de-simple @@ -0,0 +1,59 @@ +// Common Latin alphabet layout +// 0x1001E9E on key key , key , key added for testing purposes by SAB + +default xkb_symbols "basic" { + + key { [ 1, exclam, onesuperior, exclamdown ] }; + key { [ 2, quotedbl, twosuperior, oneeighth ] }; + key { [ 3, section, threesuperior, sterling ] }; + key { [ 4, dollar, onequarter, currency ] }; + key { [ 5, percent, onehalf, threeeighths ] }; + key { [ 6, asciicircum, threequarters, fiveeighths] }; + key { [ 7, ampersand, braceleft, seveneighths ] }; + key { [ 8, asterisk, bracketleft, trademark ] }; + key { [ 9, parenleft, bracketright, plusminus ] }; + key { [ 0, parenright, braceright, degree ] }; + key { [ ssharp, question, backslash, 0x1001E9E ] }; + key { [ NoSymbol, NoSymbol, NoSymbol, 0x1001E9E] }; + + + key { [ q, Q, at, Greek_OMEGA ] }; + key { [ w, W, lstroke, Lstroke ] }; + key { [ e, E, EuroSign, EuroSign ] }; + key { [ r, R, paragraph, registered ] }; + key { [ t, T, tslash, Tslash ] }; + key { [ z, Z, leftarrow, yen ] }; + key { [ u, U, downarrow, uparrow ] }; + key { [ i, I, rightarrow, idotless ] }; + key { [ o, O, oslash, Ooblique ] }; + key { [ p, P, thorn, THORN ] }; + key { [udiaeresis, Udiaeresis, NoSymbol, NoSymbol ] }; + key { [ plus, asterisk, asciitilde, macron ] }; + + key { [ a, A, ae, AE ] }; + key { [ s, S, ssharp, section ] }; + key { [ d, D, eth, ETH ] }; + key { [ f, F, dstroke, ordfeminine ] }; + key { [ g, G, eng, ENG ] }; + key { [ h, H, hstroke, Hstroke ] }; + key { [ j, J, NoSymbol, NoSymbol ] }; + key { [ k, K, kra, ampersand ] }; + key { [ l, L, lstroke, Lstroke ] }; + key { [odiaeresis, Odiaeresis, NoSymbol, 0x1001E9E] }; + key { [adiaeresis, Adiaeresis, NoSymbol, NoSymbol ] }; + key { [NoSymbol, degree, U2032, U2033 ] }; + + key { [numbersign, apostrophe,rightsinglequotemark,NoSymbol ] }; + key { [ y, Y, guillemotright, U203A ] }; + key { [ x, X, guillemotleft, U2039 ] }; + key { [ c, C, cent, copyright ] }; + key { [ v, V, doublelowquotemark, singlelowquotemark ] }; + key { [ b, B, leftdoublequotemark, leftsinglequotemark ] }; + key { [ n, N, rightdoublequotemark, rightsinglequotemark] }; + key { [ m, M, mu, masculine ] }; + key { [ comma, semicolon, periodcentered, multiply ] }; + key { [ period, colon, U2026, division ] }; + key { [ minus, underscore, endash, emdash ] }; + key { [ less, greater, bar, 0x1001E9E ] }; + +}; diff --git a/developer/src/kmc-convert/test/data/tests-xkb-kmn/de-simple_basic b/developer/src/kmc-convert/test/data/tests-xkb-kmn/de-simple_basic new file mode 100644 index 00000000000..2595abc8f5c --- /dev/null +++ b/developer/src/kmc-convert/test/data/tests-xkb-kmn/de-simple_basic @@ -0,0 +1,229 @@ + +c .................................................................................................................. +c .................................................................................................................. +c Keyman keyboard generated by kmn-convert version: 19.0.197 +c from Ukelele file: C:\Projects\keyman\keyman\developer\src\kmc-convert\test\data\tests-xkb-kmn\de_simple(basic) +c .................................................................................................................. +c .................................................................................................................. + +store(&TARGETS) 'desktop' + +begin Unicode > use(main) + +group(main) using keys + + ++ [K_1] > '1' ++ [SHIFT K_1] > '!' ++ [ALT K_1] > '¹' ++ [SHIFT ALT K_1] > '¡' + ++ [K_2] > '2' ++ [SHIFT K_2] > '"' ++ [ALT K_2] > '²' ++ [SHIFT ALT K_2] > '******* keysym name not found!oneeighth' + ++ [K_3] > '3' ++ [SHIFT K_3] > '§' ++ [ALT K_3] > '³' ++ [SHIFT ALT K_3] > '£' + ++ [K_4] > '4' ++ [SHIFT K_4] > '$' ++ [ALT K_4] > '¼' ++ [SHIFT ALT K_4] > '¤' + ++ [K_5] > '5' ++ [SHIFT K_5] > '%' ++ [ALT K_5] > '½' ++ [SHIFT ALT K_5] > '******* keysym name not found!threeeighths' + ++ [K_6] > '6' ++ [SHIFT K_6] > 'ˆ' ++ [ALT K_6] > '¾' ++ [SHIFT ALT K_6] > '******* keysym name not found!fiveeighths' + ++ [K_7] > '7' ++ [SHIFT K_7] > '&' ++ [ALT K_7] > '{' ++ [SHIFT ALT K_7] > '******* keysym name not found!seveneighths' + ++ [K_8] > '8' ++ [SHIFT K_8] > '*' ++ [ALT K_8] > '[' ++ [SHIFT ALT K_8] > '******* keysym name not found!trademark' + ++ [K_9] > '9' ++ [SHIFT K_9] > '(' ++ [ALT K_9] > ']' ++ [SHIFT ALT K_9] > '±' + ++ [K_0] > '0' ++ [SHIFT K_0] > ')' ++ [ALT K_0] > '}' ++ [SHIFT ALT K_0] > '°' + ++ [K_HYPHEN] > 'ß' ++ [SHIFT K_HYPHEN] > '?' ++ [ALT K_HYPHEN] > '\' ++ [SHIFT ALT K_HYPHEN] > '******* keysym name not found! - with 0x :0x1001E9E' + ++ [SHIFT ALT K_EQUAL] > '******* keysym name not found! - with 0x :0x1001E9E' + ++ [K_Q] > 'q' ++ [SHIFT K_Q] > 'Q' ++ [ALT K_Q] > '@' ++ [SHIFT ALT K_Q] > '******* keysym name not found!Greek_OMEGA' + ++ [K_W] > 'w' ++ [SHIFT K_W] > 'W' ++ [ALT K_W] > 'Ƴ' ++ [SHIFT ALT K_W] > 'ƣ' + ++ [K_E] > 'e' ++ [SHIFT K_E] > 'E' ++ [ALT K_E] > '******* keysym name not found!EuroSign' ++ [SHIFT ALT K_E] > '******* keysym name not found!EuroSign' + ++ [K_R] > 'r' ++ [SHIFT K_R] > 'R' ++ [ALT K_R] > '¶' ++ [SHIFT ALT K_R] > '®' + ++ [K_T] > 't' ++ [SHIFT K_T] > 'T' ++ [ALT K_T] > 'μ' ++ [SHIFT ALT K_T] > 'ά' + ++ [K_Y] > 'z' ++ [SHIFT K_Y] > 'Z' ++ [ALT K_Y] > '******* keysym name not found!leftarrow' ++ [SHIFT ALT K_Y] > '¥' + ++ [K_U] > 'u' ++ [SHIFT K_U] > 'U' ++ [ALT K_U] > '******* keysym name not found!downarrow' ++ [SHIFT ALT K_U] > '******* keysym name not found!uparrow' + ++ [K_I] > 'i' ++ [SHIFT K_I] > 'I' ++ [ALT K_I] > '******* keysym name not found!rightarrow' ++ [SHIFT ALT K_I] > 'ʹ' + ++ [K_O] > 'o' ++ [SHIFT K_O] > 'O' ++ [ALT K_O] > 'ø' ++ [SHIFT ALT K_O] > '******* keysym name not found!Ooblique' + ++ [K_P] > 'p' ++ [SHIFT K_P] > 'P' ++ [ALT K_P] > 'þ' ++ [SHIFT ALT K_P] > 'Þ' + ++ [K_LBRKT] > 'ü' ++ [SHIFT K_LBRKT] > 'Ü' + ++ [K_RBRKT] > '+' ++ [SHIFT K_RBRKT] > '*' ++ [ALT K_RBRKT] > '~' ++ [SHIFT ALT K_RBRKT] > '¯' + ++ [K_A] > 'a' ++ [SHIFT K_A] > 'A' ++ [ALT K_A] > 'æ' ++ [SHIFT ALT K_A] > 'Æ' + ++ [K_S] > 's' ++ [SHIFT K_S] > 'S' ++ [ALT K_S] > 'ß' ++ [SHIFT ALT K_S] > '§' + ++ [K_D] > 'd' ++ [SHIFT K_D] > 'D' ++ [ALT K_D] > 'ð' ++ [SHIFT ALT K_D] > 'Ð' + ++ [K_F] > 'f' ++ [SHIFT K_F] > 'F' ++ [ALT K_F] > 'ǰ' ++ [SHIFT ALT K_F] > 'ª' + ++ [K_G] > 'g' ++ [SHIFT K_G] > 'G' ++ [ALT K_G] > 'ο' ++ [SHIFT ALT K_G] > 'ν' + ++ [K_H] > 'h' ++ [SHIFT K_H] > 'H' ++ [ALT K_H] > 'ʱ' ++ [SHIFT ALT K_H] > 'ʡ' + ++ [K_J] > 'j' ++ [SHIFT K_J] > 'J' + ++ [K_K] > 'k' ++ [SHIFT K_K] > 'K' ++ [ALT K_K] > '΢' ++ [SHIFT ALT K_K] > '&' + ++ [K_L] > 'l' ++ [SHIFT K_L] > 'L' ++ [ALT K_L] > 'Ƴ' ++ [SHIFT ALT K_L] > 'ƣ' + ++ [K_COLON] > 'ö' ++ [SHIFT K_COLON] > 'Ö' ++ [SHIFT ALT K_COLON] > '******* keysym name not found! - with 0x :0x1001E9E' + ++ [K_QUOTE] > 'ä' ++ [SHIFT K_QUOTE] > 'Ä' + ++ [K_Z] > 'y' ++ [SHIFT K_Z] > 'Y' ++ [ALT K_Z] > '»' ++ [SHIFT ALT K_Z] > '******* keysym name not found! - with Uxxxx :U203A' + ++ [K_X] > 'x' ++ [SHIFT K_X] > 'X' ++ [ALT K_X] > '«' ++ [SHIFT ALT K_X] > '******* keysym name not found! - with Uxxxx :U2039' + ++ [K_C] > 'c' ++ [SHIFT K_C] > 'C' ++ [ALT K_C] > '¢' ++ [SHIFT ALT K_C] > '©' + ++ [K_V] > 'v' ++ [SHIFT K_V] > 'V' ++ [ALT K_V] > '******* keysym name not found!doublelowquotemark' ++ [SHIFT ALT K_V] > '******* keysym name not found!singlelowquotemark' + ++ [K_B] > 'b' ++ [SHIFT K_B] > 'B' ++ [ALT K_B] > '******* keysym name not found!leftdoublequotemark' ++ [SHIFT ALT K_B] > '******* keysym name not found!leftsinglequotemark' + ++ [K_N] > 'n' ++ [SHIFT K_N] > 'N' ++ [ALT K_N] > '******* keysym name not found!rightdoublequotemark' ++ [SHIFT ALT K_N] > '******* keysym name not found!rightsinglequotemark' + ++ [K_M] > 'm' ++ [SHIFT K_M] > 'M' ++ [ALT K_M] > 'µ' ++ [SHIFT ALT K_M] > 'º' + ++ [K_COMMA] > ',' ++ [SHIFT K_COMMA] > ';' ++ [ALT K_COMMA] > '·' ++ [SHIFT ALT K_COMMA] > '×' + ++ [K_PERIOD] > '.' ++ [SHIFT K_PERIOD] > ':' ++ [ALT K_PERIOD] > '******* keysym name not found! - with Uxxxx :U2026' ++ [SHIFT ALT K_PERIOD] > '÷' + ++ [K_SLASH] > '-' ++ [SHIFT K_SLASH] > '_' ++ [ALT K_SLASH] > '******* keysym name not found!endash' ++ [SHIFT ALT K_SLASH] > '******* keysym name not found!emdash' diff --git a/developer/src/kmc-convert/test/helpers/index.ts b/developer/src/kmc-convert/test/helpers/index.ts index ea6d9d0e3db..827a485c9b5 100644 --- a/developer/src/kmc-convert/test/helpers/index.ts +++ b/developer/src/kmc-convert/test/helpers/index.ts @@ -1,5 +1,5 @@ /* - * Keyman is copyright (C) SIL International. MIT License. + * Keyman is copyright (C) SIL Global. MIT License. * * Helpers and utilities for the Mocha tests. */ diff --git a/developer/src/kmc-convert/test/kmc-convert-convert/keylayout-to-kmn-converter.tests.ts b/developer/src/kmc-convert/test/kmc-convert-convert/keylayout-to-kmn-converter.tests.ts new file mode 100644 index 00000000000..c74789e9fdd --- /dev/null +++ b/developer/src/kmc-convert/test/kmc-convert-convert/keylayout-to-kmn-converter.tests.ts @@ -0,0 +1,810 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by S. Schmitt on 2025-05-12 + * + * Tests for KeylayoutToKmnConverter, KeylayoutFileReader, KmnFileWriter + * + */ +/* +import 'mocha'; +import { assert } from 'chai'; +import * as NodeAssert from 'node:assert'; +import { compilerTestCallbacks, compilerTestOptions, makePathToFixture } from './../helpers/index.js'; +import { ActionStateOutput, KeylayoutFileData, KeylayoutToKmnConverter, Rule } from '../../src/kmc-convert-convert/keylayout-to-kmn-converter.js'; +import { KeylayoutFileReader } from '../../src//kmc-convert-read/keylayout-file-reader.js'; +import { ConverterMessages } from '../../src/converter-messages.js'; + +describe('KeylayoutToKmnConverter', function () { + + before(function () { + compilerTestCallbacks.clear(); + }); + + describe('RunSpecialTestFiles', function () { + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + [makePathToFixture('../data/tests-keylayout-kmn/Test_C0.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_C1.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_C2.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_C2_several.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_C3.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_C3_several.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_C0_C1_C2_C3.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_maxKeyCode.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_messages.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_messages_controlCharacter.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_messages_superior_C2.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_messages_superior_C3.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_duplicate_missing_keycode.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_modifier.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_modifierNoCaps.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_differentAmountOfKeysInBehaviours.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_duplicate_missing_keys.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_duplicate_keys.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_ambiguous_keys.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_nr_elements.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_differentEncodings.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_ExtraWarning.keylayout')], + ].forEach(function (files) { + it(files + " should give no errors ", async function () { + sut.run(files[0]); + assert.isTrue(compilerTestCallbacks.messages.length === 0); + }); + }); + }); + + describe('RunTestFiles resulting in errors ', function () { + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + [makePathToFixture('../data/tests-keylayout-kmn/Test_moreKeyMapThanKeyMapselectERROR.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_moreKeyMapThanKeyMapselectAndJisERROR.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_moreKeyMapThanKeyMapselectERROR.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_MissingkeyERROR.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_MissingkeyMapERROR.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_MissingLayoutsERROR.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_MissingmodifierMapERROR.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_MissingkeyMapSetERROR.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_MissingActionsERROR.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_MissingTerminatorsERROR.keylayout')], + [makePathToFixture('../data/tests-keylayout-kmn/Test_MissingAllERROR.keylayout')], + ].forEach(function (files) { + it(files + " should give an error ", async function () { + sut.run(files[0]); + assert.isTrue(compilerTestCallbacks.messages.length > 0); + }); + }); + }); + + describe('RunSpecialTestFiles - create Error: unsupported characters', function () { + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + [makePathToFixture('../data/tests-keylayout-kmn/Test_characters.keylayout')], + ].forEach(function (files) { + it(files + " should give Error: unsupported characters ", async function () { + sut.run(files[0]); + assert.isTrue(compilerTestCallbacks.messages.length === 1); + }); + }); + }); + + describe('RunSpecialTestFiles - create Error: undefined action', function () { + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + [makePathToFixture('../data/tests-keylayout-kmn/Test_undefinedAction.keylayout')], + ].forEach(function (files) { + it(files + " should give Error: undefined action detected", async function () { + sut.run(files[0]); + assert.isTrue(compilerTestCallbacks.messages.length === 1); + assert.equal(compilerTestCallbacks.messages[0].code, 5292040); + }); + }); + }); + + describe('run() ', function () { + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + + it('run() should throw on null input file name and null output file name', async function () { + // note, could use 'chai as promised' library to make this more fluent: + const result = sut.run(null, null); + assert.isNotNull(result); + assert.equal(compilerTestCallbacks.messages.length, 1); + assert.deepEqual(compilerTestCallbacks.messages[0], ConverterMessages.Error_FileNotFound({ inputFilename: null })); + }); + + it('run() should throw on null input file name and empty output file name', async function () { + const result = sut.run(null, ''); + assert.isNotNull(result); + assert.equal(compilerTestCallbacks.messages.length, 1); + assert.deepEqual(compilerTestCallbacks.messages[0], ConverterMessages.Error_FileNotFound({ inputFilename: null })); + }); + + it('run() should throw on null input file name and unknown output file name', async function () { + const result = sut.run(null, 'X'); + assert.isNotNull(result); + assert.equal(compilerTestCallbacks.messages.length, 1); + assert.deepEqual(compilerTestCallbacks.messages[0], ConverterMessages.Error_FileNotFound({ inputFilename: null })); + }); + + it('run() should throw on unavailable input file name and null output file name', async function () { + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/Unavailable.keylayout'); + const result = sut.run(inputFilename, null); + assert.isNotNull(result); + assert.equal(compilerTestCallbacks.messages.length, 2); + assert.deepEqual(compilerTestCallbacks.messages[0], ConverterMessages.Error_UnableToRead()); + assert.equal(compilerTestCallbacks.messages[1].code, 5292037); + }); + }); + + describe('Run kmc-convert with or without outputfile name', async function () { + this.timeout(5000); // allow longer time for this test + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const infile = '../data/tests-keylayout-kmn/Test.keylayout'; + [ + [makePathToFixture('../data/tests-keylayout-kmn/Test.kmn')], + [makePathToFixture('')], + [], + [null], + [makePathToFixture('../data/tests-keylayout-kmn/test_OtherOutputName.kmn')], + [makePathToFixture('../data/tests-keylayout-kmn/OutputXName.bb')], + ].forEach(function (files) { + it(infile + " should run ", async function () { + await NodeAssert.doesNotReject(async () => sut.run(makePathToFixture(infile), files[0])); + assert.equal(compilerTestCallbacks.messages.length, 0); + }); + }); + }); + + describe('convert() ', function () { + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + + // ProcessedData from usable file + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/Test.keylayout'); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + const converted = sut.convertBound.convert(read, inputFilename.replace(/\.keylayout$/, '.kmn')); + + // ProcessedData from unavailable file + const inputFilenameUnavailable = makePathToFixture('../data/tests-keylayout-kmn/X.keylayout'); + const readUnavailable = sutR.read(compilerTestCallbacks.loadFile(inputFilenameUnavailable)); + const convertedUnavailable = sut.convertBound.convert(readUnavailable, inputFilenameUnavailable.replace(/\.keylayout$/, '.kmn')); + + // ProcessedData from empty file + const inputFilenameEmpty = makePathToFixture(''); + const readEmpty = sutR.read(compilerTestCallbacks.loadFile(inputFilenameEmpty)); + const convertedEmpty = sut.convertBound.convert(readEmpty, inputFilenameEmpty); + + it('should return converted array on correct input', async function () { + assert.isTrue(converted.rules.length !== 0); + }); + + it('should return empty on empty name as input', async function () { + assert.isNull(convertedUnavailable); + }); + + it('should return empty on empty input', async function () { + assert.isNull(convertedEmpty); + }); + + it('should return empty array of rules on null input', async function () { + const convertedRule = sut.convertBound.convert(null, 'ABC.kmn'); + assert.isNull(convertedRule); + }); + }); + + describe('createKmnModifier ', function () { + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + [' ', true, 'NCAPS'], + [' ', false, ''], + + ['NCAPS', true, 'NCAPS'], + ['NCAPS', false, ''], + ['caps', true, 'CAPS'], + ['CAPS', true, 'CAPS'], + ['CAPS?', true, 'NCAPS'], + ['CAPS', false, 'CAPS'], + ['CAPS?', false, ''], + ['CAPS? NCAPS', true, 'NCAPS'], + ['CAPS NCAPS', true, 'CAPS NCAPS'], + ['caps ncaps', true, 'CAPS NCAPS'], + ['NCAPS shift', true, 'NCAPS SHIFT'], + ['shift', true, 'NCAPS SHIFT'], + ['leftshift', true, 'NCAPS SHIFT'], + ['rightShift', true, 'NCAPS SHIFT'], + ['shift', false, 'SHIFT'], + ['leftshift', false, 'SHIFT'], + ['rightShift', false, 'SHIFT'], + ['shift rightShift?', true, 'NCAPS SHIFT'], + ['rightShift?', true, 'NCAPS'], + ['shift Shift?', true, 'NCAPS SHIFT'], + ['shift rightShift? caps? rightOption? rightControl', true, 'NCAPS SHIFT RCTRL'], + ['leftshift rightShift? caps? rightOption? rightControl', true, 'NCAPS SHIFT RCTRL'], + ['anycontrol', true, 'NCAPS CTRL'], + ['shift?', true, 'NCAPS'], + ['?', true, 'NCAPS'], + ['?', false, ''], + ['', true, 'NCAPS'], + [' ', false, ''], + ['wrongModifierName', false, 'wrongModifierName'], + ['shift', false, 'SHIFT'], + ['shift command', true, 'NCAPS SHIFT command'], + ['rshift', true, 'NCAPS SHIFT'], + ['rshift', false, 'SHIFT'], + ['rightshift', true, 'NCAPS SHIFT'], + ['riGhtsHift', true, 'NCAPS SHIFT'], + ['LEFTCONTROL', true, 'NCAPS LCTRL'], + ['RCONTROL', true, 'NCAPS RCTRL'], + ['leftoption', true, 'NCAPS LALT'], + ['loption', true, 'NCAPS LALT'], + ['rightoption', true, 'NCAPS RALT'], + ['roption', true, 'NCAPS RALT'], + ].forEach(function (values) { + it(('should convert "' + values[0] + '"').padEnd(36, " ") + 'to "' + values[2] + '"', async function () { + const result = sut.createKmnModifier(values[0] as string, values[1] as boolean); + assert.equal(result, values[2]); + }); + }); + }); + + describe('isAcceptableKeymanModifier ', function () { + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + ['NCAPS', true], + ['NxCAPS', false], + ['SHIFT', true], + ['ALT', true], + ['RALT', true], + ['LALT', true], + ['CTRL', true], + ['LCTRL', true], + ['RCTRL', true], + ['LCTRL CAPS', true], + ['LCTRL X', false], + ['', true], + [null, false], + ].forEach(function (values) { + it(("isAcceptableKeymanModifier(" + values[0] + ")").padEnd(38, " ") + ' should return ' + values[1], async function () { + const result = sut.isAcceptableKeymanModifier(values[0] as string); + assert.equal(result, values[1]); + }); + }); + }); + + describe('mapUkeleleKeycodeToVK ', function () { + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + [0x00, 'K_A'], + [0x31, 'K_SPACE'], + [0x18, 'K_EQUAL'], + [0x10, 'K_Y'], + [0x18, 'K_EQUAL'], + [0x21, 'K_LBRKT'], + [0x999, ''], + [-1, ''], + [null, ''], + [undefined, ''], + [, ''], + ].forEach(function (values) { + it(("mapUkeleleKeycodeToVK(" + values[0] + ")").padEnd(26, " ") + "should return " + "'" + values[1] + "'", async function () { + const result = sut.mapUkeleleKeycodeToVK(values[0] as number); + assert.equal(result, values[1]); + }); + }); + }); + + describe('checkIfCapsIsUsed ', function () { + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + [[['caps', 'xxx'], ['yyy']], true], + [[['Caps', 'xxx'], ['yyy']], true], + [[['CaPs', 'xxx'], ['yyy']], true], + [[['Caps?', 'xxx'], ['yyy']], false], + [[['zzz', 'xxx'], ['Caps?']], false], + [[['caps?', 'xxx'], ['yyy']], false], + [[['zzz', 'xxx'], ['yyy']], false], + [[['shift', 'xxx'], ['caps']], true], + [[['shift', 'caps'], ['yyy']], true], + [[['caps', 'xxx'], ['caps']], true], + [[['', 'someWordWithCaps'], ['']], false], + [null, false], + [[], false], + [[['', ''], ['']], false], + [[[' ', ' '], [' ']], false], + ].forEach(function (values) { + it(("checkIfCapsIsUsed(" + values[0] + ")").padEnd(40, " ") + "should return " + "'" + values[1] + "'", async function () { + const result = sut.checkIfCapsIsUsed(values[0] as string[][]); + assert.isTrue(result === values[1]); + }); + }); + }); + + describe('getModifierArrayFromKeyModifierArray ', function () { + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/Test.keylayout'); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + const converted = sut.convertBound.convert(read, inputFilename.replace(/\.keylayout$/, '.kmn')); + [ + [[{ key: '0', behavior: 0 }], [['', 'shift? caps? ']]], + [[{ key: '0', behavior: 2 }], [['shift? leftShift caps? ', 'anyShift caps?', 'shift leftShift caps ', 'shift? rightShift caps? ']]], + [[{ key: '0', behavior: 999 }], [null]], + [[{ key: '999', behavior: null }], [null]], + [[{ key: '0', behavior: -999 }], [null]], + [[{ key: '0', behavior: null }], [null]], + [[{ key: '0', behavior: undefined }], [null]], + [[], []], + + ].forEach(function (values) { + it((values[1] !== null) ? + ("getModifierArrayFromKeyModifierArray('" + JSON.stringify(values[0]) + "')").padEnd(68, " ") + " should return '" + JSON.stringify(values[1]) + "'" : + ("getModifierArrayFromKeyModifierArray('" + JSON.stringify(values[0]) + "')").padEnd(68, " ") + " should return '" + "null" + "'", async function () { + const result = sut.getModifierArrayFromKeyModifierArray(converted.modifiers, values[0] as unknown as KeylayoutFileData[]); + assert.deepStrictEqual(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + }); + + describe('getKeyModifierArrayFromActionID ', function () { + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/Test.keylayout'); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + [ + ['A_16', [{ "key": "32", "behavior": "5" }]], + ['A_19', [{ "key": "45", "behavior": "5" }]], + ['A_18', [{ "key": "24", "behavior": "0" }, { "key": "24", "behavior": "5" }]], + ['unknown', []], + [undefined, []], + [null, []], + [' ', []], + ['', []], + ].forEach(function (values) { + let outstring = '[ '; + for (let i = 0; i < values[1].length; i++) { + outstring = outstring + "[ " + JSON.stringify(values[1][i]) + "], "; + } + it(("getKeyModifierArrayFromActionID('" + values[0] + "')").padEnd(57, " ") + ' should return ' + outstring.substring(0, outstring.lastIndexOf(']') + 2) + " ]", async function () { + const result = sut.getKeyModifierArrayFromActionID(read, String(values[0])); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + }); + + describe('getActionIdFromActionNext ', function () { + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/Test.keylayout'); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + [ + ['none', ''], + ['0', ''], + ['A_18', ''], + ['1', 'A_16'], + ['2', 'A_8'], + ['3', 'A_17'], + ['', ''], + [' ', ''], + ['99', ''], + [null, ''], + [undefined, ''], + ['unknown', ''], + ].forEach(function (values) { + it(("getActionIdFromActionNext('" + values[0] + "')").padEnd(49, " ") + ' should return ' + "'" + values[1] + "'", async function () { + const result = sut.getActionIdFromActionNext(read, String(values[0])); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + }); + + describe('getActionIndexFromActionId ', function () { + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/Test.keylayout'); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + [ + ['none', -1], + ['A_16', 8], + ['A_18', 10], + ['A_19', 11], + ['0', -1], + ['', -1], + [' ', -1], + [null, -1], + [undefined, -1], + ['unknown', -1], + ].forEach(function (values) { + it(("getActionIndexFromActionId('" + values[0] + "')").padEnd(50, " ") + ' should return ' + values[1], async function () { + const result = sut.getActionIndexFromActionId(read, String(values[0])); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + }); + + describe('getOutputFromActionIdNone ', function () { + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/Test.keylayout'); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + [ + ['A_14', 'u'], + ['', ''], + [' ', ''], + ['A_18', ''], + ['unknown', ''], + ].forEach(function (values) { + it( + ("getOutputFromActionIdNone('" + values[0] + "')").padEnd(56, " ") + ' should return ' + "'" + values[1] + "'", async function () { + const result = sut.getOutputFromActionIdNone(read, String(values[0])); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + + [[null, ''], + [undefined, ''], + [99, ''], + ].forEach(function (values) { + it(("getOutputFromActionIdNone('" + values[0] + "')").padEnd(56, " ") + ' should return ' + values[1], async function () { + const result = sut.getOutputFromActionIdNone(read, String(values[0])); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + }); + + describe('getKeybehaviorModOutputArrayFromKeyActionbehaviorOutputArray ', function () { + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/Test.keylayout'); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + + const b1KeycodeArr: KeylayoutFileData[] = [ + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '0', outchar: 'ˆ' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '1', outchar: 'ˆ' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '2', outchar: 'ˆ' }, + { keyCode: '6', key: 'K_Z', actionId: 'A_0', behavior: '4', outchar: 'ˆ' }, + { keyCode: '25', key: 'K_9', actionId: 'A_0', behavior: '4', outchar: 'ˆ' }, + { keyCode: '43', key: 'K_COMMA', actionId: 'A_0', behavior: '4', outchar: 'ˆ' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '3', outchar: 'ˆ' }, + { keyCode: '0', key: 'K_A', actionId: 'A_1', behavior: '2', outchar: 'Â' }, + { keyCode: '0', key: 'K_A', actionId: 'A_1', behavior: '1', outchar: 'Â' }, + { keyCode: '14', key: 'K_E', actionId: 'A_10', behavior: '0', outchar: 'ê' }, + { keyCode: '34', key: 'K_I', actionId: 'A_11', behavior: '0', outchar: 'î' }, + { keyCode: '31', key: 'K_O', actionId: 'A_13', behavior: '0', outchar: 'ô' }, + { keyCode: '32', key: 'K_U', actionId: 'A_14', behavior: '0', outchar: 'û' }, + { keyCode: '14', key: 'K_E', actionId: 'A_2', behavior: '2', outchar: 'Ê' }, + { keyCode: '14', key: 'K_E', actionId: 'A_2', behavior: '1', outchar: 'Ê' }, + { keyCode: '34', key: 'K_I', actionId: 'A_3', behavior: '2', outchar: 'Î' }, + { keyCode: '34', key: 'K_I', actionId: 'A_3', behavior: '1', outchar: 'Î' }, + { keyCode: '31', key: 'K_O', actionId: 'A_5', behavior: '2', outchar: 'Ô' }, + { keyCode: '31', key: 'K_O', actionId: 'A_5', behavior: '1', outchar: 'Ô' }, + { keyCode: '32', key: 'K_U', actionId: 'A_6', behavior: '2', outchar: 'Û' }, + { keyCode: '32', key: 'K_U', actionId: 'A_6', behavior: '1', outchar: 'Û' }, + { keyCode: '0', key: 'K_A', actionId: 'A_9', behavior: '0', outchar: 'â' } + + ]; + const b1ModifierKeyArr: KeylayoutFileData[] = [ + { actionId: 'A_0', key: 'K_SPACE', behavior: '0', modifier: 'NCAPS', outchar: 'ˆ' }, + { actionId: 'A_0', key: 'K_SPACE', behavior: '1', modifier: 'CAPS', outchar: 'ˆ' }, + { actionId: 'A_0', key: 'K_SPACE', behavior: '2', modifier: 'NCAPS SHIFT', outchar: 'ˆ' }, + { actionId: 'A_0', key: 'K_SPACE', behavior: '2', modifier: 'SHIFT CAPS', outchar: 'ˆ' }, + { actionId: 'A_0', key: 'K_Z', behavior: '4', modifier: 'NCAPS SHIFT RALT', outchar: 'ˆ' }, + { actionId: 'A_0', key: 'K_9', behavior: '4', modifier: 'NCAPS SHIFT RALT', outchar: 'ˆ' }, + { actionId: 'A_0', key: 'K_COMMA', behavior: '4', modifier: 'NCAPS SHIFT RALT', outchar: 'ˆ' }, + { actionId: 'A_0', key: 'K_SPACE', behavior: '3', modifier: 'NCAPS RALT CTRL', outchar: 'ˆ' }, + { actionId: 'A_0', key: 'K_SPACE', behavior: '3', modifier: 'NCAPS CTRL', outchar: 'ˆ' }, + { actionId: 'A_1', key: 'K_A', behavior: '2', modifier: 'NCAPS SHIFT', outchar: 'Â' }, + { actionId: 'A_1', key: 'K_A', behavior: '2', modifier: 'SHIFT CAPS', outchar: 'Â' }, + { actionId: 'A_1', key: 'K_A', behavior: '1', modifier: 'CAPS', outchar: 'Â' }, + { actionId: 'A_10', key: 'K_E', behavior: '0', modifier: 'NCAPS', outchar: 'ê' }, + { actionId: 'A_11', key: 'K_I', behavior: '0', modifier: 'NCAPS', outchar: 'î' }, + { actionId: 'A_13', key: 'K_O', behavior: '0', modifier: 'NCAPS', outchar: 'ô' }, + { actionId: 'A_14', key: 'K_U', behavior: '0', modifier: 'NCAPS', outchar: 'û' }, + { actionId: 'A_2', key: 'K_E', behavior: '2', modifier: 'NCAPS SHIFT', outchar: 'Ê' }, + { actionId: 'A_2', key: 'K_E', behavior: '2', modifier: 'SHIFT CAPS', outchar: 'Ê' }, + { actionId: 'A_2', key: 'K_E', behavior: '1', modifier: 'CAPS', outchar: 'Ê' }, + { actionId: 'A_3', key: 'K_I', behavior: '2', modifier: 'NCAPS SHIFT', outchar: 'Î' }, + { actionId: 'A_3', key: 'K_I', behavior: '2', modifier: 'SHIFT CAPS', outchar: 'Î' }, + { actionId: 'A_3', key: 'K_I', behavior: '1', modifier: 'CAPS', outchar: 'Î' }, + { actionId: 'A_5', key: 'K_O', behavior: '2', modifier: 'NCAPS SHIFT', outchar: 'Ô' }, + { actionId: 'A_5', key: 'K_O', behavior: '2', modifier: 'SHIFT CAPS', outchar: 'Ô' }, + { actionId: 'A_5', key: 'K_O', behavior: '1', modifier: 'CAPS', outchar: 'Ô' }, + { actionId: 'A_6', key: 'K_U', behavior: '2', modifier: 'NCAPS SHIFT', outchar: 'Û' }, + { actionId: 'A_6', key: 'K_U', behavior: '2', modifier: 'SHIFT CAPS', outchar: 'Û' }, + { actionId: 'A_6', key: 'K_U', behavior: '1', modifier: 'CAPS', outchar: 'Û' }, + { actionId: 'A_9', key: 'K_A', behavior: '0', modifier: 'NCAPS', outchar: 'â' } + ]; + + [[b1KeycodeArr, b1ModifierKeyArr], + [[{ keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '0', modifier: '0', outchar: 'ˆ' }], [{ actionId: 'A_0', key: 'K_SPACE', behavior: '0', modifier: 'NCAPS', outchar: 'ˆ' }]], + [[{ keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '0', modifier: '', outchar: 'ˆ' }], [{ actionId: 'A_0', key: 'K_SPACE', behavior: '0', modifier: 'NCAPS', outchar: 'ˆ' }]], + [[{ keyCode: '49', key: 'K_SPACE', actionId: '', behavior: '0', modifier: '0', outchar: 'ˆ' }], [{ actionId: '', key: 'K_SPACE', behavior: '0', modifier: 'NCAPS', outchar: 'ˆ' }]], + [[{ keyCode: '49', key: '', actionId: 'A_0', behavior: '0', modifier: '0', outchar: 'ˆ' }], [{ actionId: 'A_0', key: '', behavior: '0', modifier: 'NCAPS', outchar: 'ˆ' }]], + [[{ keyCode: '', key: 'K_SPACE', actionId: 'A_0', behavior: '0', modifier: '0', outchar: 'ˆ' }], [{ actionId: 'A_0', key: 'K_SPACE', behavior: '0', modifier: 'NCAPS', outchar: 'ˆ' }]], + [[{ keyCode: '', key: '', actionId: '', behavior: '0', modifier: '', outchar: '' }], [{ actionId: '', key: '', behavior: '0', modifier: 'NCAPS', outchar: '' }]], + ].forEach(function (values) { + const isCapsUsed = true; + const stringIn = "getKeybehaviorModOutputArrayFromKeyActionbehaviorOutputArray(['" + "', '" + "', '" + values[0][0].keyCode + "', '" + values[0][0].key + "', '" + values[0][0].actionId + "', '" + values[0][0].modifier + "', '" + values[0][0].outchar + "'])"; + const stringOut = "['" + "', '" + "', '" + values[1][0].key + "', '" + values[1][0].actionId + "', '" + "', '" + values[1][0].modifier + "', '" + values[1][0].outchar + "']"; + + it((JSON.stringify(values[1]).length > 60) ? 'an array of objects should return an array of objects' : + stringIn.padEnd(74, " ") + ' should return ' + stringOut, async function () { + const result = sut.getKeyBehaviorModOutputArrayFromKeyActionBehaviorOutputArray(read, values[0], isCapsUsed); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + + [[{ keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '0', modifier: '0', outchar: 'ˆ' }, { actionId: 'A_0', key: 'K_SPACE', behavior: '0', modifier: '', outchar: 'ˆ' }], + [{ keyCode: '', key: 'K_SPACE', actionId: 'A_0', behavior: '0', modifier: '0', outchar: 'ˆ' }, { actionId: 'A_0', key: 'K_SPACE', behavior: '0', modifier: '', outchar: 'ˆ' }], + [{ keyCode: '', key: '', actionId: '', behavior: '0', modifier: '', outchar: '' }, { actionId: '', key: '', behavior: '0', modifier: '', outchar: '' }], + ].forEach(function (values) { + const isCapsUsed = false; + const stringIn = "getKeybehaviorModOutputArrayFromKeyActionbehaviorOutputArray([ '" + values[0].keyCode + "', '" + values[0].key + "', '" + values[0].actionId + "', '" + values[0].modifier + "', '" + values[0].outchar + "'])"; + const stringOut = "['" + values[1].actionId + "', '" + "', '" + values[1].modifier + "', '" + values[1].key + "', '" + values[1].outchar + "']"; + + it(stringIn.padEnd(74, " ") + ' should return ' + stringOut, async function () { + const result = sut.getKeyBehaviorModOutputArrayFromKeyActionBehaviorOutputArray(read, [values[0]], isCapsUsed); + assert.equal(JSON.stringify(result), JSON.stringify([values[1]])); + }); + }); + + [[[], []], + [undefined, []], + [null, []], + ].forEach(function (values) { + const isCaps = true; + it(("getKeybehaviorModOutputArrayFromKeyActionbehaviorOutputArray([" + values[0] + "])").padEnd(74, " ") + ' should return ' + "[" + values[1] + "]", async function () { + const result = sut.getKeyBehaviorModOutputArrayFromKeyActionBehaviorOutputArray(read, values[0], isCaps); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + }); + + describe('getActionStateOutputArrayFromActionState ', function () { + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/Test.keylayout'); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + [['1', [ + { "id": "A_0", "state": "1", "output": "ˆ" }, + { "id": "A_1", "state": "1", "output": "Â" }, + { "id": "A_10", "state": "1", "output": "ê" }, + { "id": "A_11", "state": "1", "output": "î" }, + { "id": "A_13", "state": "1", "output": "ô" }, + { "id": "A_14", "state": "1", "output": "û" }, + { "id": "A_2", "state": "1", "output": "Ê" }, + { "id": "A_3", "state": "1", "output": "Î" }, + { "id": "A_5", "state": "1", "output": "Ô" }, + { "id": "A_6", "state": "1", "output": "Û" }, + { "id": "A_9", "state": "1", "output": "â" } + ],], + ['2', [ + { "id": "A_0", "state": "2", "output": "`" }, + { "id": "A_1", "state": "2", "output": "À" }, + { "id": "A_10", "state": "2", "output": "è" }, + { "id": "A_11", "state": "2", "output": "ì" }, + { "id": "A_13", "state": "2", "output": "ò" }, + { "id": "A_14", "state": "2", "output": "ù" }, + { "id": "A_2", "state": "2", "output": "È" }, + { "id": "A_3", "state": "2", "output": "Ì" }, + { "id": "A_5", "state": "2", "output": "Ò" }, + { "id": "A_6", "state": "2", "output": "Ù" }, + { "id": "A_9", "state": "2", "output": "à" } + ],], + ['789', [],], + ['', [],], + [' ', [],], + [123, [],], + [null, [],], + [undefined, [],], + ].forEach(function (values) { + it((JSON.stringify(values[1]).length > 30) ? + ("getActionStateOutputArrayFromActionState('" + values[0] + "')").padEnd(60, " ") + ' should return an array of objects' : + ("getActionStateOutputArrayFromActionState('" + values[0] + "')").padEnd(60, " ") + ' should return ' + "'" + JSON.stringify(values[1]) + "'", async function () { + const result = sut.getActionStateOutputArrayFromActionState(read, String(values[0])); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + }); + + describe('getActionOutputbehaviorKeyModiFromActionIDStateOutput ', function () { + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/Test.keylayout'); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + const converted = sut.convertBound.convert(read, inputFilename.replace(/\.keylayout$/, '.kmn')); + [ + ['A_1', 'A', true, + [{ "outchar": "A", "actionId": "A_1", "behavior": "1", "key": "K_A", "modifier": "CAPS" }, + { "outchar": "A", "actionId": "A_1", "behavior": "2", "key": "K_A", "modifier": "NCAPS SHIFT" }, + { "outchar": "A", "actionId": "A_1", "behavior": "2", "key": "K_A", "modifier": "SHIFT CAPS" }] + ], + ['A_1', 'A', false, + [{ "outchar": "A", "actionId": "A_1", "behavior": "1", "key": "K_A", "modifier": "CAPS" }, + { "outchar": "A", "actionId": "A_1", "behavior": "2", "key": "K_A", "modifier": "SHIFT" }, + { "outchar": "A", "actionId": "A_1", "behavior": "2", "key": "K_A", "modifier": "SHIFT CAPS" }] + ], + ['A_9', 'a', true, [{ "outchar": "a", "actionId": "A_9", "behavior": "0", "key": "K_A", "modifier": "NCAPS" }]], + ['A_9', 'a', false, [{ "outchar": "a", "actionId": "A_9", "behavior": "0", "key": "K_A", "modifier": "" }]], + ['A_9', 'a', , [{ "outchar": "a", "actionId": "A_9", "behavior": "0", "key": "K_A", "modifier": "" }]], + ['A_9', '', true, [{ "outchar": "", "actionId": "A_9", "behavior": "0", "key": "K_A", "modifier": "NCAPS" }]], + ['A_9', '', false, [{ "outchar": "", "actionId": "A_9", "behavior": "0", "key": "K_A", "modifier": "" }]], + ['', 'a', true, []], + ['', 'a', false, []], + ['', '', , []], + ].forEach(function (values) { + it((JSON.stringify(values[3]).length > 35) ? + ("getActionOutputbehaviorKeyModiFromActionIDStateOutput('" + values[0] + "', '" + values[1] + "', " + values[2] + ")").padEnd(67, " ") + ' should return an array of objects' : + ("getActionOutputbehaviorKeyModiFromActionIDStateOutput('" + values[0] + "', '" + values[1] + "', " + values[2] + ")").padEnd(67, " ") + ' should return ' + "'" + JSON.stringify(values[3]) + "'", async function () { + const result = sut.getActionOutputBehaviorKeyModiFromActionIDStateOutput(read, converted.modifiers, String(values[0]), String(values[1]), Boolean(values[2])); + assert.equal(JSON.stringify(result), JSON.stringify(values[3])); + }); + }); + }); + + describe('getKeyActionOutputArrayFromActionStateOutputArray ', function () { + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/Test.keylayout'); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + + const b6ActionIdArr: ActionStateOutput[] = [ + { "id": "A_0", "state": "1", "output": "ˆ" }, + { "id": "A_1", "state": "1", "output": "Â" }, + { "id": "A_10", "state": "1", "output": "ê" }, + { "id": "A_11", "state": "1", "output": "î" }, + { "id": "A_13", "state": "1", "output": "ô" }, + { "id": "A_14", "state": "1", "output": "û" }, + { "id": "A_2", "state": "1", "output": "Ê" }, + { "id": "A_3", "state": "1", "output": "Î" }, + { "id": "A_5", "state": "1", "output": "Ô" }, + { "id": "A_6", "state": "1", "output": "Û" }, + { "id": "A_9", "state": "1", "output": "â" } + ]; + + const b1KeycodeArr: KeylayoutFileData[] = [ + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '0', outchar: 'ˆ' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '1', outchar: 'ˆ' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '2', outchar: 'ˆ' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '3', outchar: 'ˆ' }, + { keyCode: '6', key: 'K_Z', actionId: 'A_0', behavior: '4', outchar: 'ˆ' }, + { keyCode: '25', key: 'K_9', actionId: 'A_0', behavior: '4', outchar: 'ˆ' }, + { keyCode: '43', key: 'K_COMMA', actionId: 'A_0', behavior: '4', outchar: 'ˆ' }, + { keyCode: '0', key: 'K_A', actionId: 'A_1', behavior: '1', outchar: 'Â' }, + { keyCode: '0', key: 'K_A', actionId: 'A_1', behavior: '2', outchar: 'Â' }, + { keyCode: '14', key: 'K_E', actionId: 'A_10', behavior: '0', outchar: 'ê' }, + { keyCode: '34', key: 'K_I', actionId: 'A_11', behavior: '0', outchar: 'î' }, + { keyCode: '31', key: 'K_O', actionId: 'A_13', behavior: '0', outchar: 'ô' }, + { keyCode: '32', key: 'K_U', actionId: 'A_14', behavior: '0', outchar: 'û' }, + { keyCode: '14', key: 'K_E', actionId: 'A_2', behavior: '1', outchar: 'Ê' }, + { keyCode: '14', key: 'K_E', actionId: 'A_2', behavior: '2', outchar: 'Ê' }, + { keyCode: '34', key: 'K_I', actionId: 'A_3', behavior: '1', outchar: 'Î' }, + { keyCode: '34', key: 'K_I', actionId: 'A_3', behavior: '2', outchar: 'Î' }, + { keyCode: '31', key: 'K_O', actionId: 'A_5', behavior: '1', outchar: 'Ô' }, + { keyCode: '31', key: 'K_O', actionId: 'A_5', behavior: '2', outchar: 'Ô' }, + { keyCode: '32', key: 'K_U', actionId: 'A_6', behavior: '1', outchar: 'Û' }, + { keyCode: '32', key: 'K_U', actionId: 'A_6', behavior: '2', outchar: 'Û' }, + { keyCode: '0', key: 'K_A', actionId: 'A_9', behavior: '0', outchar: 'â' } + ]; + + [[b6ActionIdArr, b1KeycodeArr], + ].forEach(function (values) { + it(("getKeyActionOutputArrayFromActionStateOutputArray([['" + JSON.stringify(values[0]) + "'],..])").padEnd(73, " ") + '1 should return an array of objects', async function () { + const result = sut.getKeyActionOutputArrayFromActionStateOutputArray(read, values[0] as ActionStateOutput[]); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + + const oneEntryResult = [ + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '0', outchar: 'ˆ' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '1', outchar: 'ˆ' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '2', outchar: 'ˆ' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '3', outchar: 'ˆ' }, + { keyCode: '6', key: 'K_Z', actionId: 'A_0', behavior: '4', outchar: 'ˆ' }, + { keyCode: '25', key: 'K_9', actionId: 'A_0', behavior: '4', outchar: 'ˆ' }, + { keyCode: '43', key: 'K_COMMA', actionId: 'A_0', behavior: '4', outchar: 'ˆ' } + ]; + + const oneEntryResultNoOutput = [ + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '0', outchar: '' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '1', outchar: '' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '2', outchar: '' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '3', outchar: '' }, + { keyCode: '6', key: 'K_Z', actionId: 'A_0', behavior: '4', outchar: '' }, + { keyCode: '25', key: 'K_9', actionId: 'A_0', behavior: '4', outchar: '' }, + { keyCode: '43', key: 'K_COMMA', actionId: 'A_0', behavior: '4', outchar: '' }, + ]; + + [[[{ "id": "A_0", "state": "1", "output": "ˆ" }], oneEntryResult], + [[{ "id": "A_0", "state": "1", "output": "" }], oneEntryResultNoOutput], + [[{ "id": "A_0", "state": "", "output": "ˆ" }], oneEntryResult], + ].forEach(function (values) { + it(("getKeyActionOutputArrayFromActionStateOutputArray(['" + JSON.stringify(values[0]) + "'])").padEnd(73, " ") + ' should return an array of objects', async function () { + const result = sut.getKeyActionOutputArrayFromActionStateOutputArray(read, values[0] as ActionStateOutput[]); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + + [[[{ "id": "", "state": "1", "output": "ˆ" }], []], + [[{ "id": "", "state": "", "output": "" }], []], + [[{ "id": " ", "state": " ", "output": "" }], []], + + ].forEach(function (values) { + it(("getKeyActionOutputArrayFromActionStateOutputArray(" + JSON.stringify(values[0]) + ")").padEnd(73, " ") + ' should return ' + "'[" + JSON.stringify(values[1]) + "]'", async function () { + const result = sut.getKeyActionOutputArrayFromActionStateOutputArray(read, values[0] as ActionStateOutput[]); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + + [[, []], + [undefined, []], + [null, []], + ].forEach(function (values) { + it(("getKeyActionOutputArrayFromActionStateOutputArray(" + JSON.stringify(values[0]) + ")").padEnd(73, " ") + ' should return ' + "'[" + JSON.stringify(values[1]) + "]'", async function () { + const result = sut.getKeyActionOutputArrayFromActionStateOutputArray(read, values[0] as ActionStateOutput[]); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + }); + + describe('createRuleData ', function () { + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + [ + [ + ['../data/tests-keylayout-kmn/Test_C0.keylayout'], + [new Rule("C0", '', '', 0, 0, '', '', 0, 0, 'NCAPS', 'K_A', new TextEncoder().encode('a')), + new Rule("C0", '', '', 0, 0, '', '', 0, 0, 'CAPS', 'K_A', new TextEncoder().encode('A')), + new Rule("C0", '', '', 0, 0, '', '', 0, 0, 'NCAPS', 'K_S', new TextEncoder().encode('s')), + new Rule("C0", '', '', 0, 0, '', '', 0, 0, 'NCAPS', 'K_D', new TextEncoder().encode('d'))] + ], + [ + ['../data/tests-keylayout-kmn/Test_C1.keylayout'], + [new Rule("C1", '', '', 0, 0, '', '', 0, 0, 'NCAPS', 'K_S', new TextEncoder().encode('s')), + new Rule("C1", '', '', 0, 0, '', '', 0, 0, 'CAPS', 'K_S', new TextEncoder().encode('S'))] + ], + [ + ['../data/tests-keylayout-kmn/Test_C2.keylayout'], + [new Rule("C2", '', '', 0, 0, 'NCAPS', 'K_U', 1, 1, 'CAPS', 'K_A', new TextEncoder().encode('Â'))], + ], + [ + ['../data/tests-keylayout-kmn/Test_C3.keylayout'], + [new Rule("C3", 'NCAPS SHIFT', 'K_D', 2, 1, 'NCAPS', 'K_U', 1, 2, 'CAPS', 'K_A', new TextEncoder().encode('Â'))] + ], + + [ + ['../data/tests-keylayout-kmn/Test_C3_several.keylayout'], + [new Rule("C3", 'NCAPS RALT', 'K_8', 3, 1, 'CAPS', 'K_U', 1, 3, 'NCAPS', 'K_A', new TextEncoder().encode('â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 3, 0, 'CAPS', 'K_U', 1, 0, 'NCAPS RALT', 'K_A', new TextEncoder().encode('â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 3, 0, 'NCAPS RALT', 'K_U', 2, 2, 'NCAPS', 'K_A', new TextEncoder().encode('â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 3, 0, 'NCAPS RALT', 'K_U', 2, 0, 'NCAPS RALT', 'K_A', new TextEncoder().encode('â'))] + ], + [ + ['../data/tests-keylayout-kmn/Test_C0_C1_C2_C3.keylayout'], + [new Rule("C0", '', '', 0, 0, '', '', 0, 0, 'CAPS', 'K_A', new TextEncoder().encode('A')), + new Rule("C2", '', '', 0, 0, 'NCAPS RALT', 'K_EQUAL', 1, 1, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C1", '', '', 0, 0, '', '', 0, 0, 'CAPS', 'K_S', new TextEncoder().encode('S')), + new Rule("C1", '', '', 0, 0, '', '', 0, 0, 'NCAPS RALT', 'K_U', new TextEncoder().encode('S')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 1, 'CAPS', 'K_S', 2, 6, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 0, 'CAPS', 'K_U', 3, 3, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 0, 'NCAPS RALT', 'K_S', 4, 4, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 0, 'NCAPS RALT', 'K_U', 5, 5, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C1", '', '', 0, 0, '', '', 0, 0, 'CAPS', 'K_S', new TextEncoder().encode('S')), + new Rule("C1", '', '', 0, 0, '', '', 0, 0, 'NCAPS RALT', 'K_U', new TextEncoder().encode('S')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 0, 'CAPS', 'K_S', 2, 0, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 0, 'CAPS', 'K_U', 3, 0, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 0, 'NCAPS RALT', 'K_S', 4, 0, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 0, 'NCAPS RALT', 'K_U', 5, 0, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C1", '', '', 0, 0, '', '', 0, 0, 'CAPS', 'K_U', new TextEncoder().encode('U')),] + ], + ].forEach(function (values: any) { + it('data of \'' + values[0] + "' passed into createRuleData() " + 'should create an array of rules', async function () { + const inputFilename = makePathToFixture(values[0][0]); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + const processedData = sut.convertBound.convert(read, inputFilename.replace(/\.keylayout$/, '.kmn')); + assert.deepEqual(processedData.rules[0], values[1][0]); + }); + }); + }); + +});*/ diff --git a/developer/src/kmc-convert/test/kmc-convert-convert/xkb-to-kmn-converter.tests.ts b/developer/src/kmc-convert/test/kmc-convert-convert/xkb-to-kmn-converter.tests.ts new file mode 100644 index 00000000000..9cd10d17434 --- /dev/null +++ b/developer/src/kmc-convert/test/kmc-convert-convert/xkb-to-kmn-converter.tests.ts @@ -0,0 +1,1035 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by S. Schmitt on 2025-05-12 + * + * Tests for XkbToKmnConverter, XkbFileReader, KmnFileWriter + * + */ +import 'mocha'; +import { assert } from 'chai'; +//import * as NodeAssert from 'node:assert'; +import { compilerTestCallbacks, compilerTestOptions, makePathToFixture } from './../helpers/index.js'; +import { /*ActionStateOutput, KeylayoutFileData,*/ XkbToKmnConverter } from '../../src/kmc-convert-convert/xkb-to-kmn-converter.js'; +//import { XkbFileReader } from '../../src/kmc-convert-read/xkb-file-reader.js'; +//import { ConverterMessages } from '../../src/converter-messages.js'; + +describe('XkbToKmnConverter', function () { + + before(function () { + compilerTestCallbacks.clear(); + }); + + describe('Xkb-kmn: RunSpecialTestFiles', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + [makePathToFixture('../data/tests-xkb-kmn/de-simple')], + [makePathToFixture('../data/tests-xkb-kmn/de-simple(basic)')], + [makePathToFixture('../data/tests-xkb-kmn/de-complex')], + [makePathToFixture('../data/tests-xkb-kmn/de-complex(deadtilde)')], + ].forEach(function (files) { + it(files + " should give no errors ", async function () { + sut.run(files[0]); + assert.isTrue(compilerTestCallbacks.messages.length === 0); + }); + }); + }); + +/* + describe('Xkb-kmn: getModifier ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + ['ONE_LEVEL', ['']], + ['FOUR_LEVEL_PLUS_LOCK', ["", "SHIFT", "RALT", "SHIFT RALT", "LOCK"]], + ['EIGHT_LEVEL', ["", "SHIFT", "ALT", "SHIFT ALT", "X", "X SHIFT", "X ALT", "X SHIFT ALT"]], + ['', null], + ['unknownLevel', null], + [99, null], + [null, null], + [undefined, null], + + ].forEach(function (values) { + it(("getModifier(" + values[0] + ")").padEnd(40, " ") + "should return " + "'" + values[1] + "'", async function () { + const result = sut.getModifier(values[0] as string); + assert.deepStrictEqual((result), (values[1])); + }); + }); + }); + + describe('Xkb-kmn: get_Keyname ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + [',r,R,paragraph,registered', ''], + [',type,Group1,="FOUR_LEVEL_PLUS_LOCK",symbols[Group1]=[ssharp,question,backslash,questiondown,B;', ''], + ['key { [ 2, quotedbl, twosuperior, oneeighth ] };', null], + ['unknown', null], + ['', null], + [null, null], + [undefined, null], + ].forEach(function (values) { + it(("get_Keyname(" + values[0] + ")").padEnd(40, " ") + " should return " + "'" + values[1] + "'", async function () { + const result = sut.get_Keyname(values[0] as string); + assert.deepStrictEqual((result), (values[1])); + }); + }); + }); + + describe('Xkb-kmn: getOutput ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + [',r,R,paragraph,registered', 'r,R,paragraph,registered'], + [',type,Group1,="FOUR_LEVEL_PLUS_LOCK",symbols,Group1,=,ssharp,question,backslash,questiondown,B', 'ssharp,question,backslash,questiondown,B'], + ['key { [ 2, quotedbl, twosuperior, oneeighth ] };', undefined], + ['unknown', null], + ['', null], + [null, null], + [undefined, null], + ].forEach(function (values) { + it(("getOutput(" + values[0] + ")").padEnd(40, " ") + " should return " + "'" + values[1] + "'", async function () { + const result = sut.getOutput(values[0] as string); + assert.deepStrictEqual((result), (values[1])); + }); + }); + }); + + describe('Xkb-kmn: getKeytype ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + [',r,R,paragraph,registered', 'DEFAULT_LEVEL'], + [',type,Group1,="FOUR_LEVEL_PLUS_LOCK",symbols[Group1]=[ssharp,question,backslash,questiondown,B;', 'FOUR_LEVEL_PLUS_LOCK'], + ['key { [ 2, quotedbl, twosuperior, oneeighth ] };', 'DEFAULT_LEVEL'], + ['unknown', 'DEFAULT_LEVEL'], + ['', 'DEFAULT_LEVEL'], + [null, null], + [undefined, null], + ].forEach(function (values) { + it(("getKeytype(" + values[0] + ")").padEnd(40, " ") + " should return " + "'" + values[1] + "'", async function () { + const result = sut.getKeytype(values[0] as string, 'DEFAULT_LEVEL'); + assert.deepStrictEqual((result), (values[1])); + }); + }); + }); + + describe('Xkb-kmn: get_KMVirtKC_from_keyname ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + ['', 'K_R'], + ['', 'K_BKQUOTE'], + ['', ''], + ['', 'K_4'], + [null, null], + [undefined, null], + ].forEach(function (values) { + it(("get_KMVirtKC_from_keyname(" + values[0] + ")").padEnd(40, " ") + " should return " + "'" + values[1] + "'", async function () { + const result = sut.get_KMVirtKC_from_keyname(values[0] as string); + assert.deepStrictEqual((result), (values[1])); + }); + }); + });*/ + +//...................................................................................................... +//...................................................................................................... +//...................................................................................................... +//...................................................................................................... +//...................................................................................................... +//...................................................................................................... + +/* + describe('findParagraph ', function () { + // Todo + ['variant', 'all definition text'], + ['unexisting vriant', ''], + ['null, null], + ['', ''], + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + ['', ''], + ].forEach(function (values) { + it(("findParagraph(" + values[0] + ")").padEnd(26, " ") + "should return " + "'" + values[1] + "'", async function () { + const result = sut.findParagraph(values[0] as string); + assert.equal(result, values[1]); + }); + }); + }); + + + describe('createCompleteLines ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + ['', ''], + ].forEach(function (values) { + it(("findPacreateCompleteLinesragraph(" + values[0] + ")").padEnd(26, " ") + "should return " + "'" + values[1] + "'", async function () { + const result = sut.createCompleteLines(values[0] as string); + assert.equal(result, values[1]); + }); + }); + });*/ + + + describe('get_KMVirtKC_from_keyname ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + ['', 'K_A'], + ['', 'K_X'], + ['', 'K_BKQUOTE'], + ['AC01>', ''], + ['', ''], + ['', ''], + ['', ''], + ['', ''], + ['', ''], + [17, ''], + [null, ''], + [undefined, ''], + ].forEach(function (values) { + it(("get_KMVirtKC_from_keyname(" + values[0] + ")").padEnd(26, " ") + "should return " + "'" + values[1] + "'", async function () { + const result = sut.get_KMVirtKC_from_keyname(values[0] as string); + assert.equal(result, values[1]); + }); + }); + }); + + describe('get_Output_FromOutputname ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + ['a', 'a'], + ['7', '7'], + ['ampersand', '&'], + ['NoSymbol', ''], + [null, ''], + [undefined, ''], + // ['😎', '😎'], + ['ሴ', 'ሴ'], + ['blabla', '******* keysym name not found!blabla'], + ['17', '******* keysym name not found!17'], + ['a', '******* keysym name not found!a'], + ['0x1234', '******* keysym name not found! - with 0x :0x1234'], + ['U2345', '******* keysym name not found! - with Uxxxx :U2345'], + ['ampersand', '&'], + ['ampersand', '&'], + ['ampersand', '&'], + ['ampersand', '&'], + ].forEach(function (values) { + it(("get_Output_FromOutputname(" + values[0] + ")").padEnd(26, " ") + "should return " + "'" + values[1] + "'", async function () { + const result = sut.get_Output_FromOutputname(values[0] as string); + assert.equal(result, values[1]); + }); + }); + }); + /* + describe('isAcceptableKeymanModifier ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + ['NCAPS', true], + ['NxCAPS', false], + ['SHIFT', true], + ['ALT', true], + ['RALT', true], + ['LALT', true], + ['CTRL', true], + ['LCTRL', true], + ['RCTRL', true], + ['LCTRL CAPS', true], + ['LCTRL X', false], + ['', true], + [null, false], + ].forEach(function (values) { + it(("isAcceptableKeymanModifier(" + values[0] + ")").padEnd(38, " ") + ' should return ' + values[1], async function () { + const result = sut.isAcceptableKeymanModifier(values[0] as string); + assert.equal(result, values[1]); + }); + }); + });*/ + /* + describe('checkIfCapsIsUsed ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + [[['caps', 'xxx'], ['yyy']], true], + [[['Caps', 'xxx'], ['yyy']], true], + [[['CaPs', 'xxx'], ['yyy']], true], + [[['Caps?', 'xxx'], ['yyy']], false], + [[['zzz', 'xxx'], ['Caps?']], false], + [[['caps?', 'xxx'], ['yyy']], false], + [[['zzz', 'xxx'], ['yyy']], false], + [[['shift', 'xxx'], ['caps']], true], + [[['shift', 'caps'], ['yyy']], true], + [[['caps', 'xxx'], ['caps']], true], + [[['', 'someWordWithCaps'], ['']], false], + [null, false], + [[], false], + [[['', ''], ['']], false], + [[[' ', ' '], [' ']], false], + ].forEach(function (values) { + it(("checkIfCapsIsUsed(" + values[0] + ")").padEnd(40, " ") + "should return " + "'" + values[1] + "'", async function () { + const result = sut.checkIfCapsIsUsed(values[0] as string[][]); + assert.isTrue(result === values[1]); + }); + }); + });*/ + + + + + + + + + + + + /* + describe('Xkb-kmn: RunTestFiles resulting in errors ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + [makePathToFixture('../data/tests-xkb-kmn/Test_DifferentAmountOfMapSelectInKeyMapERROR.keylayout')], + [makePathToFixture('../data/tests-xkb-kmn/Test_MissingkeyERROR.keylayout')], + [makePathToFixture('../data/tests-xkb-kmn/Test_MissingkeyMapERROR.keylayout')], + [makePathToFixture('../data/tests-xkb-kmn/Test_MissingLayoutsERROR.keylayout')], + [makePathToFixture('../data/tests-xkb-kmn/Test_MissingmodifierMapERROR.keylayout')], + [makePathToFixture('../data/tests-xkb-kmn/Test_MissingkeyMapSetERROR.keylayout')], + [makePathToFixture('../data/tests-xkb-kmn/Test_MissingActionsERROR.keylayout')], + [makePathToFixture('../data/tests-xkb-kmn/Test_MissingTerminatorsERROR.keylayout')], + [makePathToFixture('../data/tests-xkb-kmn/Test_MissingAllERROR.keylayout')], + ].forEach(function (files) { + it(files + " should give an error ", async function () { + sut.run(files[0]); + assert.isTrue(compilerTestCallbacks.messages.length > 0); + }); + }); + }); + + describe('Xkb-kmn: RunSpecialTestFiles - create Error: unsupported characters', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + [makePathToFixture('../data/tests-xkb-kmn/Test_characters.keylayout')], + ].forEach(function (files) { + it(files + " should give Error: unsupported characters ", async function () { + sut.run(files[0]); + assert.isTrue(compilerTestCallbacks.messages.length === 1); + }); + }); + }); + + describe('Xkb-kmn: RunSpecialTestFiles - create Error: undefined action', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + [makePathToFixture('../data/tests-xkb-kmn/Test_undefinedAction.keylayout')], + ].forEach(function (files) { + it(files + " should give Error: undefined action detected", async function () { + sut.run(files[0]); + assert.isTrue(compilerTestCallbacks.messages.length === 1); + assert.equal(compilerTestCallbacks.messages[0].code, 5292040); + }); + }); + }); + + describe('Xkb-kmn: run() ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + + it('run() should throw on null input file name and null output file name', async function () { + // note, could use 'chai as promised' library to make this more fluent: + const result = sut.run(null, null); + assert.isNotNull(result); + assert.equal(compilerTestCallbacks.messages.length, 1); + assert.deepEqual(compilerTestCallbacks.messages[0], ConverterMessages.Error_FileNotFound({ inputFilename: null })); + }); + + it('run() should throw on null input file name and empty output file name', async function () { + const result = sut.run(null, ''); + assert.isNotNull(result); + assert.equal(compilerTestCallbacks.messages.length, 1); + assert.deepEqual(compilerTestCallbacks.messages[0], ConverterMessages.Error_FileNotFound({ inputFilename: null })); + }); + + it('run() should throw on null input file name and unknown output file name', async function () { + const result = sut.run(null, 'X'); + assert.isNotNull(result); + assert.equal(compilerTestCallbacks.messages.length, 1); + assert.deepEqual(compilerTestCallbacks.messages[0], ConverterMessages.Error_FileNotFound({ inputFilename: null })); + }); + + it('run() should throw on unavailable input file name and null output file name', async function () { + const inputFilename = makePathToFixture('../data/tests-xkb-kmn/Unavailable.keylayout'); + const result = sut.run(inputFilename, null); + assert.isNotNull(result); + assert.equal(compilerTestCallbacks.messages.length, 2); + assert.deepEqual(compilerTestCallbacks.messages[0], ConverterMessages.Error_UnableToRead()); + assert.equal(compilerTestCallbacks.messages[1].code, 5292037); + }); + }); + + describe('Xkb-kmn: Run kmc-convert with or without outputfile name', async function () { + this.timeout(5000); // allow longer time for this test + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const infile = '../data/tests-xkb-kmn/Test.keylayout'; + [ + [makePathToFixture('../data/tests-xkb-kmn/Test.kmn')], + [makePathToFixture('')], + [], + [null], + [makePathToFixture('../data/tests-xkb-kmn/test_OtherOutputName.kmn')], + [makePathToFixture('../data/tests-xkb-kmn/OutputXName.bb')], + ].forEach(function (files) { + it(infile + " should run ", async function () { + await NodeAssert.doesNotReject(async () => sut.run(makePathToFixture(infile), files[0])); + assert.equal(compilerTestCallbacks.messages.length, 0); + }); + }); + });*/ + ///////////////////////////////////////////////////////////// + /* + describe('Xkb-kmn: convert() ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new XkbFileReader(compilerTestCallbacks); + + // ProcessedData from usable file + const inputFilename = makePathToFixture('../data/tests-xkb-kmn/Test.keylayout'); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + const converted = sut.convertBound.convert(read, inputFilename.replace(/\.keylayout$/, '.kmn')); + + // ProcessedData from unavailable file + const inputFilenameUnavailable = makePathToFixture('../data/tests-xkb-kmn/X.keylayout'); + const readUnavailable = sutR.read(compilerTestCallbacks.loadFile(inputFilenameUnavailable)); + const convertedUnavailable = sut.convertBound.convert(readUnavailable, inputFilenameUnavailable.replace(/\.keylayout$/, '.kmn')); + + // ProcessedData from empty file + const inputFilenameEmpty = makePathToFixture(''); + const readEmpty = sutR.read(compilerTestCallbacks.loadFile(inputFilenameEmpty)); + const convertedEmpty = sut.convertBound.convert(readEmpty, inputFilenameEmpty); + + it('should return converted array on correct input', async function () { + assert.isTrue(converted.rules.length !== 0); + }); + + it('should return empty on empty name as input', async function () { + assert.isNull(convertedUnavailable); + }); + + it('should return empty on empty input', async function () { + assert.isNull(convertedEmpty); + }); + + it('should return empty array of rules on null input', async function () { + const convertedRule = sut.convertBound.convert(null, 'ABC.kmn'); + assert.isNull(convertedRule); + }); + }); + + describe('Xkb-kmn: createKmnModifier ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + [' ', true, 'NCAPS'], + [' ', false, ''], + + ['NCAPS', true, 'NCAPS'], + ['NCAPS', false, ''], + ['caps', true, 'CAPS'], + ['CAPS', true, 'CAPS'], + ['CAPS?', true, 'NCAPS'], + ['CAPS', false, 'CAPS'], + ['CAPS?', false, ''], + ['CAPS? NCAPS', true, 'NCAPS'], + ['CAPS NCAPS', true, 'CAPS NCAPS'], + ['caps ncaps', true, 'CAPS NCAPS'], + ['NCAPS shift', true, 'NCAPS SHIFT'], + ['shift', true, 'NCAPS SHIFT'], + ['leftshift', true, 'NCAPS SHIFT'], + ['rightShift', true, 'NCAPS SHIFT'], + ['shift', false, 'SHIFT'], + ['leftshift', false, 'SHIFT'], + ['rightShift', false, 'SHIFT'], + ['shift rightShift?', true, 'NCAPS SHIFT'], + ['rightShift?', true, 'NCAPS'], + ['shift Shift?', true, 'NCAPS SHIFT'], + ['shift rightShift? caps? rightOption? rightControl', true, 'NCAPS SHIFT RCTRL'], + ['leftshift rightShift? caps? rightOption? rightControl', true, 'NCAPS SHIFT RCTRL'], + ['anycontrol', true, 'NCAPS CTRL'], + ['shift?', true, 'NCAPS'], + ['?', true, 'NCAPS'], + ['?', false, ''], + ['', true, 'NCAPS'], + [' ', false, ''], + ['wrongModifierName', false, 'wrongModifierName'], + ['shift', false, 'SHIFT'], + ['shift command', true, 'NCAPS SHIFT command'], + ['rshift', true, 'NCAPS SHIFT'], + ['rshift', false, 'SHIFT'], + ['rightshift', true, 'NCAPS SHIFT'], + ['riGhtsHift', true, 'NCAPS SHIFT'], + ['LEFTCONTROL', true, 'NCAPS LCTRL'], + ['RCONTROL', true, 'NCAPS RCTRL'], + ['leftoption', true, 'NCAPS LALT'], + ['loption', true, 'NCAPS LALT'], + ['rightoption', true, 'NCAPS RALT'], + ['roption', true, 'NCAPS RALT'], + ].forEach(function (values) { + it(('should convert "' + values[0] + '"').padEnd(36, " ") + 'to "' + values[2] + '"', async function () { + const result = sut.createKmnModifier(values[0] as string, values[1] as boolean); + assert.equal(result, values[2]); + }); + }); + }); + + describe('Xkb-kmn: isAcceptableKeymanModifier ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + ['NCAPS', true], + ['NxCAPS', false], + ['SHIFT', true], + ['ALT', true], + ['RALT', true], + ['LALT', true], + ['CTRL', true], + ['LCTRL', true], + ['RCTRL', true], + ['LCTRL CAPS', true], + ['LCTRL X', false], + ['', true], + [null, false], + ].forEach(function (values) { + it(("isAcceptableKeymanModifier(" + values[0] + ")").padEnd(38, " ") + ' should return ' + values[1], async function () { + const result = sut.isAcceptableKeymanModifier(values[0] as string); + assert.equal(result, values[1]); + }); + }); + }); + + describe('Xkb-kmn: mapUkeleleKeycodeToVK ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + [0x00, 'K_A'], + [0x31, 'K_SPACE'], + [0x18, 'K_EQUAL'], + [0x10, 'K_Y'], + [0x18, 'K_EQUAL'], + [0x21, 'K_LBRKT'], + [0x999, ''], + [-1, ''], + [null, ''], + [undefined, ''], + [, ''], + ].forEach(function (values) { + it(("mapUkeleleKeycodeToVK(" + values[0] + ")").padEnd(26, " ") + "should return " + "'" + values[1] + "'", async function () { + const result = sut.mapUkeleleKeycodeToVK(values[0] as number); + assert.equal(result, values[1]); + }); + }); + }); + + describe('Xkb-kmn: checkIfCapsIsUsed ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + [ + [[['caps', 'xxx'], ['yyy']], true], + [[['Caps', 'xxx'], ['yyy']], true], + [[['CaPs', 'xxx'], ['yyy']], true], + [[['Caps?', 'xxx'], ['yyy']], false], + [[['zzz', 'xxx'], ['Caps?']], false], + [[['caps?', 'xxx'], ['yyy']], false], + [[['zzz', 'xxx'], ['yyy']], false], + [[['shift', 'xxx'], ['caps']], true], + [[['shift', 'caps'], ['yyy']], true], + [[['caps', 'xxx'], ['caps']], true], + [[['', 'someWordWithCaps'], ['']], false], + [null, false], + [[], false], + [[['', ''], ['']], false], + [[[' ', ' '], [' ']], false], + ].forEach(function (values) { + it(("checkIfCapsIsUsed(" + values[0] + ")").padEnd(40, " ") + "should return " + "'" + values[1] + "'", async function () { + / const result = sut.checkIfCapsIsUsed(values[0] as string[][]); + assert.isTrue(result === values[1]); + }); + }); + }); +*/ + + /* + + describe('Xkb-kmn: getModifierArrayFromKeyModifierArray ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new XkbFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-xkb-kmn/Test.keylayout'); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + const converted = sut.convertBound.convert(read, inputFilename.replace(/\.keylayout$/, '.kmn')); + [ + [[{ key: '0', behavior: 0 }], [['', 'shift? caps? ']]], + [[{ key: '0', behavior: 2 }], [['shift? leftShift caps? ', 'anyShift caps?', 'shift leftShift caps ', 'shift? rightShift caps? ']]], + [[{ key: '0', behavior: 999 }], [null]], + [[{ key: '999', behavior: null }], [null]], + [[{ key: '0', behavior: -999 }], [null]], + [[{ key: '0', behavior: null }], [null]], + [[], []], + + ].forEach(function (values) { + it((values[1] !== null) ? + ("getModifierArrayFromKeyModifierArray('" + JSON.stringify(values[0]) + "')").padEnd(68, " ") + " should return '" + JSON.stringify(values[1]) + "'" : + ("getModifierArrayFromKeyModifierArray('" + JSON.stringify(values[0]) + "')").padEnd(68, " ") + " should return '" + "null" + "'", async function () { + const result = sut.getModifierArrayFromKeyModifierArray(converted.modifiers, values[0] as unknown as KeylayoutFileData[]); + assert.deepStrictEqual(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + }); + + describe('Xkb-kmn: getKeyModifierArrayFromActionID ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new XkbFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-xkb-kmn/Test.keylayout'); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + [ + ['A_16', [{ "key": "32", "behavior": "5" }]], + ['A_19', [{ "key": "45", "behavior": "5" }]], + ['A_18', [{ "key": "24", "behavior": "0" }, { "key": "24", "behavior": "5" }]], + ['unknown', []], + [undefined, []], + [null, []], + [' ', []], + ['', []], + ].forEach(function (values) { + let outstring = '[ '; + for (let i = 0; i < values[1].length; i++) { + outstring = outstring + "[ " + JSON.stringify(values[1][i]) + "], "; + } + it(("getKeyModifierArrayFromActionID('" + values[0] + "')").padEnd(57, " ") + ' should return ' + outstring.substring(0, outstring.lastIndexOf(']') + 2) + " ]", async function () { + const result = sut.getKeyModifierArrayFromActionID(read, String(values[0])); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + }); + + describe('Xkb-kmn: getActionIdFromActionNext ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new XkbFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-xkb-kmn/Test.keylayout'); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + [ + ['none', ''], + ['0', ''], + ['A_18', ''], + ['1', 'A_16'], + ['2', 'A_8'], + ['3', 'A_17'], + ['', ''], + [' ', ''], + ['99', ''], + [null, ''], + [undefined, ''], + ['unknown', ''], + ].forEach(function (values) { + it(("getActionIdFromActionNext('" + values[0] + "')").padEnd(49, " ") + ' should return ' + "'" + values[1] + "'", async function () { + const result = sut.getActionIdFromActionNext(read, String(values[0])); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + }); + + describe('Xkb-kmn: getActionIndexFromActionId ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new XkbFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-xkb-kmn/Test.keylayout'); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + [ + ['none', -1], + ['A_16', 8], + ['A_18', 10], + ['A_19', 11], + ['0', -1], + ['', -1], + [' ', -1], + [null, -1], + [undefined, -1], + ['unknown', -1], + ].forEach(function (values) { + it(("getActionIndexFromActionId('" + values[0] + "')").padEnd(50, " ") + ' should return ' + values[1], async function () { + const result = sut.getActionIndexFromActionId(read, String(values[0])); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + }); + + describe('Xkb-kmn: getOutputFromActionIdNone ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new XkbFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-xkb-kmn/Test.keylayout'); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + [ + ['A_14', 'u'], + ['', ''], + [' ', ''], + ['A_18', ''], + ['unknown', ''], + ].forEach(function (values) { + it( + ("getOutputFromActionIdNone('" + values[0] + "')").padEnd(56, " ") + ' should return ' + "'" + values[1] + "'", async function () { + const result = sut.getOutputFromActionIdNone(read, String(values[0])); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + + [[null, ''], + [undefined, ''], + [99, ''], + ].forEach(function (values) { + it(("getOutputFromActionIdNone('" + values[0] + "')").padEnd(56, " ") + ' should return ' + values[1], async function () { + const result = sut.getOutputFromActionIdNone(read, String(values[0])); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + }); + + describe('Xkb-kmn: getKeybehaviorModOutputArrayFromKeyActionbehaviorOutputArray ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new XkbFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-xkb-kmn/Test.keylayout'); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + + const b1KeycodeArr: KeylayoutFileData[] = [ + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '0', outchar: 'ˆ' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '1', outchar: 'ˆ' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '2', outchar: 'ˆ' }, + { keyCode: '6', key: 'K_Z', actionId: 'A_0', behavior: '4', outchar: 'ˆ' }, + { keyCode: '25', key: 'K_9', actionId: 'A_0', behavior: '4', outchar: 'ˆ' }, + { keyCode: '43', key: 'K_COMMA', actionId: 'A_0', behavior: '4', outchar: 'ˆ' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '3', outchar: 'ˆ' }, + { keyCode: '0', key: 'K_A', actionId: 'A_1', behavior: '2', outchar: 'Â' }, + { keyCode: '0', key: 'K_A', actionId: 'A_1', behavior: '1', outchar: 'Â' }, + { keyCode: '14', key: 'K_E', actionId: 'A_10', behavior: '0', outchar: 'ê' }, + { keyCode: '34', key: 'K_I', actionId: 'A_11', behavior: '0', outchar: 'î' }, + { keyCode: '31', key: 'K_O', actionId: 'A_13', behavior: '0', outchar: 'ô' }, + { keyCode: '32', key: 'K_U', actionId: 'A_14', behavior: '0', outchar: 'û' }, + { keyCode: '14', key: 'K_E', actionId: 'A_2', behavior: '2', outchar: 'Ê' }, + { keyCode: '14', key: 'K_E', actionId: 'A_2', behavior: '1', outchar: 'Ê' }, + { keyCode: '34', key: 'K_I', actionId: 'A_3', behavior: '2', outchar: 'Î' }, + { keyCode: '34', key: 'K_I', actionId: 'A_3', behavior: '1', outchar: 'Î' }, + { keyCode: '31', key: 'K_O', actionId: 'A_5', behavior: '2', outchar: 'Ô' }, + { keyCode: '31', key: 'K_O', actionId: 'A_5', behavior: '1', outchar: 'Ô' }, + { keyCode: '32', key: 'K_U', actionId: 'A_6', behavior: '2', outchar: 'Û' }, + { keyCode: '32', key: 'K_U', actionId: 'A_6', behavior: '1', outchar: 'Û' }, + { keyCode: '0', key: 'K_A', actionId: 'A_9', behavior: '0', outchar: 'â' } + + ]; + const b1ModifierKeyArr: KeylayoutFileData[] = [ + { actionId: 'A_0', key: 'K_SPACE', behavior: '0', modifier: 'NCAPS', outchar: 'ˆ' }, + { actionId: 'A_0', key: 'K_SPACE', behavior: '1', modifier: 'CAPS', outchar: 'ˆ' }, + { actionId: 'A_0', key: 'K_SPACE', behavior: '2', modifier: 'NCAPS SHIFT', outchar: 'ˆ' }, + { actionId: 'A_0', key: 'K_SPACE', behavior: '2', modifier: 'SHIFT CAPS', outchar: 'ˆ' }, + { actionId: 'A_0', key: 'K_Z', behavior: '4', modifier: 'NCAPS SHIFT RALT', outchar: 'ˆ' }, + { actionId: 'A_0', key: 'K_9', behavior: '4', modifier: 'NCAPS SHIFT RALT', outchar: 'ˆ' }, + { actionId: 'A_0', key: 'K_COMMA', behavior: '4', modifier: 'NCAPS SHIFT RALT', outchar: 'ˆ' }, + { actionId: 'A_0', key: 'K_SPACE', behavior: '3', modifier: 'NCAPS RALT CTRL', outchar: 'ˆ' }, + { actionId: 'A_0', key: 'K_SPACE', behavior: '3', modifier: 'NCAPS CTRL', outchar: 'ˆ' }, + { actionId: 'A_1', key: 'K_A', behavior: '2', modifier: 'NCAPS SHIFT', outchar: 'Â' }, + { actionId: 'A_1', key: 'K_A', behavior: '2', modifier: 'SHIFT CAPS', outchar: 'Â' }, + { actionId: 'A_1', key: 'K_A', behavior: '1', modifier: 'CAPS', outchar: 'Â' }, + { actionId: 'A_10', key: 'K_E', behavior: '0', modifier: 'NCAPS', outchar: 'ê' }, + { actionId: 'A_11', key: 'K_I', behavior: '0', modifier: 'NCAPS', outchar: 'î' }, + { actionId: 'A_13', key: 'K_O', behavior: '0', modifier: 'NCAPS', outchar: 'ô' }, + { actionId: 'A_14', key: 'K_U', behavior: '0', modifier: 'NCAPS', outchar: 'û' }, + { actionId: 'A_2', key: 'K_E', behavior: '2', modifier: 'NCAPS SHIFT', outchar: 'Ê' }, + { actionId: 'A_2', key: 'K_E', behavior: '2', modifier: 'SHIFT CAPS', outchar: 'Ê' }, + { actionId: 'A_2', key: 'K_E', behavior: '1', modifier: 'CAPS', outchar: 'Ê' }, + { actionId: 'A_3', key: 'K_I', behavior: '2', modifier: 'NCAPS SHIFT', outchar: 'Î' }, + { actionId: 'A_3', key: 'K_I', behavior: '2', modifier: 'SHIFT CAPS', outchar: 'Î' }, + { actionId: 'A_3', key: 'K_I', behavior: '1', modifier: 'CAPS', outchar: 'Î' }, + { actionId: 'A_5', key: 'K_O', behavior: '2', modifier: 'NCAPS SHIFT', outchar: 'Ô' }, + { actionId: 'A_5', key: 'K_O', behavior: '2', modifier: 'SHIFT CAPS', outchar: 'Ô' }, + { actionId: 'A_5', key: 'K_O', behavior: '1', modifier: 'CAPS', outchar: 'Ô' }, + { actionId: 'A_6', key: 'K_U', behavior: '2', modifier: 'NCAPS SHIFT', outchar: 'Û' }, + { actionId: 'A_6', key: 'K_U', behavior: '2', modifier: 'SHIFT CAPS', outchar: 'Û' }, + { actionId: 'A_6', key: 'K_U', behavior: '1', modifier: 'CAPS', outchar: 'Û' }, + { actionId: 'A_9', key: 'K_A', behavior: '0', modifier: 'NCAPS', outchar: 'â' } + ]; + + [[b1KeycodeArr, b1ModifierKeyArr], + [[{ keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '0', modifier: '0', outchar: 'ˆ' }], [{ actionId: 'A_0', key: 'K_SPACE', behavior: '0', modifier: 'NCAPS', outchar: 'ˆ' }]], + [[{ keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '0', modifier: '', outchar: 'ˆ' }], [{ actionId: 'A_0', key: 'K_SPACE', behavior: '0', modifier: 'NCAPS', outchar: 'ˆ' }]], + [[{ keyCode: '49', key: 'K_SPACE', actionId: '', behavior: '0', modifier: '0', outchar: 'ˆ' }], [{ actionId: '', key: 'K_SPACE', behavior: '0', modifier: 'NCAPS', outchar: 'ˆ' }]], + [[{ keyCode: '49', key: '', actionId: 'A_0', behavior: '0', modifier: '0', outchar: 'ˆ' }], [{ actionId: 'A_0', key: '', behavior: '0', modifier: 'NCAPS', outchar: 'ˆ' }]], + [[{ keyCode: '', key: 'K_SPACE', actionId: 'A_0', behavior: '0', modifier: '0', outchar: 'ˆ' }], [{ actionId: 'A_0', key: 'K_SPACE', behavior: '0', modifier: 'NCAPS', outchar: 'ˆ' }]], + [[{ keyCode: '', key: '', actionId: '', behavior: '0', modifier: '', outchar: '' }], [{ actionId: '', key: '', behavior: '0', modifier: 'NCAPS', outchar: '' }]], + ].forEach(function (values) { + const isCapsUsed = true; + const stringIn = "getKeybehaviorModOutputArrayFromKeyActionbehaviorOutputArray(['" + "', '" + "', '" + values[0][0].keyCode + "', '" + values[0][0].key + "', '" + values[0][0].actionId + "', '" + values[0][0].modifier + "', '" + values[0][0].outchar + "'])"; + const stringOut = "['" + "', '" + "', '" + values[1][0].key + "', '" + values[1][0].actionId + "', '" + "', '" + values[1][0].modifier + "', '" + values[1][0].outchar + "']"; + + it((JSON.stringify(values[1]).length > 60) ? 'an array of objects should return an array of objects' : + stringIn.padEnd(74, " ") + ' should return ' + stringOut, async function () { + const result = sut.getKeyBehaviorModOutputArrayFromKeyActionBehaviorOutputArray(read, values[0], isCapsUsed); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + + [[{ keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '0', modifier: '0', outchar: 'ˆ' }, { actionId: 'A_0', key: 'K_SPACE', behavior: '0', modifier: '', outchar: 'ˆ' }], + [{ keyCode: '', key: 'K_SPACE', actionId: 'A_0', behavior: '0', modifier: '0', outchar: 'ˆ' }, { actionId: 'A_0', key: 'K_SPACE', behavior: '0', modifier: '', outchar: 'ˆ' }], + [{ keyCode: '', key: '', actionId: '', behavior: '0', modifier: '', outchar: '' }, { actionId: '', key: '', behavior: '0', modifier: '', outchar: '' }], + ].forEach(function (values) { + const isCapsUsed = false; + const stringIn = "getKeybehaviorModOutputArrayFromKeyActionbehaviorOutputArray([ '" + values[0].keyCode + "', '" + values[0].key + "', '" + values[0].actionId + "', '" + values[0].modifier + "', '" + values[0].outchar + "'])"; + const stringOut = "['" + values[1].actionId + "', '" + "', '" + values[1].modifier + "', '" + values[1].key + "', '" + values[1].outchar + "']"; + + it(stringIn.padEnd(74, " ") + ' should return ' + stringOut, async function () { + const result = sut.getKeyBehaviorModOutputArrayFromKeyActionBehaviorOutputArray(read, [values[0]], isCapsUsed); + assert.equal(JSON.stringify(result), JSON.stringify([values[1]])); + }); + }); + + [[[], []], + [undefined, []], + [null, []], + ].forEach(function (values) { + const isCaps = true; + it(("getKeybehaviorModOutputArrayFromKeyActionbehaviorOutputArray([" + values[0] + "])").padEnd(74, " ") + ' should return ' + "[" + values[1] + "]", async function () { + const result = sut.getKeyBehaviorModOutputArrayFromKeyActionBehaviorOutputArray(read, values[0], isCaps); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + }); + + describe('Xkb-kmn: getActionStateOutputArrayFromActionState ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new XkbFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-xkb-kmn/Test.keylayout'); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + [['1', [ + { "id": "A_0", "state": "1", "output": "ˆ" }, + { "id": "A_1", "state": "1", "output": "Â" }, + { "id": "A_10", "state": "1", "output": "ê" }, + { "id": "A_11", "state": "1", "output": "î" }, + { "id": "A_13", "state": "1", "output": "ô" }, + { "id": "A_14", "state": "1", "output": "û" }, + { "id": "A_2", "state": "1", "output": "Ê" }, + { "id": "A_3", "state": "1", "output": "Î" }, + { "id": "A_5", "state": "1", "output": "Ô" }, + { "id": "A_6", "state": "1", "output": "Û" }, + { "id": "A_9", "state": "1", "output": "â" } + ],], + ['2', [ + { "id": "A_0", "state": "2", "output": "`" }, + { "id": "A_1", "state": "2", "output": "À" }, + { "id": "A_10", "state": "2", "output": "è" }, + { "id": "A_11", "state": "2", "output": "ì" }, + { "id": "A_13", "state": "2", "output": "ò" }, + { "id": "A_14", "state": "2", "output": "ù" }, + { "id": "A_2", "state": "2", "output": "È" }, + { "id": "A_3", "state": "2", "output": "Ì" }, + { "id": "A_5", "state": "2", "output": "Ò" }, + { "id": "A_6", "state": "2", "output": "Ù" }, + { "id": "A_9", "state": "2", "output": "à" } + ],], + ['789', [],], + ['', [],], + [' ', [],], + [123, [],], + [null, [],], + [undefined, [],], + ].forEach(function (values) { + it((JSON.stringify(values[1]).length > 30) ? + ("getActionStateOutputArrayFromActionState('" + values[0] + "')").padEnd(60, " ") + ' should return an array of objects' : + ("getActionStateOutputArrayFromActionState('" + values[0] + "')").padEnd(60, " ") + ' should return ' + "'" + JSON.stringify(values[1]) + "'", async function () { + const result = sut.getActionStateOutputArrayFromActionState(read, String(values[0])); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + }); + + describe('Xkb-kmn: getActionOutputbehaviorKeyModiFromActionIDStateOutput ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new XkbFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-xkb-kmn/Test.keylayout'); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + const converted = sut.convertBound.convert(read, inputFilename.replace(/\.keylayout$/, '.kmn')); + [ + ['A_1', 'A', true, + [{ "outchar": "A", "actionId": "A_1", "behavior": "1", "key": "K_A", "modifier": "CAPS" }, + { "outchar": "A", "actionId": "A_1", "behavior": "2", "key": "K_A", "modifier": "NCAPS SHIFT" }, + { "outchar": "A", "actionId": "A_1", "behavior": "2", "key": "K_A", "modifier": "SHIFT CAPS" }] + ], + ['A_1', 'A', false, + [{ "outchar": "A", "actionId": "A_1", "behavior": "1", "key": "K_A", "modifier": "CAPS" }, + { "outchar": "A", "actionId": "A_1", "behavior": "2", "key": "K_A", "modifier": "SHIFT" }, + { "outchar": "A", "actionId": "A_1", "behavior": "2", "key": "K_A", "modifier": "SHIFT CAPS" }] + ], + ['A_9', 'a', true, [{ "outchar": "a", "actionId": "A_9", "behavior": "0", "key": "K_A", "modifier": "NCAPS" }]], + ['A_9', 'a', false, [{ "outchar": "a", "actionId": "A_9", "behavior": "0", "key": "K_A", "modifier": "" }]], + ['A_9', 'a', , [{ "outchar": "a", "actionId": "A_9", "behavior": "0", "key": "K_A", "modifier": "" }]], + ['A_9', '', true, [{ "outchar": "", "actionId": "A_9", "behavior": "0", "key": "K_A", "modifier": "NCAPS" }]], + ['A_9', '', false, [{ "outchar": "", "actionId": "A_9", "behavior": "0", "key": "K_A", "modifier": "" }]], + ['', 'a', true, []], + ['', 'a', false, []], + ['', '', , []], + ].forEach(function (values) { + it((JSON.stringify(values[3]).length > 35) ? + ("getActionOutputbehaviorKeyModiFromActionIDStateOutput('" + values[0] + "', '" + values[1] + "', " + values[2] + ")").padEnd(67, " ") + ' should return an array of objects' : + ("getActionOutputbehaviorKeyModiFromActionIDStateOutput('" + values[0] + "', '" + values[1] + "', " + values[2] + ")").padEnd(67, " ") + ' should return ' + "'" + JSON.stringify(values[3]) + "'", async function () { + const result = sut.getActionOutputBehaviorKeyModiFromActionIDStateOutput(read, converted.modifiers, String(values[0]), String(values[1]), Boolean(values[2])); + assert.equal(JSON.stringify(result), JSON.stringify(values[3])); + }); + }); + }); + + describe('Xkb-kmn: getKeyActionOutputArrayFromActionStateOutputArray ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new XkbFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-xkb-kmn/Test.keylayout'); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + + const b6ActionIdArr: ActionStateOutput[] = [ + { "id": "A_0", "state": "1", "output": "ˆ" }, + { "id": "A_1", "state": "1", "output": "Â" }, + { "id": "A_10", "state": "1", "output": "ê" }, + { "id": "A_11", "state": "1", "output": "î" }, + { "id": "A_13", "state": "1", "output": "ô" }, + { "id": "A_14", "state": "1", "output": "û" }, + { "id": "A_2", "state": "1", "output": "Ê" }, + { "id": "A_3", "state": "1", "output": "Î" }, + { "id": "A_5", "state": "1", "output": "Ô" }, + { "id": "A_6", "state": "1", "output": "Û" }, + { "id": "A_9", "state": "1", "output": "â" } + ]; + + const b1KeycodeArr: KeylayoutFileData[] = [ + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '0', outchar: 'ˆ' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '1', outchar: 'ˆ' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '2', outchar: 'ˆ' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '3', outchar: 'ˆ' }, + { keyCode: '6', key: 'K_Z', actionId: 'A_0', behavior: '4', outchar: 'ˆ' }, + { keyCode: '25', key: 'K_9', actionId: 'A_0', behavior: '4', outchar: 'ˆ' }, + { keyCode: '43', key: 'K_COMMA', actionId: 'A_0', behavior: '4', outchar: 'ˆ' }, + { keyCode: '0', key: 'K_A', actionId: 'A_1', behavior: '1', outchar: 'Â' }, + { keyCode: '0', key: 'K_A', actionId: 'A_1', behavior: '2', outchar: 'Â' }, + { keyCode: '14', key: 'K_E', actionId: 'A_10', behavior: '0', outchar: 'ê' }, + { keyCode: '34', key: 'K_I', actionId: 'A_11', behavior: '0', outchar: 'î' }, + { keyCode: '31', key: 'K_O', actionId: 'A_13', behavior: '0', outchar: 'ô' }, + { keyCode: '32', key: 'K_U', actionId: 'A_14', behavior: '0', outchar: 'û' }, + { keyCode: '14', key: 'K_E', actionId: 'A_2', behavior: '1', outchar: 'Ê' }, + { keyCode: '14', key: 'K_E', actionId: 'A_2', behavior: '2', outchar: 'Ê' }, + { keyCode: '34', key: 'K_I', actionId: 'A_3', behavior: '1', outchar: 'Î' }, + { keyCode: '34', key: 'K_I', actionId: 'A_3', behavior: '2', outchar: 'Î' }, + { keyCode: '31', key: 'K_O', actionId: 'A_5', behavior: '1', outchar: 'Ô' }, + { keyCode: '31', key: 'K_O', actionId: 'A_5', behavior: '2', outchar: 'Ô' }, + { keyCode: '32', key: 'K_U', actionId: 'A_6', behavior: '1', outchar: 'Û' }, + { keyCode: '32', key: 'K_U', actionId: 'A_6', behavior: '2', outchar: 'Û' }, + { keyCode: '0', key: 'K_A', actionId: 'A_9', behavior: '0', outchar: 'â' } + ]; + + [[b6ActionIdArr, b1KeycodeArr], + ].forEach(function (values) { + it(("getKeyActionOutputArrayFromActionStateOutputArray([['" + JSON.stringify(values[0]) + "'],..])").padEnd(73, " ") + '1 should return an array of objects', async function () { + const result = sut.getKeyActionOutputArrayFromActionStateOutputArray(read, values[0] as ActionStateOutput[]); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + + const oneEntryResult = [ + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '0', outchar: 'ˆ' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '1', outchar: 'ˆ' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '2', outchar: 'ˆ' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '3', outchar: 'ˆ' }, + { keyCode: '6', key: 'K_Z', actionId: 'A_0', behavior: '4', outchar: 'ˆ' }, + { keyCode: '25', key: 'K_9', actionId: 'A_0', behavior: '4', outchar: 'ˆ' }, + { keyCode: '43', key: 'K_COMMA', actionId: 'A_0', behavior: '4', outchar: 'ˆ' } + ]; + + const oneEntryResultNoOutput = [ + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '0', outchar: '' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '1', outchar: '' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '2', outchar: '' }, + { keyCode: '49', key: 'K_SPACE', actionId: 'A_0', behavior: '3', outchar: '' }, + { keyCode: '6', key: 'K_Z', actionId: 'A_0', behavior: '4', outchar: '' }, + { keyCode: '25', key: 'K_9', actionId: 'A_0', behavior: '4', outchar: '' }, + { keyCode: '43', key: 'K_COMMA', actionId: 'A_0', behavior: '4', outchar: '' }, + ]; + + [[[{ "id": "A_0", "state": "1", "output": "ˆ" }], oneEntryResult], + [[{ "id": "A_0", "state": "1", "output": "" }], oneEntryResultNoOutput], + [[{ "id": "A_0", "state": "", "output": "ˆ" }], oneEntryResult], + ].forEach(function (values) { + it(("getKeyActionOutputArrayFromActionStateOutputArray(['" + JSON.stringify(values[0]) + "'])").padEnd(73, " ") + ' should return an array of objects', async function () { + const result = sut.getKeyActionOutputArrayFromActionStateOutputArray(read, values[0] as ActionStateOutput[]); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + + [[[{ "id": "", "state": "1", "output": "ˆ" }], []], + [[{ "id": "", "state": "", "output": "" }], []], + [[{ "id": " ", "state": " ", "output": "" }], []], + + ].forEach(function (values) { + it(("getKeyActionOutputArrayFromActionStateOutputArray(" + JSON.stringify(values[0]) + ")").padEnd(73, " ") + ' should return ' + "'[" + JSON.stringify(values[1]) + "]'", async function () { + const result = sut.getKeyActionOutputArrayFromActionStateOutputArray(read, values[0] as ActionStateOutput[]); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + + [[, []], + [undefined, []], + [null, []], + ].forEach(function (values) { + it(("getKeyActionOutputArrayFromActionStateOutputArray(" + JSON.stringify(values[0]) + ")").padEnd(73, " ") + ' should return ' + "'[" + JSON.stringify(values[1]) + "]'", async function () { + const result = sut.getKeyActionOutputArrayFromActionStateOutputArray(read, values[0] as ActionStateOutput[]); + assert.equal(JSON.stringify(result), JSON.stringify(values[1])); + }); + }); + }); + + describe('Xkb-kmn: createRuleData ', function () { + const sut = new XkbToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new XkbFileReader(compilerTestCallbacks); + [ + [ + ['../data/tests-xkb-kmn/Test_C0.keylayout'], + [new Rule("C0", '', '', 0, 0, '', '', 0, 0, 'NCAPS', 'K_A', new TextEncoder().encode('a')), + new Rule("C0", '', '', 0, 0, '', '', 0, 0, 'CAPS', 'K_A', new TextEncoder().encode('A')), + new Rule("C0", '', '', 0, 0, '', '', 0, 0, 'NCAPS', 'K_S', new TextEncoder().encode('s')), + new Rule("C0", '', '', 0, 0, '', '', 0, 0, 'NCAPS', 'K_D', new TextEncoder().encode('d'))] + ], + [ + ['../data/tests-xkb-kmn/Test_C1.keylayout'], + [new Rule("C1", '', '', 0, 0, '', '', 0, 0, 'NCAPS', 'K_S', new TextEncoder().encode('s')), + new Rule("C1", '', '', 0, 0, '', '', 0, 0, 'CAPS', 'K_S', new TextEncoder().encode('S'))] + ], + [ + ['../data/tests-xkb-kmn/Test_C2.keylayout'], + [new Rule("C2", '', '', 0, 0, 'NCAPS', 'K_U', 1, 1, 'CAPS', 'K_A', new TextEncoder().encode('Â'))], + ], + [ + ['../data/tests-xkb-kmn/Test_C3.keylayout'], + [new Rule("C3", 'NCAPS SHIFT', 'K_D', 2, 1, 'NCAPS', 'K_U', 1, 2, 'CAPS', 'K_A', new TextEncoder().encode('Â'))] + ], + + [ + ['../data/tests-xkb-kmn/Test_C3_several.keylayout'], + [new Rule("C3", 'NCAPS RALT', 'K_8', 3, 1, 'CAPS', 'K_U', 1, 3, 'NCAPS', 'K_A', new TextEncoder().encode('â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 3, 0, 'CAPS', 'K_U', 1, 0, 'NCAPS RALT', 'K_A', new TextEncoder().encode('â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 3, 0, 'NCAPS RALT', 'K_U', 2, 2, 'NCAPS', 'K_A', new TextEncoder().encode('â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 3, 0, 'NCAPS RALT', 'K_U', 2, 0, 'NCAPS RALT', 'K_A', new TextEncoder().encode('â'))] + ], + [ + ['../data/tests-xkb-kmn/Test_C0_C1_C2_C3.keylayout'], + [new Rule("C0", '', '', 0, 0, '', '', 0, 0, 'CAPS', 'K_A', new TextEncoder().encode('A')), + new Rule("C2", '', '', 0, 0, 'NCAPS RALT', 'K_EQUAL', 1, 1, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C1", '', '', 0, 0, '', '', 0, 0, 'CAPS', 'K_S', new TextEncoder().encode('S')), + new Rule("C1", '', '', 0, 0, '', '', 0, 0, 'NCAPS RALT', 'K_U', new TextEncoder().encode('S')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 1, 'CAPS', 'K_S', 2, 6, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 0, 'CAPS', 'K_U', 3, 3, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 0, 'NCAPS RALT', 'K_S', 4, 4, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 0, 'NCAPS RALT', 'K_U', 5, 5, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C1", '', '', 0, 0, '', '', 0, 0, 'CAPS', 'K_S', new TextEncoder().encode('S')), + new Rule("C1", '', '', 0, 0, '', '', 0, 0, 'NCAPS RALT', 'K_U', new TextEncoder().encode('S')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 0, 'CAPS', 'K_S', 2, 0, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 0, 'CAPS', 'K_U', 3, 0, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 0, 'NCAPS RALT', 'K_S', 4, 0, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 0, 'NCAPS RALT', 'K_U', 5, 0, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C1", '', '', 0, 0, '', '', 0, 0, 'CAPS', 'K_U', new TextEncoder().encode('U')),] + ], + ].forEach(function (values: any) { + it('data of \'' + values[0] + "' passed into createRuleData() " + 'should create an array of rules', async function () { + const inputFilename = makePathToFixture(values[0][0]); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + const processedData = sut.convertBound.convert(read, inputFilename.replace(/\.keylayout$/, '.kmn')); + assert.deepEqual(processedData.rules[0], values[1][0]); + }); + }); + }); + */ +}); diff --git a/developer/src/kmc-convert/test/kmc-convert-read/kmn-file-reader.tests.ts b/developer/src/kmc-convert/test/kmc-convert-read/kmn-file-reader.tests.ts new file mode 100644 index 00000000000..59dfb9c59ee --- /dev/null +++ b/developer/src/kmc-convert/test/kmc-convert-read/kmn-file-reader.tests.ts @@ -0,0 +1,228 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by S. Schmitt on 2025-05-12 + * + * Tests for KeylayoutToKmnConverter, KeylayoutFileReader, KmnFileWriter + * + */ +/* +import 'mocha'; +import { assert } from 'chai'; +import { compilerTestCallbacks, makePathToFixture } from './../helpers/index.js'; +import { KeylayoutFileReader } from '../../src/kmc-convert-read/keylayout-file-reader.js'; + +import { Keylayout } from "@keymanapp/developer-utils"; +import { KL_KeyMapSelect } from "../../../common/web/utils/src/types/keylayout/keylayout-xml.js"; +import { KL_KeyMap } from "../../../common/web/utils/src/types/keylayout/keylayout-xml.js"; + +describe('KeylayoutFileReader', function () { + + before(function () { + compilerTestCallbacks.clear(); + }); + + describe("read() ", function () { + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + + it('read() should return filled array on correct input', async function () { + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/Test.keylayout'); + const result = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + assert.isNotEmpty(result); + }); + + it('read() should return empty array on empty input', async function () { + const result = sutR.read(compilerTestCallbacks.loadFile("")); + assert.isNull(result); + }); + + it('read() should return empty array on space as input', async function () { + const result = sutR.read(compilerTestCallbacks.loadFile(" ")); + assert.isNull(result); + }); + + it('read() should return empty array on unavailable file name', async function () { + const inputFilenameUnavailable = makePathToFixture('../data/tests-keylayout-kmn/X.keylayout'); + const result = sutR.read(compilerTestCallbacks.loadFile(inputFilenameUnavailable)); + assert.isNull(result); + }); + + it('read() should return empty array on typo in path', async function () { + const result = sutR.read(compilerTestCallbacks.loadFile(makePathToFixture('../data|Test.keylayout'))); + assert.isNull(result); + }); + }); + + describe("validate() ", function () { + + it('validate() should return true on correct inputfile', async function () { + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/Test.keylayout'); + const result: Keylayout.KeylayoutXMLSourceFile | null = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + const validated = sutR.validate(result as Keylayout.KeylayoutXMLSourceFile, inputFilename); + assert.isTrue(validated); + }); + + it('validate() should return false on inputfile with unknown tags', async function () { + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/Test_unknownTags.keylayout'); + const result: Keylayout.KeylayoutXMLSourceFile | null = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + const validated = sutR.validate(result as Keylayout.KeylayoutXMLSourceFile, inputFilename); + assert.isFalse(validated); + }); + + it('validate() should return false on inputfile with additional tags', async function () { + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/Test_additionalTags.keylayout'); + const result: Keylayout.KeylayoutXMLSourceFile | null = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + const validated = sutR.validate(result as Keylayout.KeylayoutXMLSourceFile, inputFilename); + assert.isFalse(validated); + }); + it('validate() should return false on inputfile with missing tags', async function () { + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/Test_missingTags.keylayout'); + const result: Keylayout.KeylayoutXMLSourceFile | null = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + const validated = sutR.validate(result as Keylayout.KeylayoutXMLSourceFile, inputFilename); + assert.isFalse(validated); + }); + it('validate() should return false on no entries in action-when', async function () { + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/Test_noActionWhen.keylayout'); + const result: Keylayout.KeylayoutXMLSourceFile | null = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + const validated = sutR.validate(result as Keylayout.KeylayoutXMLSourceFile, inputFilename); + assert.isFalse(validated); + }); + /* it('validate() should return false on null as input', async function () { + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/Test_noActionWhen.keylayout'); + //const result: Keylayout.KeylayoutXMLSourceFile | null = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + const validated = sutR.validate(null, inputFilename); + assert.isFalse(validated); + }); + it('validate() should return false on undefined as input', async function () { + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/Test_noActionWhen.keylayout'); + //const result: Keylayout.KeylayoutXMLSourceFile | null = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + const validated = sutR.validate(undefined, inputFilename); + assert.isFalse(validated); + }); + }); + + describe("read() ", function () { + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + + it('read() should return filled array on correct input', async function () { + + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/tests-keylayout-kmn/Test.keylayout'); + + const result = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + assert.isNotEmpty(result); + }); + + it('read() should return empty array on empty input', async function () { + const result = sutR.read(compilerTestCallbacks.loadFile("")); + assert.isNull(result); + }); + + it('read() should return empty array on space as input', async function () { + const result = sutR.read(compilerTestCallbacks.loadFile(" ")); + assert.isNull(result); + }); + + it('read() should return empty array on unavailable file name', async function () { + const inputFilenameUnavailable = makePathToFixture('../data/tests-keylayout-kmn/tests-keylayout-kmn/X.keylayout'); + const result = sutR.read(compilerTestCallbacks.loadFile(inputFilenameUnavailable)); + assert.isNull(result); + }); + + it('read() should return empty array on typo in path', async function () { + const result = sutR.read(compilerTestCallbacks.loadFile(makePathToFixture('../data|Test.keylayout'))); + assert.isNull(result); + }); + }); + + describe('findMapIndexinKeymap ', function () { + const keyMapSelect: KL_KeyMapSelect = { + mapIndex: '', + modifier: [] + }; + + keyMapSelect.modifier.push({ keys: 'caps' }); + keyMapSelect.modifier.push({ keys: 'rightOption' }); + keyMapSelect.modifier.push({ keys: 'rightShift caps' }); + + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/tests-keylayout-kmn/Test.keylayout'); + const jsonO: Keylayout.KeylayoutXMLSourceFile | null = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + [ + ['0', true], + ['7', true], + ['999', false], + ['A', false], + [123, false], + ['', false], + [null, false], + [undefined, false], + ].forEach(function (values) { + it(("findMapIndexinKeymap(keyMapSelect.mapIndex = '" + values[0] + "')").padEnd(40, " ") + "should return " + "'" + values[1] + "'", async function () { + keyMapSelect.mapIndex = values[0] as string; + const result = sutR.findMapIndexinKeymap(jsonO as Keylayout.KeylayoutXMLSourceFile, keyMapSelect); + assert.isTrue(result === values[1]); + }); + }); + }); + + describe('findIndexinKeymapSelect ', function () { + const keyMap: KL_KeyMap = { + index: '', + key: [] + }; + keyMap.key.push({ code: '0', output: 'A' }); + keyMap.key.push({ code: '1', action: 'S' }); + keyMap.key.push({ code: '2', output: 'D' }); + + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/Test.keylayout'); + const jsonO: Keylayout.KeylayoutXMLSourceFile | null = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + [ + ['0', true], + ['7', true], + ['999', false], + ['A', false], + [123, false], + ['', false], + [null, false], + [undefined, false], + ].forEach(function (values) { + it(("findIndexinKeymapSelect(keyMap.index = '" + values[0] + "')").padEnd(40, " ") + "should return " + "'" + values[1] + "'", async function () { + keyMap.index = values[0] as string; + const result = sutR.findIndexinKeymapSelect(jsonO as Keylayout.KeylayoutXMLSourceFile, keyMap); + assert.isTrue(result === values[1]); + }); + }); + }); + + describe('checkForCorrespondingElements ', function () { + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + [ + ['../data/tests-keylayout-kmn/Test.keylayout', true], + ['../data/tests-keylayout-kmn/Test_sameKeyMapAndKeyMapselectAndJisERROR.keylayout', true], + ['../data/tests-keylayout-kmn/Test_moreKeymapSelectThanKeymapERROR.keylayout', false], + ['../data/tests-keylayout-kmn/Test_moreKeyMapThanKeyMapselectERROR.keylayout', false], + ['../data/tests-keylayout-kmn/Test_moreKeyMapThanKeyMapselectAndJisERROR.keylayout', false], + ].forEach(function (values) { + it(("checkForCorrespondingElements in " + values[0] ).padEnd(40, " ") + "should return " + "'" + values[1] + "'", async function () { + const jsonO: Keylayout.KeylayoutXMLSourceFile | null = sutR.read(compilerTestCallbacks.loadFile(makePathToFixture(values[0] as string))); + const result = sutR.checkForCorrespondingElements(jsonO as Keylayout.KeylayoutXMLSourceFile); + assert.isTrue(result === values[1]); + }); + }); + }); + + + + + +}); + +*/ diff --git a/developer/src/kmc-convert/test/kmc-convert-read/xkb-file-reader.tests.ts b/developer/src/kmc-convert/test/kmc-convert-read/xkb-file-reader.tests.ts new file mode 100644 index 00000000000..20e8dc571dd --- /dev/null +++ b/developer/src/kmc-convert/test/kmc-convert-read/xkb-file-reader.tests.ts @@ -0,0 +1,52 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by S. Schmitt on 2025-05-12 + * + * Tests for KeylayoutToKmnConverter, XkbFileReader, KmnFileWriter + * + */ + +import 'mocha'; +import { assert } from 'chai'; +import { compilerTestCallbacks, makePathToFixture } from './../helpers/index.js'; +import { XkbFileReader } from '../../src/kmc-convert-read/xkb-file-reader.js'; + +describe('XkbFileReader', function () { + + before(function () { + compilerTestCallbacks.clear(); + }); + + describe("Xkb-kmn:: read() ", function () { + const sutR = new XkbFileReader(compilerTestCallbacks); + + it('read() should return filled array on correct input', async function () { + const inputFilename = makePathToFixture('../data/tests-xkb-kmn/de-simple'); + const result = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + assert.isNotEmpty(result); + }); +/* + it('read() should return empty array on empty input', async function () { + const result = sutR.read(compilerTestCallbacks.loadFile("")); + assert.isNull(result); + }); + + it('read() should return empty array on space as input', async function () { + const result = sutR.read(compilerTestCallbacks.loadFile(" ")); + assert.isNull(result); + }); + + it('read() should return empty array on unavailable file name', async function () { + const inputFilenameUnavailable = makePathToFixture('../data/tests-xkb-kmn/X.xkb'); + const result = sutR.read(compilerTestCallbacks.loadFile(inputFilenameUnavailable)); + assert.isNull(result); + }); + + it('read() should return empty array on typo in path', async function () { + const result = sutR.read(compilerTestCallbacks.loadFile(makePathToFixture('../data|Test.xkb'))); + assert.isNull(result); + });*/ + }); + +}); diff --git a/developer/src/kmc-convert/test/kmc-convert-write/kmn-file-writer.tests.ts b/developer/src/kmc-convert/test/kmc-convert-write/kmn-file-writer.tests.ts new file mode 100644 index 00000000000..8749030915e --- /dev/null +++ b/developer/src/kmc-convert/test/kmc-convert-write/kmn-file-writer.tests.ts @@ -0,0 +1,503 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by S. Schmitt on 2025-05-12 + * + * Tests for KeylayoutToKmnConverter, KeylayoutFileReader, KmnFileWriter + * + */ +/* +import 'mocha'; +import { assert } from 'chai'; +import KEYMAN_VERSION from "@keymanapp/keyman-version"; +import { compilerTestCallbacks, compilerTestOptions, makePathToFixture } from './../helpers/index.js'; +import { KeylayoutToKmnConverter, ProcessedData, Rule } from '../../src//kmc-convert-convert/keylayout-to-kmn-converter.js'; +import { KmnFileWriter } from '../../src//kmc-convert-write/kmn-file-writer.js'; +import { KeylayoutFileReader } from '../../src//kmc-convert-read/keylayout-file-reader.js'; + +describe('KmnFileWriter', function () { + + before(function () { + compilerTestCallbacks.clear(); + }); + + describe("writeDataRules() ", function () { + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/Test.keylayout'); + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + const sutW = new KmnFileWriter(compilerTestCallbacks, compilerTestOptions); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + const converted = sut.convertBound.convert(read, inputFilename.replace(/\.keylayout$/, '.kmn')); + + it('writeDataRules() should return true (no error) if written', async function () { + const result = sutW.writeDataRules(converted); + assert.isTrue(result.length > 0); + }); + + }); + + describe("writeKmnFileHeader() ", function () { + const sut = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); + const sutR = new KeylayoutFileReader(compilerTestCallbacks); + const sutW = new KmnFileWriter(compilerTestCallbacks, compilerTestOptions); + const inputFilename = makePathToFixture('../data/tests-keylayout-kmn/Test.keylayout'); + const read = sutR.read(compilerTestCallbacks.loadFile(inputFilename)); + const converted = sut.convertBound.convert(read, inputFilename.replace(/\.keylayout$/, '.kmn')); + + const outExpectedFirst: string = + "c ..................................................................................................................\n" + + "c ..................................................................................................................\n" + + "c Keyman keyboard generated by kmn-convert version: " + KEYMAN_VERSION.VERSION + "\n" + + "c from Ukelele file: "; + + const outExpectedLast: 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"; + + it(('writeKmnFileHeader should return store text with filename ').padEnd(62, " ") + 'on correct input', async function () { + const writtenCorrectName = sutW.writeKmnFileHeader(converted); + assert.equal(writtenCorrectName, (outExpectedFirst + converted.keylayoutFilename + outExpectedLast)); + }); + }); + + describe('convertToUnicodeCharacter ', function () { + const sutW = new KmnFileWriter(compilerTestCallbacks, compilerTestOptions); + [ + + ["<", '<'], + ["a", 'a'], + ["ሴ", 'ሴ'], + ["W̊", "W̊"], + ['😎', '😎'], + ["ab", 'ab'], + ["ሴЖ", 'ሴЖ'], + ["ẘẈ", "ẘẈ"], + ["😎😆", '😎😆'], + ["aሴ😆", 'aሴ😆'], + + ["a", 'a'], + ["ሴ", 'ሴ'], + ["ẘ", "ẘ"], + ["😏", '😏'], + ["", '\u0002'], + ["�", undefined], + ["a", 'a'], + ["ሴ", 'ሴ'], + ["ẛ", "ẛ"], + ["😆", '😆'], + ["", '\u0003'], + ["󴉀", '󴉀'], + ["@", undefined], + [">", '>'], + ["<", '<'], + [""", '"'], + ["'", "'"], + [">", undefined], + ["␤", '␤'], + ["␕", '␕'], + ["", ''], + ["", ''], + [undefined, undefined], + [null, undefined], + + ['&', '&'], + ['&;', '&;'], + ['&&', '&&'], + ['&&;', '&&;'], + ["&#&#", undefined], + ["&#x&#x", undefined], + ["&#", undefined], + ["&#;", undefined], + ["&#x", undefined], + ["&#x;", undefined], + ['&##', undefined], + ['&##;', undefined], + ["􏿿", "􏿿"], + ["􏿿", "􏿿"], + ["�", undefined], + ["�", undefined], + ['Ӓ56', undefined], + +//.................................................................. + + ["a", 'a'], + ["ሴ", 'ሴ'], + ["😎", '😎'], + ["", '\u0002'], + ["�", undefined], + ["a", 'a'], + ["ሴ", 'ሴ'], + ["😆", '😆'], + ["", '\u0003'], + ["󴉀", '󴉀'], + + ["@", undefined], + ["a", 'a'], + ["ሴ", 'ሴ'], + ['😎', '😎'], + ["W̊", "W̊"], + ["ab", 'ab'], + ["", ''], + ["␤", '␤'], + ["␕", '␕'], + ["", ''], + [undefined, undefined], + [null, undefined] + ].forEach(function (values) { + it(('should convert "' + values[0] + '"').padEnd(25, " ") + 'to "' + values[1] + '"', async function () { + const result = sutW.convertToUnicodeCharacter(values[0] as string); + assert.equal(result, values[1]); + }); + }); + }); + + describe('reviewRules messages', function () { + const sutW = new KmnFileWriter(compilerTestCallbacks, compilerTestOptions); + [ + [[new Rule("C0", '', '', 0, 0, '', '', 0, 0, 'UNAVAILABLE', 'K_A', new TextEncoder().encode('A'))], + [''], + [''], + ['c WARNING: unavailable modifier : here: ']], + + [[new Rule("C1", '', '', 0, 0, 'CAPS', 'K_EQUAL', 0, 0, 'UNAVAILABLE', 'K_B', new TextEncoder().encode('B'))], + [''], + [''], + ['c WARNING: unavailable modifier : here: ']], + + [[new Rule("C2", '', '', 0, 0, 'CAPS', 'K_EQUAL', 0, 0, 'UNAVAILABLE', 'K_C', new TextEncoder().encode('C'),)], + [''], + [''], + ['c WARNING: unavailable modifier : here: ']], + + [[new Rule("C2", '', '', 0, 0, 'UNAVAILABLE_dk', 'K_EQUAL', 0, 0, 'UNAVAILABLE', 'K_C', new TextEncoder().encode('C'),)], + [''], + ['c WARNING: unavailable modifier : here: '], + ['c WARNING: unavailable modifier : here: ']], + + [[new Rule("C3", 'UNAVAILABLE_prev_dk', 'K_D', 0, 0, 'UNAVAILABLE_dk', 'K_EQUAL', 0, 0, 'SHIFT', 'K_C', new TextEncoder().encode('D'),)], + ['c WARNING: unavailable modifier : here: '], + ['c WARNING: unavailable modifier : here: '], + ['c WARNING: unavailable superior rule ( [UNAVAILABLE_dk K_EQUAL] > dk(B0) ) : here: ']], + + [[new Rule("C3", 'UNAVAILABLE_prev_dk', 'K_D', 0, 0, 'UNAVAILABLE_dk', 'K_EQUAL', 0, 0, 'UNAVAIL', 'K_C', new TextEncoder().encode('D'),)], + ['c WARNING: unavailable modifier : here: '], + ['c WARNING: unavailable modifier : here: '], + ['c WARNING: unavailable modifier : here: ']], + + [[new Rule("C3", 'CAPS', 'K_D', 0, 0, 'RALT', 'K_EQUAL', 0, 0, 'SHIFT', 'K_C', new TextEncoder().encode('D'),)], + [''], + [''], + ['']], + + [[new Rule("C3", 'X', 'K_X', 0, 0, 'Y', 'K_Y', 0, 0, 'SHIFT', 'K_Z', new TextEncoder().encode('D'),)], + ['c WARNING: unavailable modifier : here: '], + ['c WARNING: unavailable modifier : here: '], + ['c WARNING: unavailable superior rule ( [Y K_Y] > dk(B0) ) : here: ']], + + ].forEach(function (values: (string[] | Rule[])[], index: number) { + it(('rule " ' + (values[0][0] as Rule).ruleType as string + ' "') + 'should create "' + values[1] + ' | ' + values[2] + ' | ' + values[3] + '"', async function () { + const result: string[] = sutW.reviewRules(values[0] as Rule[], 0); + assert.equal(result[0], values[1][0]); + assert.equal(result[1], values[2][0]); + assert.equal(result[2], values[3][0]); + }); + }); + }); + + describe('reviewRules messages duplicate and ambiguous', function () { + const sutW = new KmnFileWriter(compilerTestCallbacks, compilerTestOptions); + [ + //all + [[ + new Rule("C3", 'LALT', 'K_A', 0, 0, 'SHIFT', 'K_B', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X')), + new Rule("C3", 'LALT', 'K_A', 0, 0, 'SHIFT', 'K_B', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X')),], + ['c WARNING: duplicate rule: earlier: [LALT K_A] > dk(C0) here: '], + ["c WARNING: duplicate rule: earlier: dk(B0) + [SHIFT K_B] > dk(B0) here: "], + ["c WARNING: duplicate rule: earlier: dk(B0) + [CAPS K_C] > 'X' here: "]], + + //6-6 dup + [[ + new Rule("C3", 'LALT', 'K_A', 0, 0, 'SHIFT', 'K_B', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X')), + new Rule("C3", 'CTRL', 'K_D', 0, 0, 'NCAPS', 'K_E', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X')),], + [''], + [""], + ["c WARNING: duplicate rule: earlier: dk(B0) + [CAPS K_C] > 'X' here: "]], + + //6-6 amb + [[ + new Rule("C3", 'LALT', 'K_A', 0, 0, 'SHIFT', 'K_B', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X')), + new Rule("C3", 'CTRL', 'K_D', 0, 0, 'NCAPS', 'K_E', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('Y')),], + [''], + [""], + ["c WARNING: ambiguous rule: earlier: dk(B0) + [CAPS K_C] > 'X' here: "]], + + // 5-5 amb + [[ + new Rule("C3", 'LALT', 'K_A', 0, 0, 'NCAPS', 'K_B', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X')), + new Rule("C3", 'LALT', 'K_A', 0, 0, 'NCAPS', 'K_B', 0, 1, 'RALT', 'K_F', new TextEncoder().encode('X')),], + ['c WARNING: duplicate rule: earlier: [LALT K_A] > dk(C0) here: '], + ["c WARNING: ambiguous rule: earlier: dk(B0) + [NCAPS K_B] > dk(B0) here: "], [''], + ], + + // 5-5 dup + [[ + new Rule("C3", 'LALT', 'K_A', 0, 0, 'NCAPS', 'K_B', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X')), + new Rule("C3", 'LALT', 'K_A', 0, 0, 'NCAPS', 'K_B', 0, 0, 'RALT', 'K_F', new TextEncoder().encode('X')),], + ['c WARNING: duplicate rule: earlier: [LALT K_A] > dk(C0) here: '], + ["c WARNING: duplicate rule: earlier: dk(B0) + [NCAPS K_B] > dk(B0) here: "], + ['']], + + // 4-2 amb + [[ + new Rule("C3", 'LALT', 'K_A', 0, 0, 'SHIFT', 'K_B', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X')), + new Rule("C2", '', '', 0, 0, 'LALT', 'K_A', 0, 0, 'RALT', 'K_F', new TextEncoder().encode('X')),], + ['c WARNING: ambiguous rule: later: [LALT K_A] > dk(C0) here: '], + [''], + ['']], + + // 4-4 amb + [[ + new Rule("C3", 'LALT', 'K_A', 0, 0, 'SHIFT', 'K_B', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X')), + new Rule("C3", 'LALT', 'K_A', 1, 1, 'NCAPS', 'K_E', 0, 0, 'RALT', 'K_F', new TextEncoder().encode('Y')),], + ['c WARNING: ambiguous rule: earlier: [LALT K_A] > dk(C0) here: '], + [""], + [''],], + + // 4-4 dup + [[ + new Rule("C3", 'LALT', 'K_A', 0, 0, 'SHIFT', 'K_B', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X')), + new Rule("C3", 'LALT', 'K_A', 0, 0, 'NCAPS', 'K_E', 0, 0, 'RALT', 'K_F', new TextEncoder().encode('X')),], + ['c WARNING: duplicate rule: earlier: [LALT K_A] > dk(C0) here: '], + [''], + ['']], + + // 4-2 amb + [[ + new Rule("C3", 'LALT', 'K_A', 0, 0, 'SHIFT', 'K_B', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X')), + new Rule("C2", '', '', 0, 0, 'LALT', 'K_A', 0, 0, 'RALT', 'K_F', new TextEncoder().encode('Y')),], + ['c WARNING: ambiguous rule: later: [LALT K_A] > dk(C0) here: '], + [''], + ['']], + + // 6-3 dup + [[ + new Rule("C2", '', '', 0, 0, 'SHIFT', 'K_B', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X')), + new Rule("C3", 'CTRL', 'K_D', 0, 0, 'NCAPS', 'K_E', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X')),], + [''], + ["c WARNING: duplicate rule: earlier: dk(C0) + [CAPS K_C] > 'X' here: "], + [''],], + + // 6-3 amb + [[ + new Rule("C2", '', '', 0, 0, 'SHIFT', 'K_B', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X')), + new Rule("C3", 'CTRL', 'K_D', 0, 0, 'NCAPS', 'K_E', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('Y')),], + [''], + ["c WARNING: ambiguous rule: earlier: dk(C0) + [CAPS K_C] > 'X' here: "], + [''],], + + // 2-4 amb + [[ + new Rule("C2", '', '', 0, 0, 'SHIFT', 'K_B', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X')), + new Rule("C3", 'SHIFT', 'K_B', 0, 0, 'NCAPS', 'K_E', 0, 0, 'RALT', 'K_F', new TextEncoder().encode('Y')),], + ['c WARNING: ambiguous rule: earlier: [SHIFT K_B] > dk(A0) here: '], + [''], + ['']], + + //2-2 amb + [[ + new Rule("C2", '', '', 0, 0, 'SHIFT', 'K_B', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X')), + new Rule("C2", '', '', 0, 0, 'SHIFT', 'K_B', 1, 1, 'RALT', 'K_F', new TextEncoder().encode('Y')),], + [''], + ['c WARNING: ambiguous rule: earlier: [SHIFT K_B] > dk(C0) here: '], + ['']], + + // 2-2 dup + [[ + new Rule("C2", '', '', 0, 0, 'SHIFT', 'K_B', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X')), + new Rule("C2", '', '', 0, 0, 'SHIFT', 'K_B', 0, 0, 'RALT', 'K_F', new TextEncoder().encode('Y')),], + [''], + ['c WARNING: duplicate rule: earlier: [SHIFT K_B] > dk(C0) here: '], + ['']], + + // 3-3 dup + [[ + new Rule("C2", '', '', 0, 0, 'SHIFT', 'K_B', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X')), + new Rule("C2", '', '', 0, 0, 'NCAPS', 'K_E', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X')),], + [''], + [''], + ["c WARNING: duplicate rule: earlier: dk(A0) + [CAPS K_C] > 'X' here: "]], + + // 3-3 amb + [[ + new Rule("C2", '', '', 0, 0, 'SHIFT', 'K_B', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X')), + new Rule("C2", '', '', 0, 0, 'NCAPS', 'K_E', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('Y')),], + [''], + [''], + ["c WARNING: ambiguous rule: earlier: dk(A0) + [CAPS K_C] > 'X' here: "]], + + // 2-1 amb + [[ + new Rule("C2", '', '', 0, 0, 'RALT', 'K_B', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X')), + new Rule("C0", '', '', 0, 0, '', '', 0, 0, 'RALT', 'K_B', new TextEncoder().encode('Y'))], + [''], + [''], + ['c WARNING: ambiguous rule: later: [RALT K_B] > dk(A0) here: ']], + + // 1-1 amb + [[ + new Rule("C0", '', '', 0, 0, '', '', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X')), + new Rule("C0", '', '', 0, 0, '', '', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('Y'))], + [''], + [''], + ["c WARNING: ambiguous rule: earlier: [CAPS K_C] > 'X' here: "]], + + // 1-1 amb + [[ + new Rule("C0", '', '', 0, 0, '', '', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X')), + new Rule("C0", '', '', 0, 0, '', '', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('X'))], + [''], + [''], + ["c WARNING: duplicate rule: earlier: [CAPS K_C] > 'X' here: "]], + + ].forEach(function (values: (string[] | Rule[])[], index: number) { + it('rule ' + (values[0][0] as Rule).ruleType as string + ' should create " ' + ' "' + values[1] + ' | ' + values[2] + ' | ' + values[3] + '"', async function () { + const result: string[] = sutW.reviewRules(values[0] as Rule[], 1); + assert.equal(result[0], values[1][0]); + assert.equal(result[1], values[2][0]); + assert.equal(result[2], values[3][0]); + }); + }); + }); + + describe('reviewRules messages duplicate and ambiguous with Extra warning', function () { + const sutW = new KmnFileWriter(compilerTestCallbacks, compilerTestOptions); + [[[ + new Rule("C0", '', '', 0, 0, '', '', 0, 0, 'RALT', 'K_B', new TextEncoder().encode('X')), + new Rule("C2", '', '', 0, 0, 'RALT', 'K_B', 0, 0, 'CAPS', 'K_C', new TextEncoder().encode('Y')), + new Rule("C0", '', '', 0, 0, '', '', 0, 0, 'RALT', 'K_B', new TextEncoder().encode('Z')) + ], + [''], + [''], + ["c WARNING: ambiguous rule: later: [RALT K_B] > dk(A0) ambiguous rule: earlier: [RALT K_B] > 'X' here: PLEASE CHECK THE FOLLOWING RULE AS IT WILL NOT BE WRITTEN ! "]], + ].forEach(function (values: (string[] | Rule[])[], index: number) { + it(('rule ' + (values[0][0]as Rule).ruleType as string + ' should create " ' + ' "') + values[1] + ' | ' + values[2] + ' | ' + values[3] + '"', async function () { + const result: string[] = sutW.reviewRules(values[0]as Rule[], 2); + assert.equal(result[0], values[1][0]); + assert.equal(result[1], values[2][0]); + assert.equal(result[2], values[3][0]); + }); + }); + }); + + describe('write from intermediate data array', function () { + const sutW = new KmnFileWriter(compilerTestCallbacks, compilerTestOptions); + [ + [ + [ // see ../data/Test_C0.keylayout / + new Rule("C0", '', '', 0, 0, '', '', 0, 0, 'NCAPS', 'K_A', new TextEncoder().encode('a')), + new Rule("C0", '', '', 0, 0, '', '', 0, 0, 'CAPS', 'K_A', new TextEncoder().encode('A')), + new Rule("C0", '', '', 0, 0, '', '', 0, 0, 'NCAPS', 'K_S', new TextEncoder().encode('s')), + new Rule("C0", '', '', 0, 0, '', '', 0, 0, 'NCAPS', 'K_D', new TextEncoder().encode('d')) + ], + ["+ [NCAPS K_A] > 'a'\n" + + "+ [CAPS K_A] > 'A'\n\n" + + "+ [NCAPS K_S] > 's'\n\n" + + "+ [NCAPS K_D] > 'd'\n"] + ], + [ + [ // see ../data/Test_C1.keylayout / + new Rule("C1", '', '', 0, 0, '', '', 0, 0, 'NCAPS', 'K_S', new TextEncoder().encode('s')), + new Rule("C1", '', '', 0, 0, '', '', 0, 0, 'CAPS', 'K_S', new TextEncoder().encode('S')) + ], + ["+ [NCAPS K_S] > 's'\n" + + "+ [CAPS K_S] > 'S'\n"] + ], + [ + [ // see ../data/Test_C2.keylayout / + new Rule("C2", '', '', 0, 0, 'NCAPS', 'K_U', 1, 1, 'CAPS', 'K_A', new TextEncoder().encode('Â')) + ], + ["+ [NCAPS K_U] > dk(A1)\n" + + "dk(A1) + [CAPS K_A] > 'Â'\n\n"] + ], + [ + [ // see ../data/Test_C3.keylayout / + new Rule("C3", 'NCAPS SHIFT', 'K_D', 2, 1, 'NCAPS', 'K_U', 1, 2, 'CAPS', 'K_A', new TextEncoder().encode('Â')) + ], + ["+ [NCAPS SHIFT K_D] > dk(A2)\n" + + "dk(A2) + [NCAPS K_U] > dk(B1)\n" + + "dk(B1) + [CAPS K_A] > 'Â'\n\n" + ] + ], + [ + [ // see ../data/Test_C0_C1_C2_C3.keylayout / + new Rule("C0", '', '', 0, 0, '', '', 0, 0, 'CAPS', 'K_A', new TextEncoder().encode('A')), + new Rule("C2", '', '', 0, 0, 'NCAPS RALT', 'K_EQUAL', 1, 1, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C1", '', '', 0, 0, '', '', 0, 0, 'CAPS', 'K_S', new TextEncoder().encode('S')), + new Rule("C1", '', '', 0, 0, '', '', 0, 0, 'NCAPS RALT', 'K_U', new TextEncoder().encode('S')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 1, 'CAPS', 'K_S', 2, 6, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 0, 'CAPS', 'K_U', 3, 3, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 0, 'NCAPS RALT', 'K_S', 4, 4, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 0, 'NCAPS RALT', 'K_U', 5, 5, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C1", '', '', 0, 0, '', '', 0, 0, 'CAPS', 'K_S', new TextEncoder().encode('S')), + new Rule("C1", '', '', 0, 0, '', '', 0, 0, 'NCAPS RALT', 'K_U', new TextEncoder().encode('S')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 0, 'CAPS', 'K_S', 2, 0, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 0, 'CAPS', 'K_U', 3, 0, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 0, 'NCAPS RALT', 'K_S', 4, 0, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 6, 0, 'NCAPS RALT', 'K_U', 5, 0, 'CAPS', 'K_D', new TextEncoder().encode('Â')), + new Rule("C1", '', '', 0, 0, '', '', 0, 0, 'CAPS', 'K_U', new TextEncoder().encode('U')), + ], + ["+ [CAPS K_A] > 'A'\n" + + "+ [CAPS K_S] > 'S'\n\n" + + "+ [NCAPS RALT K_U] > 'S'\n" + + "+ [CAPS K_U] > 'U'\n" + + "+ [NCAPS RALT K_EQUAL] > dk(A1)\n" + + "dk(A1) + [CAPS K_D] > 'Â'\n\n" + + "+ [NCAPS RALT K_8] > dk(A6)\n" + + "dk(A6) + [CAPS K_S] > dk(B2)\n" + + "dk(B2) + [CAPS K_D] > 'Â'\n\n" + + "dk(A6) + [CAPS K_U] > dk(B3)\n" + + "dk(B3) + [CAPS K_D] > 'Â'\n\n" + + "dk(A6) + [NCAPS RALT K_S] > dk(B4)\n" + + "dk(B4) + [CAPS K_D] > 'Â'\n\n" + + "dk(A6) + [NCAPS RALT K_U] > dk(B5)\n" + + "dk(B5) + [CAPS K_D] > 'Â'\n\n"] + ], + [ + [ // see ../data/Test_C3_several.keylayout / + new Rule("C3", 'NCAPS RALT', 'K_8', 3, 1, 'CAPS', 'K_U', 1, 3, 'NCAPS', 'K_A', new TextEncoder().encode('â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 3, 0, 'CAPS', 'K_U', 1, 0, 'NCAPS RALT', 'K_A', new TextEncoder().encode('â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 3, 0, 'NCAPS RALT', 'K_U', 2, 2, 'NCAPS', 'K_A', new TextEncoder().encode('â')), + new Rule("C3", 'NCAPS RALT', 'K_8', 3, 0, 'NCAPS RALT', 'K_U', 2, 0, 'NCAPS RALT', 'K_A', new TextEncoder().encode('â')) + ], + ["+ [NCAPS RALT K_8] > dk(A3)\n" + + "dk(A3) + [CAPS K_U] > dk(B1)\n" + + "dk(B1) + [NCAPS K_A] > 'â'\n\n" + + "dk(B1) + [NCAPS RALT K_A] > 'â'\n\n" + + "dk(A3) + [NCAPS RALT K_U] > dk(B2)\n" + + "dk(B2) + [NCAPS K_A] > 'â'\n\n" + + "dk(B2) + [NCAPS RALT K_A] > 'â'\n\n" + ] + ], + ].forEach(function (values: (string[] | Rule[])[], index: number) { + it(('an array of Rules should create a set of kmn rules '), async function () { + const data: ProcessedData = { + keylayoutFilename: "", + kmnFilename: "", + modifiers: [[]], + rules: values[0] as Rule[] + }; + const result1 = sutW.writeDataRules(data); + assert.isTrue(result1 === values[1][0]); + }); + }); + }); + +}); +*/ \ No newline at end of file diff --git a/developer/src/kmc-convert/test/test-keylayout-to-kmn-converter.ts b/developer/src/kmc-convert/test/test-keylayout-to-kmn-converter.ts deleted file mode 100644 index 449dba9afca..00000000000 --- a/developer/src/kmc-convert/test/test-keylayout-to-kmn-converter.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Keyman is copyright (C) SIL International. MIT License. - */ -import 'mocha'; -import {assert} from 'chai'; -import {compilerTestCallbacks, compilerTestOptions} from './helpers/index.js'; -import {KeylayoutToKmnConverter} from '../src/keylayout-to-kmn/keylayout-to-kmn-converter.js'; - -describe('KeylayoutToKmnConverter', function() { - - before(function() { - compilerTestCallbacks.clear(); - }); - - it('should throw on null inputs', async function () { - // const inputFilename = makePathToFixture('file.keylayout'); - const converter = new KeylayoutToKmnConverter(compilerTestCallbacks, compilerTestOptions); - // note, could use 'chai as promised' library to make this more fluent: - let threw = false; - try { - await converter.run(null, null, null); - } catch { - threw = true; - } - assert.isTrue(threw); - }); - - }); diff --git a/developer/src/kmc-convert/test/tsconfig.json b/developer/src/kmc-convert/test/tsconfig.json index ae9de6c9a5f..78127d66db9 100644 --- a/developer/src/kmc-convert/test/tsconfig.json +++ b/developer/src/kmc-convert/test/tsconfig.json @@ -8,7 +8,7 @@ "baseUrl": ".", }, "include": [ - "**/test-*.ts", + "**/*.tests.ts", "./helpers/index.ts", ], "references": [ diff --git a/package-lock.json b/package-lock.json index d668b01a7e7..db123f18226 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,7 +55,7 @@ "@web/dev-server-import-maps": "^0.2.0", "@web/test-runner": "^0.18.1", "@web/test-runner-playwright": "^0.11.0", - "ajv": "^8.12.0", + "ajv": "^8.17.1", "ajv-cli": "^5.0.0", "ajv-formats": "^2.1.1", "chai": "^5.1.0", @@ -151,6 +151,7 @@ "restructure": "3.0.1" }, "devDependencies": { + "ajv": "^8.12.0", "ajv-cli": "^5.0.0", "ajv-formats": "^2.1.1", @@ -367,7 +368,8 @@ "name": "@keymanapp/kmc-convert", "license": "MIT", "dependencies": { - "@keymanapp/developer-utils": "*" + "@keymanapp/developer-utils": "*", + "ajv": "^8.17.1" }, "devDependencies": { "@keymanapp/developer-test-helpers": "*", @@ -1936,6 +1938,23 @@ "string-argv": "~0.3.1" } }, + "node_modules/@microsoft/api-extractor/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@microsoft/api-extractor/node_modules/ajv-formats": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", @@ -4758,6 +4777,7 @@ } }, "node_modules/ajv": { + "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", @@ -7814,7 +7834,6 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "dev": true, "license": "MIT" }, "node_modules/fast-fifo": { @@ -9519,8 +9538,7 @@ "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -11782,7 +11800,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "engines": { "node": ">=0.10.0" } diff --git a/package.json b/package.json index b95599c8bb0..697bf312e17 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "@web/dev-server-import-maps": "^0.2.0", "@web/test-runner": "^0.18.1", "@web/test-runner-playwright": "^0.11.0", - "ajv": "^8.12.0", + "ajv": "^8.17.1", "ajv-cli": "^5.0.0", "ajv-formats": "^2.1.1", "chai": "^5.1.0", diff --git a/resources/standards-data/keylayout/create_keylayout_schema.sh b/resources/standards-data/keylayout/create_keylayout_schema.sh new file mode 100755 index 00000000000..92815832f6c --- /dev/null +++ b/resources/standards-data/keylayout/create_keylayout_schema.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +# Copyright: © SIL International. +# Description: create a *.schema.json file from a *.XSD file +# Create Date: 20 May 2025 +# Authors: S. Schmitt + + +set -eu + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../../resources/build/builder-basic.inc.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +. "$KEYMAN_ROOT/resources/build/jq.inc.sh" + +cd "$THIS_SCRIPT_PATH" + + DTD_DIR="${THIS_SCRIPT_PATH}/dtd" + +# a file to check + CHECK_1="${DTD_DIR}/keylayout.xsd" + + if [[ ! -f "${CHECK_1}" ]]; + then + builder_die "${CHECK_1} does not exist" + fi + + builder_echo heading "Converting XSD to JSON…" +for xsd in dtd/*.xsd; +do + base=$(basename "${xsd}" .xsd | tr A-Z a-z ) + json=${base}.schema.json + builder_echo debug "${xsd} -> ${json}" + (cd .. ; npx -p jgexml xsd2json ${THIS_SCRIPT_PATH}/"${xsd}" ${THIS_SCRIPT_PATH}/"${json}") || exit + + mv "${json}" tmp.json + # reformat with prettier(JQ) + ${JQ} . -S < tmp.json > "${json}" || (rm tmp.json ; builder_die "failed to transform final schema ${json}") + rm tmp.json +done + +echo +builder_echo success "Keylayout copied correctly from ${DTD_DIR}" +echo diff --git a/resources/standards-data/keylayout/dtd/keylayout.dtd b/resources/standards-data/keylayout/dtd/keylayout.dtd new file mode 100644 index 00000000000..1e7064c6b24 --- /dev/null +++ b/resources/standards-data/keylayout/dtd/keylayout.dtd @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/standards-data/keylayout/dtd/keylayout.xsd b/resources/standards-data/keylayout/dtd/keylayout.xsd new file mode 100644 index 00000000000..554d5bbd273 --- /dev/null +++ b/resources/standards-data/keylayout/dtd/keylayout.xsd @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/standards-data/keylayout/keylayout.schema.json b/resources/standards-data/keylayout/keylayout.schema.json new file mode 100644 index 00000000000..e42b23c290f --- /dev/null +++ b/resources/standards-data/keylayout/keylayout.schema.json @@ -0,0 +1,240 @@ +{ + "$schema": "http://json-schema.org/schema#", + "additionalProperties": false, + "properties": { + "keyboard": { + "additionalProperties": false, + "properties": { + "actions": { + "additionalProperties": false, + "properties": { + "action": { + "items": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "when": { + "items": { + "additionalProperties": false, + "properties": { + "next": { + "type": "string" + }, + "output": { + "type": "string" + }, + "state": { + "type": "string" + } + }, + "type": "object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "when" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "action" + ], + "type": "object" + }, + "group": { + "type": "string" + }, + "id": { + "type": "string" + }, + "keyMapSet": { + "items": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "keyMap": { + "items": { + "additionalProperties": false, + "properties": { + "baseIndex": { + "type": "string" + }, + "baseMapSet": { + "type": "string" + }, + "index": { + "type": "string" + }, + "key": { + "items": { + "additionalProperties": false, + "properties": { + "action": { + "type": "string" + }, + "code": { + "type": "string" + }, + "output": { + "type": "string" + } + }, + "type": "object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "key" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "keyMap" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + }, + "layouts": { + "additionalProperties": false, + "properties": { + "layout": { + "items": { + "additionalProperties": false, + "properties": { + "first": { + "type": "string" + }, + "last": { + "type": "string" + }, + "mapSet": { + "type": "string" + }, + "modifiers": { + "type": "string" + } + }, + "type": "object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "layout" + ], + "type": "object" + }, + "maxout": { + "type": "string" + }, + "modifierMap": { + "items": { + "additionalProperties": false, + "properties": { + "defaultIndex": { + "type": "string" + }, + "id": { + "type": "string" + }, + "keyMapSelect": { + "items": { + "additionalProperties": false, + "properties": { + "mapIndex": { + "type": "string" + }, + "modifier": { + "items": { + "additionalProperties": false, + "properties": { + "keys": { + "type": "string" + } + }, + "type": "object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "modifier" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "keyMapSelect" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + }, + "name": { + "type": "string" + }, + "terminators": { + "additionalProperties": false, + "properties": { + "when": { + "items": { + "additionalProperties": false, + "properties": { + "output": { + "type": "string" + }, + "state": { + "type": "string" + } + }, + "type": "object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "when" + ], + "type": "object" + } + }, + "required": [ + "layouts", + "modifierMap", + "keyMapSet", + "actions", + "terminators" + ] + } + }, + "required": [ + "keyboard" + ], + "title": "C:/Projects/keyman/keyman/resources/standards-data/keylayout/dtd/keylayout.xsd", + "type": "object" +} diff --git a/resources/standards-data/keylayout/readme.md b/resources/standards-data/keylayout/readme.md new file mode 100644 index 00000000000..2a09af113e7 --- /dev/null +++ b/resources/standards-data/keylayout/readme.md @@ -0,0 +1,24 @@ + +# Keylayout + +A keylayout file is a configuration file used macOS to define custom keyboard mappings, mapping physical key presses to specific characters or Unicode symbols. These XML-based files, created with Ukelele, allow users to create custom layouts for different languages or special haracters, such as mapping Option + Key combinations. With kmc-convert we can convert these keylayout files to .kmn files which can be used in Keyman keyboards. +For validating a .keylayout file a keylayout.schema.json is needed. + +This is to get a **keylayout.schema.json**: + + +- We first need to have a `.XSD file` created from a `.keylayout file` (which is an xml-file type ) +` e.g. resources\standards-data\keylayout\dtd\keylayout.xsd` + + +- Then we need to run `./create_keylayout_schema.sh` to obtain a `.schema.json` file +` e.g resources\standards-data\keylayout\create_keylayout_schema.sh` + + +- Then we need to run `build.sh configure` of `common/web/types/` which will create a `schema.validate.mjs` file +` e.g. common/web/types/build.sh configure` + + +- The .mjs can then be used via `SchemaValidators` to validate keylayout files +` e.g. SchemaValidators.default.keylayout(source)` +