From 3497e97b00a730be0a18aa8f34cb65d61f767b2a Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Sat, 13 Jul 2024 07:28:18 +1000 Subject: [PATCH] feat(developer): automatic version detection for `U_xxxx_yyyy` ids Currently, automatic Keyman minimum version determination works for all features in Keyman keyboards _except_ `U_xxxx_yyyy` touch key identifiers. This change adds the automatic keyboard versioning functionality to the keymanweb compiler as well, following the pattern that's in kmcmplib. This makes the `store(&version)` line completely optional, and it may be possible to deprecate it in future versions of Keyman Developer. --- common/web/types/src/kmx/kmx.ts | 50 +++++++++----- .../src/kmw-compiler/compiler-globals.ts | 51 ++++++++++++-- .../src/kmw-compiler/javascript-strings.ts | 39 +++++++---- .../src/kmw-compiler/kmw-compiler-messages.ts | 6 +- .../kmc-kmn/src/kmw-compiler/kmw-compiler.ts | 33 ++++++--- .../src/kmw-compiler/validate-layout-file.ts | 20 +++--- .../version_u_xxxx_yyyy.keyman-touch-layout | 53 +++++++++++++++ .../test/fixtures/kmw/version_u_xxxx_yyyy.kmn | 7 ++ .../fixtures/kmw/version_u_xxxx_yyyy_14.kmn | 8 +++ .../src/kmc-kmn/test/kmw/test-kmw-compiler.ts | 68 +++++++++++++------ developer/src/kmc-kmn/test/test-features.ts | 12 ++-- developer/src/kmcmplib/src/versioning.cpp | 8 ++- 12 files changed, 273 insertions(+), 82 deletions(-) create mode 100644 developer/src/kmc-kmn/test/fixtures/kmw/version_u_xxxx_yyyy.keyman-touch-layout create mode 100644 developer/src/kmc-kmn/test/fixtures/kmw/version_u_xxxx_yyyy.kmn create mode 100644 developer/src/kmc-kmn/test/fixtures/kmw/version_u_xxxx_yyyy_14.kmn diff --git a/common/web/types/src/kmx/kmx.ts b/common/web/types/src/kmx/kmx.ts index c5989ec115..35c0c61c3d 100644 --- a/common/web/types/src/kmx/kmx.ts +++ b/common/web/types/src/kmx/kmx.ts @@ -7,6 +7,25 @@ import * as r from 'restructure'; // In memory representations of KMX structures // kmx-builder will transform these to the corresponding COMP_xxxx +export enum KMX_Version { + VERSION_30 = 0x00000300, + VERSION_31 = 0x00000301, + VERSION_32 = 0x00000302, + VERSION_40 = 0x00000400, + VERSION_50 = 0x00000500, + VERSION_501 = 0x00000501, + VERSION_60 = 0x00000600, + VERSION_70 = 0x00000700, + VERSION_80 = 0x00000800, + VERSION_90 = 0x00000900, + VERSION_100 = 0x00000A00, + VERSION_140 = 0x00000E00, + VERSION_150 = 0x00000F00, + VERSION_160 = 0x00001000, + VERSION_170 = 0x00001100 +}; + + export class KEYBOARD { fileVersion?: number; // dwFileVersion (TSS_FILEVERSION) @@ -129,22 +148,21 @@ export class KMXFile { // File version identifiers (COMP_KEYBOARD.dwFileVersion) // - public static readonly VERSION_30 = 0x00000300; - public static readonly VERSION_31 = 0x00000301; - public static readonly VERSION_32 = 0x00000302; - public static readonly VERSION_40 = 0x00000400; - public static readonly VERSION_50 = 0x00000500; - public static readonly VERSION_501 = 0x00000501; - public static readonly VERSION_60 = 0x00000600; - public static readonly VERSION_70 = 0x00000700; - public static readonly VERSION_80 = 0x00000800; - public static readonly VERSION_90 = 0x00000900; - public static readonly VERSION_100 = 0x00000A00; - public static readonly VERSION_140 = 0x00000E00; - public static readonly VERSION_150 = 0x00000F00; - - public static readonly VERSION_160 = 0x00001000; - public static readonly VERSION_170 = 0x00001100; + public static readonly VERSION_30 = KMX_Version.VERSION_30; + public static readonly VERSION_31 = KMX_Version.VERSION_31; + public static readonly VERSION_32 = KMX_Version.VERSION_32; + public static readonly VERSION_40 = KMX_Version.VERSION_40; + public static readonly VERSION_50 = KMX_Version.VERSION_50; + public static readonly VERSION_501 = KMX_Version.VERSION_501; + public static readonly VERSION_60 = KMX_Version.VERSION_60; + public static readonly VERSION_70 = KMX_Version.VERSION_70; + public static readonly VERSION_80 = KMX_Version.VERSION_80; + public static readonly VERSION_90 = KMX_Version.VERSION_90; + public static readonly VERSION_100 = KMX_Version.VERSION_100; + public static readonly VERSION_140 = KMX_Version.VERSION_140; + public static readonly VERSION_150 = KMX_Version.VERSION_150; + public static readonly VERSION_160 = KMX_Version.VERSION_160; + public static readonly VERSION_170 = KMX_Version.VERSION_170; public static readonly VERSION_MIN = this.VERSION_50; public static readonly VERSION_MAX = this.VERSION_170; diff --git a/developer/src/kmc-kmn/src/kmw-compiler/compiler-globals.ts b/developer/src/kmc-kmn/src/kmw-compiler/compiler-globals.ts index 3b09a18f47..52149c44c8 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/compiler-globals.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/compiler-globals.ts @@ -14,6 +14,18 @@ export let FUnreachableKeys: KMX.KEY[]; export let FCallFunctions: string[]; export let FFix183_LadderLength: number; +let _minimumKeymanVersion: number; + +export function minimumKeymanVersion() { + return _minimumKeymanVersion; +} + +export function minimumKeymanVersionToString() { + const major = (_minimumKeymanVersion & KMX.KMXFile.VERSION_MASK_MAJOR) >> 8; + const minor = _minimumKeymanVersion & KMX.KMXFile.VERSION_MASK_MINOR; + return `${major}.${minor}`; +} + export function setupGlobals( _callbacks: CompilerCallbacks, _options: CompilerOptions, @@ -33,20 +45,45 @@ export function setupGlobals( FUnreachableKeys = []; FCallFunctions = []; FFix183_LadderLength = 100; // TODO: option + _minimumKeymanVersion = fk.fileVersion; } -export function IsKeyboardVersion10OrLater(): boolean { +function isKeyboardVersionAutomaticallyDetermined() { + return (fk.flags & KMX.KMXFile.KF_AUTOMATICVERSION) == KMX.KMXFile.KF_AUTOMATICVERSION; +} + +function verifyMinimumRequiredKeymanVersion(version: KMX.KMX_Version) { + if(isKeyboardVersionAutomaticallyDetermined()) { + _minimumKeymanVersion = Math.max(version, _minimumKeymanVersion); + return true; + } + + return _minimumKeymanVersion >= version; +} + +/** + * Verify that minimum supported Keyman version in the keyboard is version 15.0, + * and upgrade to that version if possible and necessary. + * + * Will upgrade the minimum version to 15.0 if `KF_AUTOMATICVERSION` flag is set + * for the keyboard, which correlates to having no `store(&version)` line in the + * .kmn source file. + * + * @returns `true` if the version is now 15.0 or higher, `false` if a lower + * version has been specified in the source file `store(&version)` line. + */ +export function verifyMinimumRequiredKeymanVersion15(): boolean { + return verifyMinimumRequiredKeymanVersion(KMX.KMX_Version.VERSION_150); +} + +export function isKeyboardVersion10OrLater(): boolean { return fk.fileVersion >= KMX.KMXFile.VERSION_100; } -export function IsKeyboardVersion14OrLater(): boolean { +export function isKeyboardVersion14OrLater(): boolean { return fk.fileVersion >= KMX.KMXFile.VERSION_140; } -export function IsKeyboardVersion15OrLater(): boolean { - return fk.fileVersion >= KMX.KMXFile.VERSION_150; -} - -export function IsKeyboardVersion17OrLater(): boolean { +export function isKeyboardVersion17OrLater(): boolean { return fk.fileVersion >= KMX.KMXFile.VERSION_170; } diff --git a/developer/src/kmc-kmn/src/kmw-compiler/javascript-strings.ts b/developer/src/kmc-kmn/src/kmw-compiler/javascript-strings.ts index 55eca7791f..046001096f 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/javascript-strings.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/javascript-strings.ts @@ -1,10 +1,12 @@ import { TSentinelRecord, GetSuppChar, ExpandSentinel, incxstr, xstrlen, xstrlen_printing } from "./util.js"; import { KMX } from "@keymanapp/common-types"; -import { callbacks, FCallFunctions, FFix183_LadderLength, FMnemonic, FTabStop, FUnreachableKeys, IsKeyboardVersion10OrLater, IsKeyboardVersion14OrLater, kmxResult, nl, options } from "./compiler-globals.js"; +import { callbacks, FCallFunctions, FFix183_LadderLength, FMnemonic, FTabStop, FUnreachableKeys, + kmxResult, nl, options, isKeyboardVersion10OrLater, isKeyboardVersion14OrLater } from "./compiler-globals.js"; import { KmwCompilerMessages } from "./kmw-compiler-messages.js"; import { FormatModifierAsBitflags, RuleIsExcludedByPlatform } from "./kmw-compiler.js"; -import { KMXCodeNames, SValidIdentifierCharSet, UnreachableKeyCodes, USEnglishShift, USEnglishUnshift, USEnglishValues } from "./constants.js"; +import { KMXCodeNames, SValidIdentifierCharSet, UnreachableKeyCodes, USEnglishShift, + USEnglishUnshift, USEnglishValues } from "./constants.js"; import { KMWVKeyNames, TKeymanWebTouchStandardKey, VKeyNames } from "./keymanweb-key-codes.js"; export function JavaScript_Name(i: number, pwszName: string, KeepNameForPersistentStorage: boolean = false): string { // I3659 @@ -49,7 +51,7 @@ export function JavaScript_Store(fk: KMX.KEYBOARD, line: number, pwsz: string): let n = pwsz.indexOf(wcsentinel); // Start: plain text store. Always use for < 10.0, conditionally for >= 10.0. - if(n < 0 || !IsKeyboardVersion10OrLater()) { + if(n < 0 || !isKeyboardVersion10OrLater()) { result = '"'; while(pwsz.length) { if(pwsz.charCodeAt(0) == KMX.KMXFile.UC_SENTINEL) { @@ -247,20 +249,31 @@ export function JavaScript_Shift(fkp: KMX.KEY, FMnemonic: boolean): number { } if (fkp.ShiftFlags & KMX.KMXFile.ISVIRTUALKEY) { - if(IsKeyboardVersion10OrLater()) { + if(isKeyboardVersion10OrLater()) { + // don't attempt to upgrade to v10 at this point, only if we are already v10+ // Full chiral modifier and state key support starts with KeymanWeb 10.0 return fkp.ShiftFlags; } // Non-chiral support only and no support for state keys if (fkp.ShiftFlags & (KMX.KMXFile.LCTRLFLAG | KMX.KMXFile.RCTRLFLAG | KMX.KMXFile.LALTFLAG | KMX.KMXFile.RALTFLAG)) { // I4118 + // TODO: automatic version upgrade + // if(verifyMinimumRequiredKeymanVersion10()) { + // // upgrade to v10 if possible + // return fkp.ShiftFlags; + // } callbacks.reportMessage(KmwCompilerMessages.Warn_ExtendedShiftFlagsNotSupportedInKeymanWeb({line:fkp.Line, flags: 'LALT, RALT, LCTRL, RCTRL'})); } if (fkp.ShiftFlags & ( KMX.KMXFile.CAPITALFLAG | KMX.KMXFile.NOTCAPITALFLAG | KMX.KMXFile.NUMLOCKFLAG | KMX.KMXFile.NOTNUMLOCKFLAG | KMX.KMXFile.SCROLLFLAG | KMX.KMXFile.NOTSCROLLFLAG)) { // I4118 - callbacks.reportMessage(KmwCompilerMessages.Warn_ExtendedShiftFlagsNotSupportedInKeymanWeb({line:fkp.Line, flags: 'CAPS and NCAPS'})); + // TODO: automatic version upgrade + // if(verifyMinimumRequiredKeymanVersion10()) { + // // upgrade to v10 if possible + // return fkp.ShiftFlags; + // } + callbacks.reportMessage(KmwCompilerMessages.Warn_ExtendedShiftFlagsNotSupportedInKeymanWeb({line:fkp.Line, flags: 'CAPS and NCAPS'})); } return KMX.KMXFile.ISVIRTUALKEY | (fkp.ShiftFlags & (KMX.KMXFile.K_SHIFTFLAG | KMX.KMXFile.K_CTRLFLAG | KMX.KMXFile.K_ALTFLAG)); @@ -403,7 +416,7 @@ export function JavaScript_KeyAsString(fkp: KMX.KEY, FMnemonic: boolean): string } export function JavaScript_ContextMatch(fk: KMX.KEYBOARD, fkp: KMX.KEY, context: string): string { - if(IsKeyboardVersion10OrLater()) { + if(isKeyboardVersion10OrLater()) { return JavaScript_FullContextValue(fk, fkp, context); } else { @@ -429,7 +442,7 @@ function CheckStoreForInvalidFunctions(fk: KMX.KEYBOARD, key: KMX.KEY, store: KM n = store.dpString.indexOf(wcsentinel); // Disable the check with versions >= 10.0, since we now support deadkeys in stores. - if (n >= 0 && !IsKeyboardVersion10OrLater) { + if (n >= 0 && !isKeyboardVersion10OrLater()) { rec = ExpandSentinel(fk, store.dpString, n); callbacks.reportMessage(KmwCompilerMessages.Error_NotSupportedInKeymanWebStore({code:GetCodeName(rec.Code), store:store.dpName})); } @@ -630,7 +643,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp for(let I = 1; I < Index; I++) { let recContext = ExpandSentinel(fk, pwszContext, x); - if(IsKeyboardVersion10OrLater()) { + if(isKeyboardVersion10OrLater()) { if(recContext.IsSentinel && [KMX.KMXFile.CODE_NUL, KMX.KMXFile.CODE_IFOPT, KMX.KMXFile.CODE_IFSYSTEMSTORE].includes(recContext.Code)) { Result--; } @@ -666,7 +679,8 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp case KMX.KMXFile.CODE_NOTANY: // #917: Minimum version required is 14.0: the KCXO function was only added for 14.0 // Note that this is checked in compiler.cpp as well, so this error can probably never occur - if(!IsKeyboardVersion14OrLater()) { + // TODO: automatic version upgrade + if(!isKeyboardVersion14OrLater()) { callbacks.reportMessage(KmwCompilerMessages.Error_NotAnyRequiresVersion14({line:fkp.Line})); } Result += nlt + `k.KCXO(${len},t,${AdjustIndex(fkp.dpContext, xstrlen(fkp.dpContext))},${AdjustIndex(fkp.dpContext, ContextIndex)+1});`; @@ -701,7 +715,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp let pwsz = pwszOutput; if(fkp != null) { - if(IsKeyboardVersion10OrLater()) { + if(isKeyboardVersion10OrLater()) { // KMW >= 10.0 use the full, sentinel-based length for context deletions. len = xstrlen(fkp.dpContext); let n = len; @@ -726,7 +740,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp } let x = 0; - if(IsKeyboardVersion10OrLater() && pwsz.length > 0) { + if(isKeyboardVersion10OrLater() && pwsz.length > 0) { if(!isGroupReadOnly(fk, fgp)) { Result += nlt+`k.KDC(${len},t);`; // I3681 } @@ -932,7 +946,8 @@ export function zeroPadHex(n: number, len: number): string { * @return string of JavaScript code, e.g. 'keyCodes.K_A /* 0x41 * /' */ function FormatKeyAsString(key: number): string { - if(IsKeyboardVersion10OrLater()) { + // TODO: automatic version upgrade + if(isKeyboardVersion10OrLater()) { // Depends on flags defined in KeymanWeb 10.0 if (key <= 255 && KMWVKeyNames[key] != '') { return 'keyCodes.'+KMWVKeyNames[key]+ ' /* 0x' + zeroPadHex(key, 2) + ' */'; diff --git a/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts b/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts index 19d58d64ba..f2a88fed7f 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts @@ -3,7 +3,7 @@ import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerEvent, CompilerM import { kmnfile } from "./compiler-globals.js"; const Namespace = CompilerErrorNamespace.KmwCompiler; -// 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; @@ -49,4 +49,8 @@ export class KmwCompilerMessages extends KmnCompilerMessages { static HINT_TouchLayoutUsesUnsupportedGesturesDownlevel = SevHint | 0x0005; static Hint_TouchLayoutUsesUnsupportedGesturesDownlevel = (o:{keyId:string}) => m(this.HINT_TouchLayoutUsesUnsupportedGesturesDownlevel, `The touch layout uses a flick or multi-tap gesture on key ${def(o.keyId)}, which is only available on version 17.0+ of Keyman`); + + static INFO_MinimumEngineVersion = SevInfo | 0x0006; + static Info_MinimumEngineVersion = (o:{version:string}) => m(this.INFO_MinimumEngineVersion, + `The compiler has assigned a minimum web engine version of ${o.version} based on features used in this keyboard`); }; diff --git a/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler.ts b/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler.ts index 974e4fd485..ae67b731e0 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler.ts @@ -1,6 +1,6 @@ import { KMX, CompilerOptions, CompilerCallbacks, KvkFileReader, VisualKeyboard, KmxFileReader, KvkFile } from "@keymanapp/common-types"; import { ExpandSentinel, incxstr, xstrlen } from "./util.js"; -import { options, nl, FTabStop, setupGlobals, IsKeyboardVersion10OrLater, callbacks, FFix183_LadderLength, FCallFunctions, fk, IsKeyboardVersion17OrLater } from "./compiler-globals.js"; +import { options, nl, FTabStop, setupGlobals, callbacks, FFix183_LadderLength, FCallFunctions, fk, minimumKeymanVersionToString, isKeyboardVersion10OrLater, isKeyboardVersion17OrLater } from "./compiler-globals.js"; import { JavaScript_ContextMatch, JavaScript_KeyAsString, JavaScript_Name, JavaScript_OutputString, JavaScript_Rules, JavaScript_Shift, JavaScript_ShiftAsString, JavaScript_Store, zeroPadHex } from './javascript-strings.js'; import { KmwCompilerMessages } from "./kmw-compiler-messages.js"; import { ValidateLayoutFile } from "./validate-layout-file.js"; @@ -206,8 +206,13 @@ export function WriteCompiledKeyboard( // Following line caches the Keyman major version `${FTabStop}this._v=(typeof keyman!="undefined"&&typeof keyman.version=="string")?parseInt(keyman.version,10):9;${nl}` + `${FTabStop}this.KI="${sName}";${nl}` + - `${FTabStop}this.KN="${RequotedString(sFullName)}";${nl}` + - `${FTabStop}this.KMINVER="${(keyboard.fileVersion & KMX.KMXFile.VERSION_MASK_MAJOR) >> 8}.${keyboard.fileVersion & KMX.KMXFile.VERSION_MASK_MINOR}";${nl}` + + `${FTabStop}this.KN="${RequotedString(sFullName)}";${nl}`; + + // We split the result text here to allow for setting the minimum required + // version at the very end of this function + const resultPrefix = result; + + result = `${FTabStop}this.KV=${sVisualKeyboard};${nl}` + `${FTabStop}this.KDU=${fDisplayUnderlying?'1':'0'};${nl}` + `${FTabStop}this.KH=${sHelp};${nl}` + @@ -400,7 +405,17 @@ export function WriteCompiledKeyboard( } result += sEmbedJS + '}' + nl; // I3681 - return result; + + const minVer = minimumKeymanVersionToString(); + const resultMinVer = `${FTabStop}this.KMINVER="${minVer}";${nl}`; + + if((fk.flags & KMX.KMXFile.KF_AUTOMATICVERSION) == KMX.KMXFile.KF_AUTOMATICVERSION) { + // Note: the KeymanWeb compiler is responsible for reporting minimum + // version for the web targets + callbacks.reportMessage(KmwCompilerMessages.Info_MinimumEngineVersion({version:minVer})); + } + + return resultPrefix + resultMinVer + result; } /// @@ -440,9 +455,9 @@ function GetKeyboardModifierBitmask(keyboard: KMX.KEYBOARD, fMnemonic: boolean): /// @return string of JavaScript code /// function JavaScript_SetupDebug() { - if(IsKeyboardVersion10OrLater()) { + if(isKeyboardVersion10OrLater()) { if(options.saveDebug) { - if(IsKeyboardVersion17OrLater()) { + if(isKeyboardVersion17OrLater()) { return 'var modCodes = KeymanWeb.Codes.modifierCodes;'+nl+ FTabStop+'var keyCodes = KeymanWeb.Codes.keyCodes;'+nl; } else { @@ -455,7 +470,7 @@ function JavaScript_SetupDebug() { } function JavaScript_SetupProlog() { - if(IsKeyboardVersion10OrLater()) { + if(isKeyboardVersion10OrLater()) { return 'if(typeof keyman === \'undefined\') {'+nl+ FTabStop+'console.log(\'Keyboard requires KeymanWeb 10.0 or later\');'+nl+ FTabStop+'if(typeof tavultesoft !== \'undefined\') tavultesoft.keymanweb.util.alert("This keyboard requires KeymanWeb 10.0 or later");'+nl+ @@ -465,7 +480,7 @@ function JavaScript_SetupProlog() { } function JavaScript_SetupEpilog() { - if(IsKeyboardVersion10OrLater()) { + if(isKeyboardVersion10OrLater()) { return '}'; } return ''; @@ -560,7 +575,7 @@ export function FormatModifierAsBitflags(FBitMask: number): string { //TODO: We need to think about mnemonic layouts which are incompletely supported at present //tavultesoft.keymanweb.osk. - if(IsKeyboardVersion10OrLater()) { + if(isKeyboardVersion10OrLater()) { // This depends on flags defined in KeymanWeb 10.0 result = ''; diff --git a/developer/src/kmc-kmn/src/kmw-compiler/validate-layout-file.ts b/developer/src/kmc-kmn/src/kmw-compiler/validate-layout-file.ts index e2f0d23a73..82c05dc03f 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/validate-layout-file.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/validate-layout-file.ts @@ -1,7 +1,9 @@ import { KMX, TouchLayout, TouchLayoutFileReader, TouchLayoutFileWriter } from "@keymanapp/common-types"; -import { callbacks, fk, IsKeyboardVersion14OrLater, IsKeyboardVersion15OrLater, IsKeyboardVersion17OrLater } from "./compiler-globals.js"; +import { callbacks, minimumKeymanVersion, verifyMinimumRequiredKeymanVersion15, + isKeyboardVersion14OrLater, isKeyboardVersion17OrLater } from "./compiler-globals.js"; import { JavaScript_Key } from "./javascript-strings.js"; -import { TRequiredKey, CRequiredKeys, CSpecialText, CSpecialText14Map, CSpecialText17Map, CSpecialTextMinVer, CSpecialTextMaxVer } from "./constants.js"; +import { TRequiredKey, CRequiredKeys, CSpecialText, CSpecialText14Map, CSpecialText17Map, + CSpecialTextMinVer, CSpecialTextMaxVer } from "./constants.js"; import { KeymanWebTouchStandardKeyNames, KMWAdditionalKeyNames, VKeyNames } from "./keymanweb-key-codes.js"; import { KmwCompilerMessages } from "./kmw-compiler-messages.js"; import * as Osk from '../compiler/osk.js'; @@ -121,7 +123,7 @@ function CheckKey( callbacks.reportMessage(KmwCompilerMessages.Error_TouchLayoutInvalidIdentifier({keyId: FId, platformName: platformId, layerId: layer.id})); return false; } - else if (FValid == TKeyIdType.Key_Unicode_Multi && !IsKeyboardVersion15OrLater()) { + else if (FValid == TKeyIdType.Key_Unicode_Multi && !verifyMinimumRequiredKeymanVersion15()) { callbacks.reportMessage(KmwCompilerMessages.Error_TouchLayoutIdentifierRequires15({keyId: FId, platformName: platformId, layerId: layer.id})); return false; } @@ -144,10 +146,11 @@ function CheckKey( // Keyman versions before 14 do not support '*special*' labels on non-special keys. // ZWNJ use, however, is safe because it will be transformed in function // TransformSpecialKeys14 to '<|>', which does not require the custom OSK font. - const mapVersion = Math.max(Math.min(fk.fileVersion, CSpecialTextMaxVer), CSpecialTextMinVer); + const mapVersion = Math.max(Math.min(minimumKeymanVersion(), CSpecialTextMaxVer), CSpecialTextMinVer); const specialText = CSpecialText.get(mapVersion); if(specialText.includes(FText) && - !IsKeyboardVersion14OrLater() && + // TODO: automatic version upgrade + !isKeyboardVersion14OrLater() && !([TouchLayout.TouchLayoutKeySp.special, TouchLayout.TouchLayoutKeySp.specialActive].includes(FKeyType))) { callbacks.reportMessage(KmwCompilerMessages.Warn_TouchLayoutSpecialLabelOnNormalKey({ keyId: FId, @@ -187,7 +190,7 @@ function CheckDictionaryKeyValidity(fk: KMX.KEYBOARD, FDictionary: string[]) { function TransformSpecialKeys14(FDebug: boolean, sLayoutFile: string): string { // Rewrite Special key labels that are only supported in Keyman 14+ // This code is a little ugly but effective. - if(!IsKeyboardVersion14OrLater()) { + if(!isKeyboardVersion14OrLater()) { for(let i = 0; i < CSpecialText14Map.length; i++) { // Assumes the JSON output format will not change if(FDebug) { @@ -203,7 +206,7 @@ function TransformSpecialKeys14(FDebug: boolean, sLayoutFile: string): string { function TransformSpecialKeys17(FDebug: boolean, sLayoutFile: string): string { // Rewrite Special key labels that are only supported in Keyman 17+ // This code is a little ugly but effective. - if(!IsKeyboardVersion17OrLater()) { + if(!isKeyboardVersion17OrLater()) { for(let i = 0; i < CSpecialText17Map.length; i++) { // Assumes the JSON output format will not change if(FDebug) { @@ -239,7 +242,8 @@ export function ValidateLayoutFile(fk: KMX.KEYBOARD, FDebug: boolean, sLayoutFil let hasWarnedOfGestureUseDownlevel = false; const warnGesturesIfNeeded = function(keyId: string) { - if(!hasWarnedOfGestureUseDownlevel && !IsKeyboardVersion17OrLater()) { + // TODO: automatic version upgrade + if(!hasWarnedOfGestureUseDownlevel && !isKeyboardVersion17OrLater()) { hasWarnedOfGestureUseDownlevel = true; callbacks.reportMessage(KmwCompilerMessages.Hint_TouchLayoutUsesUnsupportedGesturesDownlevel({keyId})); } diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/version_u_xxxx_yyyy.keyman-touch-layout b/developer/src/kmc-kmn/test/fixtures/kmw/version_u_xxxx_yyyy.keyman-touch-layout new file mode 100644 index 0000000000..6e32c688eb --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/version_u_xxxx_yyyy.keyman-touch-layout @@ -0,0 +1,53 @@ +{ + "tablet": { + "font": "Tahoma", + "layer": [ + { + "id": "default", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_Q", + "text": "q" + }, + { + "id": "U_0041_0300", + "text": "A'" + }, + { + "id": "U_0042_0300", + "text": "B'" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": 90, + "sp": 1 + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": 120, + "sp": 1 + }, + { + "id": "K_SPACE", + "text": "", + "width": 630, + "sp": 0 + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": 140, + "sp": 1 + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/version_u_xxxx_yyyy.kmn b/developer/src/kmc-kmn/test/fixtures/kmw/version_u_xxxx_yyyy.kmn new file mode 100644 index 0000000000..a56940c608 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/version_u_xxxx_yyyy.kmn @@ -0,0 +1,7 @@ +store(&NAME) 'version_u_xxxx_yyyy' +store(&TARGETS) 'mobile' +store(&LAYOUTFILE) 'version_u_xxxx_yyyy.keyman-touch-layout' + +begin Unicode > use(main) + +group(main) using keys diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/version_u_xxxx_yyyy_14.kmn b/developer/src/kmc-kmn/test/fixtures/kmw/version_u_xxxx_yyyy_14.kmn new file mode 100644 index 0000000000..06635955e5 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/version_u_xxxx_yyyy_14.kmn @@ -0,0 +1,8 @@ +store(&NAME) 'version_u_xxxx_yyyy' +store(&TARGETS) 'mobile' +store(&LAYOUTFILE) 'version_u_xxxx_yyyy.keyman-touch-layout' +store(&VERSION) '14.0' + +begin Unicode > use(main) + +group(main) using keys diff --git a/developer/src/kmc-kmn/test/kmw/test-kmw-compiler.ts b/developer/src/kmc-kmn/test/kmw/test-kmw-compiler.ts index 6732351576..927e535fe3 100644 --- a/developer/src/kmc-kmn/test/kmw/test-kmw-compiler.ts +++ b/developer/src/kmc-kmn/test/kmw/test-kmw-compiler.ts @@ -6,8 +6,10 @@ import { fileURLToPath } from 'url'; import fs from 'fs'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { KmnCompilerResult, KmnCompiler } from '../../src/compiler/compiler.js'; -import { ETLResult, extractTouchLayout as parseWebTestResult } from './util.js'; +import { ETLResult, extractTouchLayout } from './util.js'; import { KeymanFileTypes } from '@keymanapp/common-types'; +import { KmnCompilerMessages } from '../../src/compiler/kmn-compiler-messages.js'; +import { KmwCompilerMessages } from '../../src/kmw-compiler/kmw-compiler-messages.js'; const __dirname = dirname(fileURLToPath(import.meta.url)).replace(/\\/g, '/'); const fixturesDir = __dirname + '/../../../test/fixtures/kmw/'; @@ -38,50 +40,74 @@ describe('KeymanWeb Compiler', function() { callbacks.clear(); }); - it('should compile a complex keyboard', function() { - run_test_keyboard(kmnCompiler, 'khmer_angkor'); + it('should compile a complex keyboard', async function() { + await run_test_keyboard(kmnCompiler, 'khmer_angkor'); }); - it('should handle option stores', function() { + it('should handle option stores', async function() { // // This is enough to verify that the option store is set appropriately with // KLOAD because the fixture has that code present: // // this.s_foo_6=KeymanWeb.KLOAD(this.KI,"foo","0"); // - run_test_keyboard(kmnCompiler, 'test_options'); + await run_test_keyboard(kmnCompiler, 'test_options'); }); - it('should translate every "character style" key correctly', function() { + it('should translate every "character style" key correctly', async function() { // // This is enough to verify that every character style key is encoded in the // same way as the fixture. // - run_test_keyboard(kmnCompiler, 'test_keychars'); + await run_test_keyboard(kmnCompiler, 'test_keychars'); }); - it('should handle readonly groups', function() { - run_test_keyboard(kmnCompiler, 'test_readonly_groups'); + it('should handle readonly groups', async function() { + await run_test_keyboard(kmnCompiler, 'test_readonly_groups'); }); - it('should handle context(n) in output of rule, v10.0 generation', function() { - run_test_keyboard(kmnCompiler, 'test_contextn_in_output'); + it('should handle context(n) in output of rule, v10.0 generation', async function() { + await run_test_keyboard(kmnCompiler, 'test_contextn_in_output'); }); - it('should handle context(n) in output of rule, v9.0 generation', function() { - run_test_keyboard(kmnCompiler, 'test_contextn_in_output_9'); + it('should handle context(n) in output of rule, v9.0 generation', async function() { + await run_test_keyboard(kmnCompiler, 'test_contextn_in_output_9'); }); - it('should handle context(n) in context part of rule, v9.0 generation', function() { - run_test_keyboard(kmnCompiler, 'test_context_in_context_9'); + it('should handle context(n) in context part of rule, v9.0 generation', async function() { + await run_test_keyboard(kmnCompiler, 'test_context_in_context_9'); }); - it('should handle context(n) in context part of rule, v10.0 generation', function() { - run_test_keyboard(kmnCompiler, 'test_context_in_context'); + it('should handle context(n) in context part of rule, v10.0 generation', async function() { + await run_test_keyboard(kmnCompiler, 'test_context_in_context'); + }); + + it('should determine the minimum version correctly with U_xxxx_yyyy touch ids', async function() { + const filenames = generateTestFilenames('version_u_xxxx_yyyy'); + + let result = await kmnCompiler.run(filenames.source, null); + assert.isNotNull(result); + assert.isTrue(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumEngineVersion)); + // The min version message from the .kmn compiler is generic 208A INFO_Info; + // we expect only 1 of the info messages -- for the .kmx target (not 2) + assert.equal(callbacks.messages.filter(item => item.code == KmnCompilerMessages.INFO_Info).length, 1); + + const data = new TextDecoder('utf-8').decode(result.artifacts.js.data); + assert.match(data, /KMINVER="15.0"/, `Could not find expected 'KMINVER="15.0"'`); + }); + + it('should give an error if the minimum version specified in the keyboard does not support U_xxxx_yyyy touch ids', async function() { + const filenames = generateTestFilenames('version_u_xxxx_yyyy_14'); + + let result = await kmnCompiler.run(filenames.source, null); + assert.isNull(result); + // The min version message from the .kmn compiler is generic 208A INFO_Info + assert.isFalse(callbacks.hasMessage(KmnCompilerMessages.INFO_Info)); + assert.isFalse(callbacks.hasMessage(KmwCompilerMessages.INFO_MinimumEngineVersion)); + assert.isTrue(callbacks.hasMessage(KmwCompilerMessages.ERROR_TouchLayoutIdentifierRequires15)); }); }); - async function run_test_keyboard(kmnCompiler: KmnCompiler, id: string): Promise<{ result: KmnCompilerResult, actualCode: string, actual: ETLResult, expectedCode: string, expected: ETLResult }> { const filenames = generateTestFilenames(id); @@ -96,11 +122,11 @@ async function run_test_keyboard(kmnCompiler: KmnCompiler, id: string): expected: null, actual: null, }; - value.actual = parseWebTestResult(value.actualCode); - value.expected = parseWebTestResult(value.expectedCode); + value.actual = extractTouchLayout(value.actualCode); + value.expected = extractTouchLayout(value.expectedCode); if(debug) { - // This is mostly to allow us to verify that parseWebTestResult is doing what we want + // This is mostly to allow us to verify that extractTouchLayout is doing what we want // fs.writeFileSync(filenames.binary + '.strip.js', value.actual.js); // fs.writeFileSync(filenames.fixture + '.strip.js', value.expected.js); fs.writeFileSync(filenames.binary, value.actualCode); diff --git a/developer/src/kmc-kmn/test/test-features.ts b/developer/src/kmc-kmn/test/test-features.ts index a030ba984c..1f82a03ed5 100644 --- a/developer/src/kmc-kmn/test/test-features.ts +++ b/developer/src/kmc-kmn/test/test-features.ts @@ -24,21 +24,21 @@ describe('Keyboard compiler features', function() { const versions = [ // TODO(lowpri): we should add a test for each version + also test automatic feature detection - ['16.0', '160', KMX.KMXFile.VERSION_160], - ['17.0', '170', KMX.KMXFile.VERSION_170], + {major: '16.0', vstr: '160', vernum: KMX.KMXFile.VERSION_160}, + {major: '17.0', vstr: '170', vernum: KMX.KMXFile.VERSION_170}, ]; for(const v of versions) { - it(`should build a version ${v[0]} keyboard`, async function() { - const fixtureName = makePathToFixture('features', `version_${v[1]}.kmn`); + it(`should build a version ${v.major} keyboard`, async function() { + const fixtureName = makePathToFixture('features', `version_${v.vstr}.kmn`); - const result = await compiler.run(fixtureName, `version_${v[1]}.kmx`); + const result = await compiler.run(fixtureName, `version_${v.vstr}.kmx`); if(result === null) callbacks.printMessages(); assert.isNotNull(result); const reader = new KmxFileReader(); const keyboard = reader.read(result.artifacts.kmx.data); - assert.equal(keyboard.fileVersion, v[2]); + assert.equal(keyboard.fileVersion, v.vernum); }); } }); diff --git a/developer/src/kmcmplib/src/versioning.cpp b/developer/src/kmcmplib/src/versioning.cpp index c00dc78e80..4c2f04e1e9 100644 --- a/developer/src/kmcmplib/src/versioning.cpp +++ b/developer/src/kmcmplib/src/versioning.cpp @@ -12,8 +12,12 @@ KMX_BOOL kmcmp::CheckKeyboardFinalVersion(PFILE_KEYBOARD fk) { fk->version = VERSION_60; // minimum version that we can be safe with } - snprintf(buf, 128, "The compiler has assigned a minimum engine version of %d.%d based on features used in this keyboard", (int)((fk->version & 0xFF00) >> 8), (int)(fk->version & 0xFF)); - AddCompileWarning(buf); + if(kmcmp::CompileTarget != CKF_KEYMANWEB) { + // Note: the KeymanWeb compiler is responsible for reporting minimum + // version for the web targets + snprintf(buf, 128, "The compiler has assigned a minimum engine version of %d.%d based on features used in this keyboard", (int)((fk->version & 0xFF00) >> 8), (int)(fk->version & 0xFF)); + AddCompileWarning(buf); + } } return TRUE;