diff --git a/HISTORY.md b/HISTORY.md index 76c681d539..fefc4911e7 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,60 @@ # Keyman Version History +## 17.0.333 stable 2025-01-16 + +* fix(core): permanently disable logging (#12674) +* fix(linux): pushing of updated changelog branch (#12819) +* fix(linux): work around Lintian errors (#12817) +* fix(core): implement ldml_processor::get_key_list() (#12816) +* chore(linux): Update debian changelog (#12022) +* fix(android): use main looper to dispatch key events when OSK is hidden (#12875) +* chore: use GitHub PR titles when writing HISTORY.md (#12908) +* fix(developer): filter incorrect fonts out of .keyboard_info (#12913) + +## 17.0.332 stable 2024-11-06 + +* fix(developer): create Server config directory before options save (#12609) +* fix(developer): handle merge commits when checking git log date (#12628) +* fix(linux): set environment variable for rendering of downloads dialog (#12617) + +## 17.0.331 stable 2024-10-30 + +* fix(android): Hide suggestion banner on password fields (#12466) +* fix(common): declare dep on @keymanapp/ldml-keyboard-constants (#12475) +* fix(oem/fv): Update keyboard versions and names for fv_all.kmp (#12504) +* chore(ios): renew certificate (#12513) +* fix(developer): prevent invalid string ids (#12524) +* fix(developer): ignore excess whitespace in `` attribute (#12523) + +## 17.0.330 stable 2024-09-16 + +* refactor(android): Move Sentry and APK to publish task (#12392) +* fix(developer): rewrite ldml visual keyboard compiler (#12406) +* fix(developer): check vars string usage before definition (#12407) + +## 17.0.329 stable 2024-09-09 + +* chore(android,ios): Add ojibwa ifinal/rdot keyboards to FirstVoices (#12020) +* change(web): revert #11174, which loads keyboards before initializing the OSK (#12040) +* fix(web): unrevert #11258, leaving OSK hidden before instructed to display (#12058) +* chore(common): use `nvm` to select version of node for builds (#12074) +* fix(developer): ignore scan code if zero in debugger (#12182) +* fix(developer): enforce presence of Version field when FollowKeyboardVersion is not set, in package compiler (#12206) +* fix(developer): enforce presence of kps Info.Description field in info compilers (#12207) +* fix(web): disable fat-finger data use when mayCorrect = false (#12226) +* chore(common): allow build agents to automatically select emsdk version, and enable support for 3.1.60+ (#12245) +* fix(web): fix documentation-keyboard spacebar-text scaling (#12240) +* fix(core): set mac build version for meson cli build to 10.13 (#12246) +* change(ios): defer registration of fonts past initialization (#12241) +* chore(android,ios): Update FirstVoices keyboards to 12.15 (#12301) +* fix(core): properly support 'other' modifier state with `uint32_t` type (#12285) +* fix(developer): find last matching key in LDML key bag when building KVK (#12284) +* fix(android): check in material-stepper as internal Maven dependency (#12324) +* fix(linux): add `keymanFacename` to .ldml file (#12283) +* chore(oem/fv): Update to fv_all 13.0 (#12363) +* fix(mac): avoid crash on startup with macOS 10.15 (Catalina) (#12364) +* fix(android): skip language counts for lexical-model packages (#12368) + ## 17.0.328 stable 2024-07-27 * fix(web): add nullish test in setOsk (#12041) diff --git a/VERSION.md b/VERSION.md index 3e94a1aa9f..19627ad9a5 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -17.0.329 \ No newline at end of file +17.0.334 \ No newline at end of file diff --git a/android/KMAPro/build.gradle b/android/KMAPro/build.gradle index 7e49d3dc01..4fbfd72881 100644 --- a/android/KMAPro/build.gradle +++ b/android/KMAPro/build.gradle @@ -2,8 +2,10 @@ buildscript { repositories { google() - jcenter() mavenCentral() + flatDir { + dirs "kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/" + } } dependencies { classpath 'com.android.tools.build:gradle:7.4.0' @@ -11,16 +13,16 @@ buildscript { classpath 'io.sentry:sentry-android-gradle-plugin:2.1.2' classpath 'name.remal:gradle-plugins:1.5.0' - // From jcenter() which could be sunset in future + // From jcenter() which was deprecated August 2024 // https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/ - classpath 'com.stepstone.stepper:material-stepper:4.3.1' + classpath 'com.stepstone.stepper:material-stepper:4.3.1@aar' } } allprojects { repositories { + maven { url uri("${projectDir}/libs") } google() - jcenter() mavenCentral() maven { url "https://jitpack.io" } } diff --git a/android/KMAPro/build.sh b/android/KMAPro/build.sh index 192e321118..c348a91be7 100755 --- a/android/KMAPro/build.sh +++ b/android/KMAPro/build.sh @@ -14,6 +14,8 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "$KEYMAN_ROOT/resources/build/build-help.inc.sh" . "$KEYMAN_ROOT/resources/build/build-download-resources.sh" +. "$KEYMAN_ROOT/android/KMAPro/build-play-store-notes.inc.sh" + # This script runs from its own folder cd "$THIS_SCRIPT_PATH" @@ -32,6 +34,7 @@ builder_describe "Builds Keyman for Android app." \ "configure" \ "build" \ "test Runs lint and unit tests." \ + "publish Publishes symbols to Sentry and the APK to the Play Store." \ "--ci Don't start the Gradle daemon. For CI" \ "--upload-sentry Upload to sentry" @@ -116,3 +119,13 @@ if builder_start_action test; then builder_finish_action success test fi + +if builder_start_action publish; then + # Copy Release Notes + generateReleaseNotes + + # Publish symbols and Keyman for Android to Play Store + ./gradlew $DAEMON_FLAG publishSentry publishReleaseApk + + builder_finish_action success publish +fi diff --git a/android/KMAPro/kMAPro/build.gradle b/android/KMAPro/kMAPro/build.gradle index 226e912fbe..f431f09ea9 100644 --- a/android/KMAPro/kMAPro/build.gradle +++ b/android/KMAPro/kMAPro/build.gradle @@ -105,12 +105,17 @@ android { } // how to configure the sentry android gradle plugin -sentry { - // Disables or enables the automatic configuration of Native symbols - uploadNativeSymbols = true +task publishSentry { + doLast { + println 'Publishing Keyman symbols to Sentry' + sentry { + // Disables or enables the automatic configuration of Native symbols + uploadNativeSymbols = true - // Does or doesn't include the source code of native code for Sentry - includeNativeSources = true + // Does or doesn't include the source code of native code for Sentry + includeNativeSources = true + } + } } String env_keys_json_file = System.getenv("keys_json_file") @@ -145,7 +150,6 @@ repositories { dirs 'libs' } google() - jcenter() } dependencies { diff --git a/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1-javadoc.jar b/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1-javadoc.jar new file mode 100644 index 0000000000..895d772c39 Binary files /dev/null and b/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1-javadoc.jar differ diff --git a/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1-sources.jar b/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1-sources.jar new file mode 100644 index 0000000000..a3c4148387 Binary files /dev/null and b/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1-sources.jar differ diff --git a/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1.aar b/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1.aar new file mode 100644 index 0000000000..9bc9a3f05c Binary files /dev/null and b/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1.aar differ diff --git a/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1.pom b/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1.pom new file mode 100644 index 0000000000..5bd1fc5d39 --- /dev/null +++ b/android/KMAPro/kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/material-stepper-4.3.1.pom @@ -0,0 +1,90 @@ + + + 4.0.0 + com.stepstone.stepper + material-stepper + 4.3.1 + aar + Android Material Stepper + This library allows to use Material steppers inside Android applications. + https://github.com/stepstone-tech/android-material-stepper + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + zawadz88 + Piotr Zawadzki + piotr.zawadzki@stepstone.com + + + + https://github.com/stepstone-tech/android-material-stepper.git + https://github.com/stepstone-tech/android-material-stepper.git + https://github.com/stepstone-tech/android-material-stepper + + + + com.android.support + appcompat-v7 + 25.4.0 + compile + + + junit + junit + 4.12 + test + + + org.mockito + mockito-core + 2.7.21 + test + + + com.squareup.assertj + assertj-android + 1.1.1 + test + + + org.robolectric + robolectric + 3.3.1 + test + + + httpclient + org.apache.httpcomponents + + + commons-logging + commons-logging + + + + + org.jetbrains.kotlin + kotlin-stdlib-jre7 + 1.1.4-3 + test + + + org.jetbrains.kotlin + kotlin-reflect + 1.1.4-3 + test + + + com.nhaarman + mockito-kotlin + 1.4.0 + test + + + diff --git a/android/KMAPro/kMAPro/src/main/AndroidManifest.xml b/android/KMAPro/kMAPro/src/main/AndroidManifest.xml index 7196abdb22..d4028e8a70 100644 --- a/android/KMAPro/kMAPro/src/main/AndroidManifest.xml +++ b/android/KMAPro/kMAPro/src/main/AndroidManifest.xml @@ -7,10 +7,6 @@ - - - - diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/CheckPermissions.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/CheckPermissions.java index 888d430b0b..431d97e353 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/CheckPermissions.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/CheckPermissions.java @@ -35,8 +35,8 @@ public class CheckPermissions { checkPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE); } else { // API 33+ - //https://developer.android.com/about/versions/13/behavior-changes-13#granular-media-permissions - // Manifest.permission.READ_MEDIA_AUDIO doesn't seem to be needed + // We had to remove these MEDIA permissions from AndroidManifest.xml so these will end up failing + // https://support.google.com/googleplay/android-developer/answer/14115180?hl=en permissionsOK = permissionsOK && checkPermission(activity, Manifest.permission.READ_MEDIA_IMAGES); permissionsOK = permissionsOK && checkPermission(activity, Manifest.permission.READ_MEDIA_VIDEO); } diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/PackageActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/PackageActivity.java index 5de23272f5..17203b9ada 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/PackageActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/PackageActivity.java @@ -112,7 +112,8 @@ public class PackageActivity extends AppCompatActivity implements // Number of languages associated with the first keyboard in a keyboard package. // lexical-model packages will be 0 - final int languageCount = kmpProcessor.getLanguageCount(pkgInfo, PackageProcessor.PP_KEYBOARDS_KEY, 0); + final int languageCount = (keyboardCount > 0) ? + kmpProcessor.getLanguageCount(pkgInfo, PackageProcessor.PP_KEYBOARDS_KEY, 0) : 0; // Sanity check for keyboard packages if (pkgTarget.equals(PackageProcessor.PP_TARGET_KEYBOARDS)) { diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java index ac86b11c28..39e81bac64 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java @@ -31,6 +31,7 @@ import android.content.pm.ApplicationInfo; import android.content.res.Configuration; import android.net.Uri; import android.os.Handler; +import android.os.Looper; import android.util.DisplayMetrics; import android.util.Log; import android.view.GestureDetector; @@ -70,6 +71,10 @@ final class KMKeyboard extends WebView { protected KeyboardType keyboardType = KeyboardType.KEYBOARD_TYPE_UNDEFINED; protected ArrayList javascriptAfterLoad = new ArrayList<>(); + // .getMainLooper() returns the looper associated with the main UI thread. + // https://stackoverflow.com/questions/13974661/runonuithread-vs-looper-getmainlooper-post-in-android + private Handler jsQueuer = new Handler(Looper.getMainLooper()); + private static String currentKeyboard = null; /** @@ -368,7 +373,7 @@ final class KMKeyboard extends WebView { if(this.javascriptAfterLoad.size() > 0) { // Don't call this WebView method on just ANY thread - run it on the main UI thread. // https://stackoverflow.com/a/22611010 - this.postDelayed(new Runnable() { + jsQueuer.postDelayed(new Runnable() { @Override public void run() { StringBuilder allCalls = new StringBuilder(); diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java index b80f6766a3..1e28e4af0b 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java @@ -164,7 +164,7 @@ public final class KMKeyboardWebViewClient extends WebViewClient { // appContext instead of context? SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); boolean modelPredictionPref = false; - if (KMManager.currentLexicalModel != null) { + if (!KMManager.getMayPredictOverride() && KMManager.currentLexicalModel != null) { modelPredictionPref = prefs.getBoolean(KMManager.getLanguagePredictionPreferenceKey(KMManager.currentLexicalModel.get(KMManager.KMKey_LanguageID)), true); } KMManager.setBannerOptions(modelPredictionPref); diff --git a/android/build-publish.sh b/android/build-publish.sh deleted file mode 100755 index 29c152c993..0000000000 --- a/android/build-publish.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env bash -# CI script to publish specified app APKs to the Play Store. -# The APKs should already have been built from a separate script - -# set -x: Debugging use, print each statement -# set -x - -## START STANDARD BUILD SCRIPT INCLUDE -# adjust relative paths as necessary -THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" -. "${THIS_SCRIPT%/*}/../resources/build/build-utils.sh" -## END STANDARD BUILD SCRIPT INCLUDE - -. "$KEYMAN_ROOT/android/KMAPro/build-play-store-notes.inc.sh" - -echo Publishing APKs to Play Store - -# -# Prevents 'clear' on exit of mingw64 bash shell -# -SHLVL=0 - -display_usage ( ) { - echo "build-publish.sh [-no-daemon] [-kmapro] [-fv]" - echo - echo "Publish app to the Play Store" - echo " -no-daemon Don't start the Gradle daemon. Use for CI" - echo " -kmapro Keyman for Android" - echo " -fv First Voices" - exit 1 -} - -NO_DAEMON=false -DO_KMAPRO=false -DO_FV=false - -# Parse args -while [[ $# -gt 0 ]] ; do - key="$1" - case $key in - -no-daemon) - NO_DAEMON=true - ;; - -kmapro) - DO_KMAPRO=true - ;; - -fv) - DO_FV=true - ;; - -h|-\?) - display_usage - ;; - esac - shift # past argument -done - -# Override JAVA_HOME to OpenJDK 11 -set_java_home - -echo -echo "NO_DAEMON: $NO_DAEMON" -echo "DO_KMAPRO: $DO_KMAPRO" -echo "DO_FV: $DO_FV" -echo - -if [ "$NO_DAEMON" = true ]; then - DAEMON_FLAG=--no-daemon -else - DAEMON_FLAG= -fi - -BUILD_FLAGS="publishReleaseApk" -echo "BUILD_FLAGS $BUILD_FLAGS" - -# Publish Keyman for Android -if [ "$DO_KMAPRO" = true ]; then - # Copy Release Notes - generateReleaseNotes - cd "$KEYMAN_ROOT/android/KMAPro/" - ./gradlew $DAEMON_FLAG $BUILD_FLAGS -fi - -# Publish FV app -if [ "$DO_FV" = true ]; then - cd "$KEYMAN_ROOT/oem/firstvoices/android/" - ./gradlew $DAEMON_FLAG $BUILD_FLAGS -fi diff --git a/android/build.sh b/android/build.sh index b22d8887f1..ad650c067e 100755 --- a/android/build.sh +++ b/android/build.sh @@ -25,7 +25,7 @@ builder_describe \ configure \ build \ test \ - "publish Publishes the APKs to the Play Store." \ + "publish Publishes symbols to Sentry and the APKs to the Play Store." \ --ci+ \ --upload-sentry+ \ ":engine=KMEA Keyman Engine for Android" \ diff --git a/android/help/troubleshooting/grant-storage-permission.md b/android/help/troubleshooting/grant-storage-permission.md index 1b8554d7f0..7b47cdf4ea 100644 --- a/android/help/troubleshooting/grant-storage-permission.md +++ b/android/help/troubleshooting/grant-storage-permission.md @@ -19,13 +19,7 @@ permission to access storage / "file and media". The screenshot below is from An ### Android 13.0+ Devices -Step 1) -Go to Android Settings. +Due to Google Play Store's "Photo and Video Permissions policy", +Keyman for Android must use a system picker to browse for .kmp files from within the Keyman app. -Step 2) -Depending on your device, click "Apps" and grant Keyman permission to the following: - * Photos and videos --> Always allow all - -The screenshot below is from Android 14.0. - -![](../android_images/keyman-storage-permission-34b.png) +Follow the steps in [Installing Custom Keyboards/Dictionaries](../basic/installing-custom-packages). diff --git a/common/web/types/package.json b/common/web/types/package.json index 529624bc64..fb65261c7b 100644 --- a/common/web/types/package.json +++ b/common/web/types/package.json @@ -30,6 +30,7 @@ }, "dependencies": { "@keymanapp/keyman-version": "*", + "@keymanapp/ldml-keyboard-constants": "*", "restructure": "git+https://github.com/keymanapp/dependency-restructure.git#7a188a1e26f8f36a175d95b67ffece8702363dfc", "semver": "^7.5.2", "xml2js": "git+https://github.com/keymanapp/dependency-node-xml2js#535fe732dc408d697e0f847c944cc45f0baf0829" diff --git a/common/web/types/src/consts/modifier-key-constants.ts b/common/web/types/src/consts/modifier-key-constants.ts new file mode 100644 index 0000000000..245cbf70f3 --- /dev/null +++ b/common/web/types/src/consts/modifier-key-constants.ts @@ -0,0 +1,33 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * Modifier key bit-flags + */ + +export const ModifierKeyConstants = { + // Define Keyman Developer modifier bit-flags (exposed for use by other modules) + // Compare against /common/include/kmx_file.h. CTRL+F "#define LCTRLFLAG" to find the secton. + LCTRLFLAG: 0x0001, // Left Control flag + RCTRLFLAG: 0x0002, // Right Control flag + LALTFLAG: 0x0004, // Left Alt flag + RALTFLAG: 0x0008, // Right Alt flag + K_SHIFTFLAG: 0x0010, // Either shift flag + K_CTRLFLAG: 0x0020, // Either ctrl flag + K_ALTFLAG: 0x0040, // Either alt flag + K_METAFLAG: 0x0080, // Either Meta-key flag (tentative). Not usable in keyboard rules; + // Used internally (currently, only by KMW) to ensure Meta-key + // shortcuts safely bypass rules + // Meta key = Command key on macOS, Windows key on Windows/Linux + CAPITALFLAG: 0x0100, // Caps lock on + NOTCAPITALFLAG: 0x0200, // Caps lock NOT on + NUMLOCKFLAG: 0x0400, // Num lock on + NOTNUMLOCKFLAG: 0x0800, // Num lock NOT on + SCROLLFLAG: 0x1000, // Scroll lock on + NOTSCROLLFLAG: 0x2000, // Scroll lock NOT on + ISVIRTUALKEY: 0x4000, // It is a Virtual Key Sequence + VIRTUALCHARKEY: 0x8000, // Keyman 6.0: Virtual Key Cap Sequence NOT YET + + // Note: OTHER_MODIFIER = 0x10000, used by KMX+ for the + // other modifier flag in layers, > 16 bit so not available here. + // See keys_mod_other in keyman_core_ldml.ts +} diff --git a/common/web/types/src/kmx/kmx-plus.ts b/common/web/types/src/kmx/kmx-plus.ts index 6e3c73d33c..42dc7305a7 100644 --- a/common/web/types/src/kmx/kmx-plus.ts +++ b/common/web/types/src/kmx/kmx-plus.ts @@ -285,7 +285,7 @@ export class Vars extends Section { }); } findStringVariableValue(id: string): string { - return Vars.findVariable(this.strings, id)?.value?.value; // Unwrap: Variable, StrsItem + return Vars.findVariable(this.strings, id)?.value?.value ?? null; // Unwrap: Variable, StrsItem } substituteSetRegex(str: string, sections: DependencySections): string { return str.replaceAll(VariableParser.SET_REFERENCE, (_entire, id) => { diff --git a/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts b/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts index 4f5c30bb1a..efe3984ff0 100644 --- a/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts +++ b/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts @@ -113,6 +113,7 @@ export class LDMLKeyboardXMLSourceFileReader { for (const sub of obj) { // retain the same subtag if (!this.boxImportsAndSpecials(sub, subtag)) { + // resolveImports has already logged a message return false; } } @@ -125,6 +126,7 @@ export class LDMLKeyboardXMLSourceFileReader { boxXmlArray(obj, key); // Now, resolve the import if (!this.resolveImports(obj, subtag)) { + // resolveImports has already logged a message return false; } // now delete the import array we so carefully constructed, the caller does not @@ -132,6 +134,7 @@ export class LDMLKeyboardXMLSourceFileReader { delete obj['import']; } else { if (!this.boxImportsAndSpecials(obj[key], key)) { + // resolveImports has already logged a message return false; } } @@ -151,6 +154,7 @@ export class LDMLKeyboardXMLSourceFileReader { // first, the explicit imports for (const asImport of ([...obj['import'] as LKImport[]].reverse())) { if (!this.resolveOneImport(obj, subtag, asImport)) { + // resolveOneImport has already logged a message return false; } } @@ -161,6 +165,7 @@ export class LDMLKeyboardXMLSourceFileReader { base: constants.cldr_import_base, path: constants.cldr_implied_keys_import }, true)) { + // resolveOneImport has already logged a message return false; } } else if (subtag === 'forms') { @@ -169,6 +174,7 @@ export class LDMLKeyboardXMLSourceFileReader { base: constants.cldr_import_base, path: constants.cldr_implied_forms_import }, true)) { + // resolveOneImport has already logged a message return false; } } @@ -266,7 +272,8 @@ export class LDMLKeyboardXMLSourceFileReader { // An alternative fix would be to pull xml2js directly from github // rather than using the version tagged on npmjs.com. }); - parser.parseString(file, (e: unknown, r: unknown) => { a = r as LDMLKeyboardXMLSourceFile }); // TODO-LDML: isn't 'e' the error? + const data = new TextDecoder().decode(file); + parser.parseString(data, (e: unknown, r: unknown) => { if(e) throw e; a = r as LDMLKeyboardXMLSourceFile }); // TODO-LDML: isn't 'e' the error? return a; })(); return source; @@ -278,14 +285,23 @@ export class LDMLKeyboardXMLSourceFileReader { */ public load(file: Uint8Array): LDMLKeyboardXMLSourceFile | null { if (!file) { + throw new Error(`file parameter must not be null`); + } + + let source: LDMLKeyboardXMLSourceFile = null; + try { + source = this.loadUnboxed(file); + } catch(e) { + this.callbacks.reportMessage(CommonTypesMessages.Error_InvalidXml({e})); return null; } - const source = this.loadUnboxed(file); - if(this.boxArrays(source)) { + + if (this.boxArrays(source)) { return source; - } else { - return null; } + + // boxArrays ... resolveImports has already logged a message + return null; } loadTestDataUnboxed(file: Uint8Array): any { diff --git a/common/web/types/src/main.ts b/common/web/types/src/main.ts index 5c79ed6761..53200dec80 100644 --- a/common/web/types/src/main.ts +++ b/common/web/types/src/main.ts @@ -20,6 +20,7 @@ export { VariableParser, MarkerParser } from './ldml-keyboard/pattern-parser.js' export { LDMLKeyboardXMLSourceFileReader, LDMLKeyboardXMLSourceFileReaderOptions } from './ldml-keyboard/ldml-keyboard-xml-reader.js'; export * as Constants from './consts/virtual-key-constants.js'; +export { ModifierKeyConstants } from './consts/modifier-key-constants.js'; export { defaultCompilerOptions, CompilerBaseOptions, CompilerCallbacks, CompilerOptions, CompilerEvent, CompilerErrorNamespace, CompilerErrorSeverity, CompilerPathCallbacks, CompilerFileSystemCallbacks, CompilerCallbackOptions, diff --git a/common/web/types/src/util/common-events.ts b/common/web/types/src/util/common-events.ts index eee8a6c84c..5b3fd7d507 100644 --- a/common/web/types/src/util/common-events.ts +++ b/common/web/types/src/util/common-events.ts @@ -1,3 +1,6 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + */ import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageDef as def, CompilerMessageSpec as m } from './compiler-interfaces.js'; import { constants } from '@keymanapp/ldml-keyboard-constants'; @@ -43,4 +46,8 @@ export class CommonTypesMessages { static Error_TestDataUnexpectedArray = (o: {subtag: string}) => m(this.ERROR_TestDataUnexpectedArray, `Problem reading test data: expected single ${def(o.subtag)} element, found multiple`); + + static ERROR_InvalidXml = SevError | 0x0008; + static Error_InvalidXml = (o:{e: any}) => + m(this.ERROR_InvalidXml, `The XML file could not be read: ${(o.e ?? '').toString()}`); }; diff --git a/core/cross-mac-arm64.build b/core/cross-mac-arm64.build index 720e9f3100..faf3e6e9c1 100644 --- a/core/cross-mac-arm64.build +++ b/core/cross-mac-arm64.build @@ -5,9 +5,9 @@ cpu = 'arm64' endian = 'little' [binaries] -c = ['clang', '-arch', 'arm64'] -objc = ['clang', '-arch', 'arm64'] -cpp = ['clang++', '-arch', 'arm64'] +c = ['clang', '-arch', 'arm64', '-mmacosx-version-min=10.13'] +objc = ['clang', '-arch', 'arm64', '-mmacosx-version-min=10.13'] +cpp = ['clang++', '-arch', 'arm64', '-mmacosx-version-min=10.13'] ar = 'ar' ld = 'ld' strip = 'strip' diff --git a/core/cross-mac-x86_64.build b/core/cross-mac-x86_64.build index 2ab2c62a60..3378fe11a4 100644 --- a/core/cross-mac-x86_64.build +++ b/core/cross-mac-x86_64.build @@ -5,9 +5,9 @@ cpu = 'x86_64' endian = 'little' [binaries] -c = ['clang', '-arch', 'x86_64'] -objc = ['clang', '-arch', 'x86_64'] -cpp = ['clang++', '-arch', 'x86_64'] +c = ['clang', '-arch', 'x86_64', '-mmacosx-version-min=10.13'] +objc = ['clang', '-arch', 'x86_64', '-mmacosx-version-min=10.13'] +cpp = ['clang++', '-arch', 'x86_64', '-mmacosx-version-min=10.13'] ar = 'ar' ld = 'ld' strip = 'strip' diff --git a/core/include/keyman/keyman_core_api_vkeys.h b/core/include/keyman/keyman_core_api_vkeys.h index 7e4a6d181a..7e65d8ad91 100644 --- a/core/include/keyman/keyman_core_api_vkeys.h +++ b/core/include/keyman/keyman_core_api_vkeys.h @@ -16,6 +16,7 @@ #pragma once enum km_core_modifier_state { + KM_CORE_MODIFIER_NONE = 0, KM_CORE_MODIFIER_LCTRL = 1 << 0, KM_CORE_MODIFIER_RCTRL = 1 << 1, KM_CORE_MODIFIER_LALT = 1 << 2, diff --git a/core/src/kmx/kmx_plus.h b/core/src/kmx/kmx_plus.h index a8e5c1f0e4..e3a510b76d 100644 --- a/core/src/kmx/kmx_plus.h +++ b/core/src/kmx/kmx_plus.h @@ -455,7 +455,13 @@ public: bool setLayr(const COMP_KMXPLUS_LAYR *newLayr); bool valid() const; + /** + * @param list index from 0 to layr->listCount + */ const COMP_KMXPLUS_LAYR_LIST *getList(KMX_DWORD list) const; + /** + * @param entry index value: COMP_KMXPLUS_LAYR_LIST.layer but less than COMP_KMXPLUS_LAYR_LIST.layer+COMP_KMXPLUS_LAYR_LIST.count + */ const COMP_KMXPLUS_LAYR_ENTRY *getEntry(KMX_DWORD entry) const; const COMP_KMXPLUS_LAYR_ROW *getRow(KMX_DWORD row) const; const COMP_KMXPLUS_LAYR_KEY *getKey(KMX_DWORD key) const; diff --git a/core/src/kmx/kmx_processevent.cpp b/core/src/kmx/kmx_processevent.cpp index 604c6f6fe2..80e0f39c04 100644 --- a/core/src/kmx/kmx_processevent.cpp +++ b/core/src/kmx/kmx_processevent.cpp @@ -12,7 +12,7 @@ using namespace kmx; /* Globals */ KMX_BOOL km::core::kmx::g_debug_ToConsole = FALSE; -KMX_BOOL km::core::kmx::g_debug_KeymanLog = TRUE; +KMX_BOOL km::core::kmx::g_debug_KeymanLog = FALSE; // workaround for #12661 KMX_BOOL km::core::kmx::g_silent = FALSE; /* diff --git a/core/src/ldml/ldml_processor.cpp b/core/src/ldml/ldml_processor.cpp index f90fcfaa94..4e42c3acea 100644 --- a/core/src/ldml/ldml_processor.cpp +++ b/core/src/ldml/ldml_processor.cpp @@ -85,7 +85,7 @@ ldml_processor::ldml_processor(path const & kb_path, const std::vector } else { str = keyEntry->get_to_string(); } - keys.add((km_core_virtual_key)kmapEntry->vkey, (uint16_t)kmapEntry->mod, str); + keys.add((km_core_virtual_key)kmapEntry->vkey, kmapEntry->mod, str); } } // else: no keys! but still valid. Just, no keys. @@ -348,8 +348,7 @@ km_core_attr const & ldml_processor::attributes() const { } km_core_keyboard_key * ldml_processor::get_key_list() const { - km_core_keyboard_key* key_list = new km_core_keyboard_key(KM_CORE_KEYBOARD_KEY_LIST_END); - return key_list; + return keys.get_key_list(); } km_core_keyboard_imx * ldml_processor::get_imx_list() const { diff --git a/core/src/ldml/ldml_vkeys.cpp b/core/src/ldml/ldml_vkeys.cpp index 07a2ff08ad..dc3f22f18b 100644 --- a/core/src/ldml/ldml_vkeys.cpp +++ b/core/src/ldml/ldml_vkeys.cpp @@ -8,6 +8,8 @@ #include "ldml_vkeys.hpp" #include "kmx_file.h" #include +#include +#include namespace km { namespace core { @@ -17,11 +19,119 @@ vkeys::vkeys() : vkey_to_string() { } void -vkeys::add(km_core_virtual_key vk, uint16_t modifier_state, std::u16string output) { +vkeys::add(km_core_virtual_key vk, km_core_ldml_modifier_state modifier_state, std::u16string output) { // construct key const vkey_id id(vk, modifier_state); // assign the string vkey_to_string[id] = output; + // includes all keys - including gaps. + all_vkeys.insert(id); +} + +km_core_keyboard_key * +vkeys::get_key_list() const { + // prescan to find out which modifier flags are used + + std::size_t other_key_count = 0; // number of 'OTHER' keys, which will need to expand to all of the other_state set ( so other_state.size()) + + std::set all_modifiers; + for (const auto &k : all_vkeys) { + const auto mod = k.second; + if (mod == LDML_KEYS_MOD_OTHER) { + other_key_count++; + } + all_modifiers.insert(mod); + } + std::set other_state; + + // Alt + if (all_modifiers.count(LDML_KEYS_MOD_ALT) == 0 && all_modifiers.count(LDML_KEYS_MOD_ALTL) == 0 && all_modifiers.count(LDML_KEYS_MOD_ALTR) == 0) { + // no ALT keys were seen, so OTHER includes ALT + other_state.insert(KM_CORE_MODIFIER_ALT); + } else if(all_modifiers.count(LDML_KEYS_MOD_ALTL) == 0) { + other_state.insert(KM_CORE_MODIFIER_RALT); + } else if(all_modifiers.count(LDML_KEYS_MOD_ALTR) == 0) { + other_state.insert(KM_CORE_MODIFIER_LALT); + } + + // ctrl + if (all_modifiers.count(LDML_KEYS_MOD_CTRL) == 0 && all_modifiers.count(LDML_KEYS_MOD_CTRLL) == 0 && all_modifiers.count(LDML_KEYS_MOD_CTRLR) == 0) { + // no CTRL keys were seen, so OTHER includes CTRL + other_state.insert(KM_CORE_MODIFIER_CTRL); + } else if(all_modifiers.count(LDML_KEYS_MOD_CTRLL) == 0) { + other_state.insert(KM_CORE_MODIFIER_RCTRL); + } else if(all_modifiers.count(LDML_KEYS_MOD_CTRLR) == 0) { + other_state.insert(KM_CORE_MODIFIER_LCTRL); + } + + // shift + if (all_modifiers.count(LDML_KEYS_MOD_SHIFT) == 0) { + other_state.insert(KM_CORE_MODIFIER_SHIFT); + } + + // caps + if (all_modifiers.count(LDML_KEYS_MOD_CAPS) == 0) { + other_state.insert(KM_CORE_MODIFIER_CAPS); + } + + // none- it's possible there is no 'none' layer + if (all_modifiers.count(LDML_KEYS_MOD_NONE) == 0) { + other_state.insert(KM_CORE_MODIFIER_NONE); + } + + // We need ALL combinations of the other_state, except for 'all off'. + // The number of additions will be (2**(other_state.size())-1 + // Also, since the LDML_KEYS_MOD_OTHER modifier is excluded, we + // will need to subtract 1 when calculating new_list_size + const std::size_t other_expanded_count = (1 << other_state.size()) - 1; + + std::vector other_expanded_mods(other_expanded_count); + + // populate the expanded list. + // we start at 1 because 0 is "all bits off" (00000b) + for (std::size_t expansion = 1; expansion <= other_expanded_count; expansion++) { + uint32_t &expanded_mod = other_expanded_mods.at(expansion-1) = KM_CORE_MODIFIER_NONE; + std::size_t bit_mask = 1; + for (const auto mod : other_state) { + // Check if this modifier is on in this iteration of the expansion + // bit_mask will be 2^0 … 2^(other_state().size()-1) + if (bit_mask & expansion) { + // do we include this entry? check if this bit is on + expanded_mod |= mod; + } + // shift the bitmask over + assert(expanded_mod <= KM_CORE_MODIFIER_MASK_CAPS); + bit_mask <<= 1; + } + } + + const std::size_t new_list_size = all_vkeys.size() // original size + + (other_key_count * (other_expanded_count - 1))// number of additional entries needed + + 1; // terminator + km_core_keyboard_key *list = new km_core_keyboard_key[new_list_size]; + std::size_t n = 0; + for (const auto &k : all_vkeys) { + const auto vkey = k.first; + const auto mod = k.second; + + if (mod == LDML_KEYS_MOD_OTHER) { + // expand to all of other_state + for (const auto expanded_mod : other_expanded_mods) { + list[n].key = vkey; + list[n++].modifier_flag = expanded_mod; + assert(n <= new_list_size); + } + } else { + assert(mod <= KM_CORE_MODIFIER_MASK_CAPS); // that no LDMLisms escape + list[n].key = vkey; + list[n++].modifier_flag = mod; + assert(n <= new_list_size); + } + } + // add the list terminator + list[n++] = KM_CORE_KEYBOARD_KEY_LIST_END; + assert(n == new_list_size); + return list; } static const uint16_t BOTH_ALT = LALTFLAG | RALTFLAG; diff --git a/core/src/ldml/ldml_vkeys.hpp b/core/src/ldml/ldml_vkeys.hpp index ed3368dc7e..916d805e2c 100644 --- a/core/src/ldml/ldml_vkeys.hpp +++ b/core/src/ldml/ldml_vkeys.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include "keyman_core.h" @@ -19,10 +20,17 @@ namespace km { namespace core { namespace ldml { +/** + * LDML keyboards have 32-bit modifier flags in order to support + * LDML_KEYS_MOD_OTHER (0x10000), unlike the Core APIs which have only 16 bit + * modifier flags. + */ +typedef uint32_t km_core_ldml_modifier_state; + /** * identifier for keybag lookup */ -typedef std::pair vkey_id; +typedef std::pair vkey_id; /** * LDML Class to manage all things key related: vkey remapping and vkey to string @@ -30,14 +38,15 @@ typedef std::pair vkey_id; class vkeys { private: std::map vkey_to_string; + std::set all_vkeys; public: vkeys(); /** - * add a vkey to the bag + * add a vkey to the bag. */ - void add(km_core_virtual_key vk, uint16_t modifier_state, std::u16string output); + void add(km_core_virtual_key vk, km_core_ldml_modifier_state ldml_modifier_state, std::u16string output); /** * Lookup a vkey, returns an empty string if not found @@ -46,6 +55,11 @@ public: std::u16string lookup(km_core_virtual_key vk, uint16_t modifier_state, bool &found) const; + /** + * For implementing ldml_processor::get_key_list() + */ + km_core_keyboard_key* get_key_list() const; + private: /** * Non-recursive internal lookup of a specific ID diff --git a/core/tests/unit/ldml/keyboards/k_004_tinyshift.xml b/core/tests/unit/ldml/keyboards/k_004_tinyshift.xml index 4346fa801b..2d152867a7 100644 --- a/core/tests/unit/ldml/keyboards/k_004_tinyshift.xml +++ b/core/tests/unit/ldml/keyboards/k_004_tinyshift.xml @@ -2,8 +2,9 @@ @@ -23,5 +24,11 @@ + + + + + + diff --git a/core/tests/unit/ldml/ldml.cpp b/core/tests/unit/ldml/ldml.cpp index 3c27046b23..cb4fd45e15 100644 --- a/core/tests/unit/ldml/ldml.cpp +++ b/core/tests/unit/ldml/ldml.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include "path.hpp" #include "state.hpp" @@ -266,6 +267,63 @@ verify_context(std::u16string &text_store, km_core_state *&test_state, std::vect delete[] buf; } + +/** + * @param actual the list from get_key_list() + * @param expected optional list with keys to check, can be empty - not exhaistive + * @returns true if passing + */ +bool +verify_key_list(std::set &actual, std::set &expected) { + bool equals = true; + // error if any bad modifier keys + for(const auto &akey : actual) { + if (akey.modifier_state > KM_CORE_MODIFIER_MASK_CAPS) { + equals = false; + std::u16string dump = convert(akey.dump()); // akey.dump() + std::wcout << console_color::fg(console_color::BRIGHT_RED) << "- FAIL - key_map had key with bad modifier " << akey.modifier_state << ": " << dump << console_color::reset() << std::endl; + } + } + // error if any expected keys missing (note expected may be empty) + for(const auto &ekey : expected) { + if (actual.count(ekey) == 0) { + equals = false; + std::u16string dump = convert(ekey.dump()); // akey.dump() + std::wcout << console_color::fg(console_color::BRIGHT_RED) << "- FAIL - key_map had missing key " << dump << console_color::reset() << std::endl; + } + } + if (equals) { + std::wcout << console_color::fg(console_color::GREEN) << " " << actual.size() << " vkeys OK, verified " << expected.size() << console_color::reset() << std::endl; + } + return equals; +} + +/** + * @param actual_list the list from get_key_list() + * @param keylist optional string with keys to check, can be empty + * @param test the LDML test source, for additional data + * @returns true if passing + */ +bool +verify_key_list(const km_core_keyboard_key *actual_list, const std::u16string &expected_list, const km::tests::LdmlTestSource &test) { + std::set actual, expected; + // convert actual list + while (actual_list != nullptr && !(actual_list->key == 0 && actual_list->modifier_flag == 0)) { + km::tests::key_event k(actual_list->key, (uint16_t)actual_list->modifier_flag); + actual.insert(k); + actual_list++; // advance pointer + } + // parse the expected list + std::string keylist = convert(expected_list); + while (!keylist.empty() && keylist[0] == '[') { + const km::tests::key_event k = km::tests::LdmlEmbeddedTestSource::parse_next_key(keylist); + if (!k.empty()) { + expected.emplace(k); + } + } + return verify_key_list(actual, expected); +} + int run_test(const km::core::path &source, const km::core::path &compiled, km::tests::LdmlTestSource& test_source) { km_core_keyboard * test_kb = nullptr; @@ -371,6 +429,17 @@ run_test(const km::core::path &source, const km::core::path &compiled, km::tests errorLine = __LINE__; } } break; + case km::tests::LDML_ACTION_CHECK_KEYLIST: { + std::cout << "- checking keylist" << std::endl; + // get keylist from kbd + const km_core_keyboard_key* actual_list = test_kb->get_key_list(); + if (!verify_key_list(actual_list, action.string, test_source)) { + errorLine = __LINE__; + } else { + std::cout << " .. passes." << std::endl; + } + delete [] actual_list; + } break; case km::tests::LDML_ACTION_FAIL: { // test requested failure std::wcout << console_color::fg(console_color::BRIGHT_RED) << "- FAIL: " << action.string << console_color::reset() @@ -421,7 +490,7 @@ int run_all_tests(const km::core::path &source, const km::core::path &compiled, std::vector failures; // track failures for summary - int embedded_result = embedded_test_source.load_source(source); + int embedded_result = embedded_test_source.load_source(source, compiled); if (!filter.empty()) { // Always skip the embedded test if there's a filter. diff --git a/core/tests/unit/ldml/ldml_test_source.cpp b/core/tests/unit/ldml/ldml_test_source.cpp index 2db7efe6e3..bc01151056 100644 --- a/core/tests/unit/ldml/ldml_test_source.cpp +++ b/core/tests/unit/ldml/ldml_test_source.cpp @@ -122,6 +122,50 @@ bool LdmlTestSource::get_expected_beep() const { return false; } +int LdmlTestSource::load_kmx_plus(const km::core::path &compiled) { + + // check and load the KMX (yes, once again) + if(!km::core::ldml_processor::is_kmxplus_file(compiled, rawdata)) { + std::cerr << "Reading KMX for test purposes failed: " << compiled << std::endl; + return __LINE__; + } + + auto comp_keyboard = (const km::core::kmx::COMP_KEYBOARD*)rawdata.data(); + // initialize the kmxplus object with our copy + kmxplus.reset(new km::core::kmx::kmx_plus(comp_keyboard, rawdata.size())); + + if (!kmxplus->is_valid()) { + std::cerr << "kmx_plus invalid" << std::endl; + return __LINE__; + } + + if (!kmxplus->key2Helper.valid()) { + std::cerr << "kmx_plus invalid" << std::endl; + return __LINE__; + } + + return 0; // success +} + +bool LdmlTestSource::get_vkey_table(std::set &fillin) const { + if (!kmxplus || !kmxplus->is_valid()) { + return false; // fail + } + + // just dump the kmap table + for (KMX_DWORD kmapIdx = 0; kmapIdx < kmxplus->key2->kmapCount; kmapIdx++) { + const km::core::kmx::COMP_KMXPLUS_KEYS_KMAP *kmap = kmxplus->key2Helper.getKmap(kmapIdx); + if (kmap == nullptr) { + return false; + } + if (kmap->vkey > 0xFF) { + continue; // synthetic key- skip + } + fillin.insert(key_event(kmap->vkey, kmap->mod)); + } + return true; +} + // String trim functions from https://stackoverflow.com/a/217605/1836776 // trim from start (in place) static inline void @@ -259,12 +303,13 @@ LdmlEmbeddedTestSource::is_token(const std::string token, std::string &line) { } int -LdmlEmbeddedTestSource::load_source( const km::core::path &path ) { +LdmlEmbeddedTestSource::load_source( const km::core::path &path, const km::core::path &compiled ) { const std::string s_keys = "@@keys: "; const std::string s_expected = "@@expected: "; const std::string s_context = "@@context: "; const std::string s_capsLock = "@@capsLock: "; const std::string s_expecterror = "@@expect-error: "; + const std::string s_keylist = "@@keylist: "; // Parse out the header statements in file.kmn that tell us (a) environment, (b) key sequence, (c) start context, (d) expected // result @@ -296,6 +341,10 @@ LdmlEmbeddedTestSource::load_source( const km::core::path &path ) { context = parse_source_string(line); } else if (is_token(s_capsLock, line)) { set_caps_lock_on(parse_source_string(line).compare(u"1") == 0); + } else if (is_token(s_keylist, line)) { + set_keylist(line); + } else if (line[0] == '@') { + std::cerr << path << " warning, unknown @-command " << line << std::endl; } } @@ -311,7 +360,13 @@ LdmlEmbeddedTestSource::load_source( const km::core::path &path ) { return __LINE__; } - return 0; + // and load KMX+ for keylist + if (!expected_error) { + // don't attempt to load KMX+ on expected error + return load_kmx_plus(compiled); + } else { + return 0; + } } km_core_status @@ -352,7 +407,7 @@ LdmlTestSource::char_to_event(char ch) { } uint16_t -LdmlTestSource::get_modifier(std::string const m) { +LdmlTestSource::get_modifier(std::string const &m) { for (int i = 0; km::core::kmx::s_modifier_names[i].name; i++) { if (m == km::core::kmx::s_modifier_names[i].name) { return km::core::kmx::s_modifier_names[i].modifier; @@ -361,6 +416,12 @@ LdmlTestSource::get_modifier(std::string const m) { return 0; } +std::string key_event::dump() const { + std::stringstream f; + f << "Key: {" << km::core::kmx::Debug_VirtualKey(vk) << ", " << km::core::kmx::Debug_ModifierName(modifier_state) << "}"; + return f.str(); +} + key_event LdmlEmbeddedTestSource::vkey_to_event(std::string const &vk_event) { // vkey format is MODIFIER MODIFIER K_NAME @@ -396,34 +457,37 @@ LdmlEmbeddedTestSource::vkey_to_event(std::string const &vk_event) { void LdmlEmbeddedTestSource::next_action(ldml_action &fillin) { - if (is_done || keys.empty()) { - // We were already done. return done. - fillin.type = LDML_ACTION_DONE; - return; + if (keys.empty()) { + // #3 we are almost done, let's run the key check + if (check_keylist) { + fillin.type = LDML_ACTION_CHECK_KEYLIST; + fillin.string = expected_keylist; // could be empty + check_keylist = false; + } else { + fillin.type = LDML_ACTION_DONE; + } } else if(keys[0].empty()) { + // #2. Then, when we finish a key set, we check the 'expected' at the end of it. // Got to the end of a key set. time to check fillin.type = LDML_ACTION_CHECK_EXPECTED; fillin.string = expected[0]; // copy expected expected.pop_front(); keys.pop_front(); - if (keys.empty()) { - is_done = true; // so we get DONE next time - } } else { + // #1 First, we process each key fillin.type = LDML_ACTION_KEY_EVENT; fillin.k = next_key(); } } - key_event LdmlEmbeddedTestSource::next_key() { // mutate this->keys - return next_key(keys[0]); + return parse_next_key(keys[0]); } key_event -LdmlEmbeddedTestSource::next_key(std::string &keys) { +LdmlEmbeddedTestSource::parse_next_key(std::string &keys) { // Parse the next element of the string, chop it off, and return it // mutates keys if (keys.length() == 0) @@ -448,10 +512,10 @@ LdmlEmbeddedTestSource::next_key(std::string &keys) { class LdmlJsonTestSource : public LdmlTestSource { public: - LdmlJsonTestSource(const std::string &path, km::core::kmx::kmx_plus *kmxplus); + LdmlJsonTestSource(const std::string &path); virtual ~LdmlJsonTestSource(); virtual const std::u16string &get_context(); - int load(const nlohmann::json &test); + int load(const nlohmann::json &test, const km::core::path &compiled); virtual void next_action(ldml_action &fillin); private: std::string path; @@ -462,14 +526,14 @@ private: * Which action are we on? */ std::size_t action_index = -1; - const km::core::kmx::kmx_plus *kmxplus; /** @return false if not found */ bool set_key_from_id(key_event& k, const std::u16string& id); bool loaded_context = false; + bool check_keys = true; }; -LdmlJsonTestSource::LdmlJsonTestSource(const std::string &path, km::core::kmx::kmx_plus *k) -:path(path), kmxplus(k) { +LdmlJsonTestSource::LdmlJsonTestSource(const std::string &path) +:path(path) { } @@ -514,6 +578,13 @@ bool LdmlJsonTestSource::set_key_from_id(key_event& k, const std::u16string& id) void LdmlJsonTestSource::next_action(ldml_action &fillin) { if ((action_index+1) >= data["/actions"_json_pointer].size()) { + // add check keylist + if (check_keys) { + fillin.type = LDML_ACTION_CHECK_KEYLIST; + fillin.string.clear(); + check_keys = false; + return; + } // at end, done fillin.type = LDML_ACTION_DONE; return; @@ -581,19 +652,21 @@ LdmlJsonTestSource::get_context() { return context; } -int LdmlJsonTestSource::load(const nlohmann::json &data) { +int LdmlJsonTestSource::load(const nlohmann::json &data, const km::core::path &compiled) { this->data = data; - // TODO-LDML: validate here? - return 0; + // TODO-LDML: validate JSON here? + + // load up the kmx_plus + return load_kmx_plus(compiled); } #if defined(HAVE_ICU4C) class LdmlJsonRepertoireTestSource : public LdmlTestSource { public: - LdmlJsonRepertoireTestSource(const std::string &path, km::core::kmx::kmx_plus *kmxplus); + LdmlJsonRepertoireTestSource(const std::string &path ); virtual ~LdmlJsonRepertoireTestSource(); virtual const std::u16string &get_context(); - int load(const nlohmann::json &test); + int load(const nlohmann::json &test, const km::core::path &compiled); virtual void next_action(ldml_action &fillin); private: std::string path; @@ -605,11 +678,10 @@ private: std::unique_ptr uset; std::unique_ptr iterator; bool need_check = false; // set this after each char - const km::core::kmx::kmx_plus *kmxplus; }; -LdmlJsonRepertoireTestSource::LdmlJsonRepertoireTestSource(const std::string &path, km::core::kmx::kmx_plus *k) -:path(path), kmxplus(k){ +LdmlJsonRepertoireTestSource::LdmlJsonRepertoireTestSource(const std::string &path) +:path(path) { } @@ -697,7 +769,7 @@ LdmlJsonRepertoireTestSource::get_context() { return context; // no context needed } -int LdmlJsonRepertoireTestSource::load(const nlohmann::json &data) { +int LdmlJsonRepertoireTestSource::load(const nlohmann::json &data, const km::core::path &compiled) { this->data = data; // TODO-LDML // Need an update to json.hpp to use contains() // if (data.contains("/type"_json_pointer)) { @@ -728,7 +800,7 @@ int LdmlJsonRepertoireTestSource::load(const nlohmann::json &data) { // } // #endif iterator = std::unique_ptr(new icu::UnicodeSetIterator(*uset)); - return 0; + return load_kmx_plus(compiled); } #endif // HAVE_ICU4C @@ -752,25 +824,6 @@ int LdmlJsonTestSourceFactory::load(const km::core::path &compiled, const km::co return __LINE__; // empty } - // check and load the KMX (yes, once again) - if(!km::core::ldml_processor::is_kmxplus_file(compiled, rawdata)) { - std::cerr << "Reading KMX for test purposes failed: " << compiled << std::endl; - return __LINE__; - } - - auto comp_keyboard = (const km::core::kmx::COMP_KEYBOARD*)rawdata.data(); - // initialize the kmxplus object with our copy - kmxplus.reset(new km::core::kmx::kmx_plus(comp_keyboard, rawdata.size())); - - if (!kmxplus->is_valid()) { - std::cerr << "kmx_plus invalid" << std::endl; - return __LINE__; - } - - if (!kmxplus->key2Helper.valid()) { - std::cerr << "kmx_plus invalid" << std::endl; - return __LINE__; - } auto conformsTo = data["/keyboardTest3/conformsTo"_json_pointer].get(); assert_or_return(std::string(LDML_CLDR_TEST_VERSION_LATEST) == conformsTo); @@ -793,8 +846,8 @@ int LdmlJsonTestSourceFactory::load(const km::core::path &compiled, const km::co test_path.append(info_name).append("/tests/").append(tests_name).append("/").append(test_name); // std::cout << "JSON: reading " << info_name << "/" << test_path << std::endl; - std::unique_ptr subtest(new LdmlJsonTestSource(test_path, kmxplus.get())); - assert_or_return(subtest->load(test) == 0); + std::unique_ptr subtest(new LdmlJsonTestSource(test_path)); + assert_or_return(subtest->load(test, compiled) == 0); test_map[test_path] = std::unique_ptr(subtest.release()); } } @@ -809,8 +862,8 @@ int LdmlJsonTestSourceFactory::load(const km::core::path &compiled, const km::co std::string test_path; test_path.append(info_name).append("/repertoire/").append(rep_name); - std::unique_ptr reptest(new LdmlJsonRepertoireTestSource(test_path, kmxplus.get())); - assert_or_return(reptest->load(rep) == 0); + std::unique_ptr reptest(new LdmlJsonRepertoireTestSource(test_path)); + assert_or_return(reptest->load(rep, compiled) == 0); test_map[test_path] = std::unique_ptr(reptest.release()); } diff --git a/core/tests/unit/ldml/ldml_test_source.hpp b/core/tests/unit/ldml/ldml_test_source.hpp index 4637af9bd4..c8e7061ed6 100644 --- a/core/tests/unit/ldml/ldml_test_source.hpp +++ b/core/tests/unit/ldml/ldml_test_source.hpp @@ -4,6 +4,7 @@ #include "path.hpp" #include +#include #include #include "kmx/kmx_plus.h" @@ -14,6 +15,34 @@ namespace tests { struct key_event { km_core_virtual_key vk; uint16_t modifier_state; + public: + key_event() : vk(0), modifier_state(0) { + } + key_event(km_core_virtual_key k, uint16_t m) :vk(k), modifier_state(m) { + } + std::string dump() const; + int compare(const key_event &other) const { + if (vk < other.vk) return -1; + if (vk > other.vk) return 1; + if (modifier_state < other.modifier_state) return -1; + if (modifier_state > other.modifier_state) return 1; + return 0; + } + + bool operator<(const key_event &other) const { + return compare(other) < 0; + } + bool operator>(const key_event &other) const { + return compare(other) > 0; + } + bool operator==(const key_event &other) const { + return compare(other) == 0; + } + /** true if unset (null) key */ + bool empty() const { + return vk == 0 && modifier_state == 0; + } + }; enum ldml_action_type { @@ -37,6 +66,10 @@ enum ldml_action_type { * expected text */ LDML_ACTION_CHECK_EXPECTED, + /** + * string - keylist to check + */ + LDML_ACTION_CHECK_KEYLIST, // TODO-LDML: gestures, etc? Depends on touch. /** @@ -77,10 +110,17 @@ public: virtual km_core_status get_expected_load_status(); virtual const std::u16string &get_context() = 0; virtual bool get_expected_beep() const; + /** + * fillin a list + * @param fillin key list to be filled in with all vkeys in the hardware map. + * @param modifier modifier to load key list for + * @returns false on load fail, true on OK + */ + bool get_vkey_table(std::set &fillin) const; // helper functions static key_event char_to_event(char ch); - static uint16_t get_modifier(std::string const m); + static uint16_t get_modifier(std::string const &m); static std::u16string parse_source_string(std::string const &s); static std::u16string parse_u8_source_string(std::string const &s); @@ -102,6 +142,12 @@ private: private: bool _caps_lock_on = false; +protected: + /** populate rawdata and kmxplus */ + int load_kmx_plus(const km::core::path &compiled); + // copy of the kbd data, for lookups + std::vector rawdata; + std::unique_ptr kmxplus; }; typedef std::map> JsonTestMap; @@ -120,9 +166,6 @@ class LdmlJsonTestSourceFactory { const JsonTestMap& get_tests() const; private: JsonTestMap test_map; - // copy of the kbd data, for lookups - std::vector rawdata; - std::unique_ptr kmxplus; }; @@ -134,7 +177,7 @@ public: /** * Load the test_source from comments in the .xml source */ - int load_source(const km::core::path &path); + int load_source(const km::core::path &path, const km::core::path &compiled); virtual km_core_status get_expected_load_status(); virtual const std::u16string &get_context(); @@ -143,10 +186,6 @@ public: virtual void next_action(ldml_action &fillin); private: - - bool is_token(const std::string token, std::string &line); - key_event vkey_to_event(std::string const &vk_event); - key_event next_key(std::string &keys); key_event next_key(); std::deque keys; @@ -155,6 +194,24 @@ private: bool expected_beep = false; bool expected_error = false; bool is_done = false; + /** did we check the keylist yet? */ + bool check_keylist = true; + /** set the expected keys in the keylist */ + void set_keylist(std::string const& s) { + expected_keylist = parse_source_string(s); + } + + std::u16string expected_keylist; + + /** returns false on fail and updates the message */ + bool handle_check_keylist(std::string &message) const; + +// utility + static key_event vkey_to_event(std::string const &vk_event); + static bool is_token(const std::string token, std::string &line); + +public: + static key_event parse_next_key(std::string &keys); }; } // namespace tests diff --git a/core/wasm.build.linux.in b/core/wasm.build.linux.in index d449dcf94a..78ce0b3030 100644 --- a/core/wasm.build.linux.in +++ b/core/wasm.build.linux.in @@ -1,4 +1,4 @@ [binaries] -c = ['$EMSCRIPTEN_BASE/emcc.py'] -cpp = ['$EMSCRIPTEN_BASE/em++.py'] -ar = ['$EMSCRIPTEN_BASE/emar.py'] \ No newline at end of file +c = ['$EMSCRIPTEN_BASE/emcc'] +cpp = ['$EMSCRIPTEN_BASE/em++'] +ar = ['$EMSCRIPTEN_BASE/emar'] \ No newline at end of file diff --git a/core/wasm.build.mac.in b/core/wasm.build.mac.in index d449dcf94a..78ce0b3030 100644 --- a/core/wasm.build.mac.in +++ b/core/wasm.build.mac.in @@ -1,4 +1,4 @@ [binaries] -c = ['$EMSCRIPTEN_BASE/emcc.py'] -cpp = ['$EMSCRIPTEN_BASE/em++.py'] -ar = ['$EMSCRIPTEN_BASE/emar.py'] \ No newline at end of file +c = ['$EMSCRIPTEN_BASE/emcc'] +cpp = ['$EMSCRIPTEN_BASE/em++'] +ar = ['$EMSCRIPTEN_BASE/emar'] \ No newline at end of file diff --git a/core/wasm.defs.build b/core/wasm.defs.build index 4a598a1e1c..f41af337a3 100644 --- a/core/wasm.defs.build +++ b/core/wasm.defs.build @@ -1,7 +1,7 @@ [binaries] -c = ['emcc.py'] -cpp = ['em++.py'] -ar = ['emar.py'] +c = ['emcc'] +cpp = ['em++'] +ar = ['emar'] exe_wrapper = 'node' [properties] diff --git a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts index ed704c6e8c..3605b81f8d 100644 --- a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts +++ b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts @@ -493,9 +493,6 @@ export class KeyboardInfoCompiler implements KeymanCompiler { keyboard_info.languages[language] = {}; } - const fontSource = [].concat(...kmpJsonData.keyboards.map(e => e.displayFont ? [e.displayFont] : []), ...kmpJsonData.keyboards.map(e => e.webDisplayFonts ?? [])); - const oskFontSource = [].concat(...kmpJsonData.keyboards.map(e => e.oskFont ? [e.oskFont] : []), ...kmpJsonData.keyboards.map(e => e.webOskFonts ?? [])); - for(const bcp47 of Object.keys(keyboard_info.languages)) { const language = keyboard_info.languages[bcp47]; @@ -520,6 +517,20 @@ export class KeyboardInfoCompiler implements KeymanCompiler { // do it right now. // + // The code below: + // 1. Only includes fonts associated with keyboards which support the current bcp47 (filter) + // 2. Joins the displayFont and webDisplayFonts data, and removes duplicates (...new Set()) + + const supportedKeyboards = kmpJsonData.keyboards.filter(k => k.languages.find(lang => lang.id == bcp47)); + const fontSource = [...new Set([].concat( + ...supportedKeyboards.map(e => e.displayFont ? [e.displayFont] : []), + ...supportedKeyboards.map(e => e.webDisplayFonts ?? []) + ))]; + const oskFontSource = [...new Set([].concat( + ...supportedKeyboards.map(e => e.oskFont ? [e.oskFont] : []), + ...supportedKeyboards.map(e => e.webOskFonts ?? []) + ))]; + if(fontSource.length) { language.font = await this.fontSourceToKeyboardInfoFont(kpsFilename, kmpJsonData, fontSource); if(language.font == null) { diff --git a/developer/src/kmc-ldml/package.json b/developer/src/kmc-ldml/package.json index ffb495ac7b..a2b56a24f3 100644 --- a/developer/src/kmc-ldml/package.json +++ b/developer/src/kmc-ldml/package.json @@ -35,12 +35,14 @@ "@keymanapp/developer-test-helpers": "*", "@keymanapp/resources-gosh": "*", "@types/chai": "^4.1.7", + "@types/common-tags": "^1.8.4", "@types/mocha": "^5.2.7", "@types/node": "^20.4.1", "@types/semver": "^7.3.12", "c8": "^7.12.0", "chai": "^4.3.4", "chalk": "^2.4.2", + "common-tags": "^1.8.2", "mocha": "^8.4.0", "ts-node": "^9.1.1", "typescript": "^4.9.5" diff --git a/developer/src/kmc-ldml/src/compiler/compiler.ts b/developer/src/kmc-ldml/src/compiler/compiler.ts index b972baf750..6e923f9855 100644 --- a/developer/src/kmc-ldml/src/compiler/compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/compiler.ts @@ -1,4 +1,4 @@ -import { LDMLKeyboardXMLSourceFileReader, LDMLKeyboard, KMXPlus, CompilerCallbacks, LDMLKeyboardTestDataXMLSourceFile, UnicodeSetParser, KeymanCompiler, KeymanCompilerResult, KeymanCompilerArtifacts, defaultCompilerOptions, KMXBuilder, KvkFileWriter, KeymanCompilerArtifactOptional } from '@keymanapp/common-types'; +import { KMX, LDMLKeyboardXMLSourceFileReader, LDMLKeyboard, KMXPlus, CompilerCallbacks, LDMLKeyboardTestDataXMLSourceFile, UnicodeSetParser, KeymanCompiler, KeymanCompilerResult, KeymanCompilerArtifacts, defaultCompilerOptions, KMXBuilder, KvkFileWriter, KeymanCompilerArtifactOptional } from '@keymanapp/common-types'; import { LdmlCompilerOptions } from './ldml-compiler-options.js'; import { CompilerMessages } from './messages.js'; import { BkspCompiler, TranCompiler } from './tran.js'; @@ -125,18 +125,34 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { return null; } + outputFilename = outputFilename ?? inputFilename.replace(/\.xml$/, '.kmx'); + // In order for the KMX file to be loaded by non-KMXPlus components, it is helpful // to duplicate some of the metadata KMXPlusMetadataCompiler.addKmxMetadata(kmx.kmxplus, kmx.keyboard, compilerOptions); // Use the builder to generate the binary output file - const builder = new KMXBuilder(kmx, compilerOptions.saveDebug); - const kmx_binary = builder.compile(); + const kmxBuilder = new KMXBuilder(kmx, compilerOptions.saveDebug); + const keyboardId = this.callbacks.path.basename(outputFilename, '.kmx'); + const vkCompiler = new LdmlKeyboardVisualKeyboardCompiler(this.callbacks); + const vkCompilerResult = vkCompiler.compile(kmx.kmxplus, keyboardId); + if(vkCompilerResult === null) { + return null; + } + const vkData = typeof vkCompilerResult == 'object' ? vkCompilerResult : null; - const vkcompiler = new LdmlKeyboardVisualKeyboardCompiler(this.callbacks); - const vk = vkcompiler.compile(source); - const writer = new KvkFileWriter(); - const kvk_binary = writer.write(vk); + if(vkData) { + kmx.keyboard.stores.push({ + dpName: '', + dpString: keyboardId + '.kvk', + dwSystemID: KMX.KMXFile.TSS_VISUALKEYBOARD + }); + } + + const kmxBinary = kmxBuilder.compile(); + + const kvkWriter = new KvkFileWriter(); + const kvkBinary = vkData ? kvkWriter.write(vkData) : null; // Note: we could have a step of generating source files here // KvksFileWriter()... @@ -149,12 +165,10 @@ export class LdmlKeyboardCompiler implements KeymanCompiler { //KMW17.0: const encoder = new TextEncoder(); //KMW17.0: const kmw_binary = encoder.encode(kmw_string); - outputFilename = outputFilename ?? inputFilename.replace(/\.xml$/, '.kmx'); - return { artifacts: { - kmx: { data: kmx_binary, filename: outputFilename }, - kvk: { data: kvk_binary, filename: outputFilename.replace(/\.kmx$/, '.kvk') }, + kmx: { data: kmxBinary, filename: outputFilename }, + kvk: kvkBinary ? { data: kvkBinary, filename: outputFilename.replace(/\.kmx$/, '.kvk') } : null, //KMW17.0: js: { data: kmw_binary, filename: outputFilename.replace(/\.kmx$/, '.js') }, } }; diff --git a/developer/src/kmc-ldml/src/compiler/empty-compiler.ts b/developer/src/kmc-ldml/src/compiler/empty-compiler.ts index 911e56d896..514024fedd 100644 --- a/developer/src/kmc-ldml/src/compiler/empty-compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/empty-compiler.ts @@ -5,7 +5,7 @@ import { VarsCompiler } from './vars.js'; import { CompilerMessages } from './messages.js'; /** - * Compiler for typrs that don't actually consume input XML + * Compiler for types that don't actually consume input XML */ export abstract class EmptyCompiler extends SectionCompiler { private _id: SectionIdent; diff --git a/developer/src/kmc-ldml/src/compiler/keymanweb-compiler.ts b/developer/src/kmc-ldml/src/compiler/keymanweb-compiler.ts deleted file mode 100644 index a35739247b..0000000000 --- a/developer/src/kmc-ldml/src/compiler/keymanweb-compiler.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { CompilerCallbacks, VisualKeyboard, LDMLKeyboard, TouchLayoutFileWriter, KeymanFileTypes } from "@keymanapp/common-types"; -import { LdmlCompilerOptions } from "./ldml-compiler-options.js"; -import { TouchLayoutCompiler } from "./touch-layout-compiler.js"; -import { LdmlKeyboardVisualKeyboardCompiler } from "./visual-keyboard-compiler.js"; - -const MINIMUM_KMW_VERSION = '16.0'; - -export class LdmlKeyboardKeymanWebCompiler { - private readonly options: LdmlCompilerOptions; - private readonly nl: string; - private readonly tab: string; - constructor(private callbacks: CompilerCallbacks, options?: LdmlCompilerOptions) { - this.options = { ...options }; - this.nl = this.options.saveDebug ? "\n" : ''; - this.tab = this.options.saveDebug ? " " : ''; - } - - public compileVisualKeyboard(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile) { - const nl = this.nl, tab = this.tab; - const vkc = new LdmlKeyboardVisualKeyboardCompiler(this.callbacks); - const vk: VisualKeyboard.VisualKeyboard = vkc.compile(source); - - let result = - `{F: ' 1em ${JSON.stringify(vk.header.unicodeFont.name)}', `+ - `K102: ${vk.header.flags & VisualKeyboard.VisualKeyboardHeaderFlags.kvkh102 ? 1 : 0}};${nl}` + // TODO-LDML: escape ' and " in font name correctly - `${tab}this.KV.KLS={${nl}` + - `${tab}${tab}TODO_LDML: ${vk.keys.length}${nl}` + - // TODO-LDML: fill in KLS - `${tab}}`; - - return result; - } - - public compileTouchLayout(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile) { - const tlcompiler = new TouchLayoutCompiler(); - const layout = tlcompiler.compileToJavascript(source); - const writer = new TouchLayoutFileWriter({formatted: this.options.saveDebug}); - return writer.compile(layout); - } - - private cleanName(name: string): string { - let result = this.callbacks.path.basename(name, KeymanFileTypes.Source.LdmlKeyboard); - if(!result) { - throw new Error(`Invalid file name ${name}`); - } - - result = result.replaceAll(/[^a-z0-9]/g, '_'); - if(result.match(/^[0-9]/)) { - // Can't have a digit as initial - result = '_' + result; - } - return result; - } - - public compile(name: string, source: LDMLKeyboard.LDMLKeyboardXMLSourceFile): string { - const nl = this.nl, tab = this.tab; - - const sName = 'Keyboard_'+this.cleanName(name); - const displayUnderlying = true; // TODO-LDML - const modifierBitmask = 0; // TODO-LDML: define the modifiers used by this keyboard - const vkDictionary = ''; // TODO-LDML: vk dictionary for touch keys - const hasSupplementaryPlaneChars = false; // TODO-LDML - const isRTL = false; // TODO-LDML - - let result = - `if(typeof keyman === 'undefined') {${nl}` + - `${tab}console.error('Keyboard requires KeymanWeb ${MINIMUM_KMW_VERSION} or later');${nl}` + - `} else {${nl}` + - `${tab}KeymanWeb.KR(new ${sName}());${nl}` + - `}${nl}` + - `function ${sName}() {${nl}` + - // `${tab}${this.setupDebug()}${nl}` + ? we may use this for modifierBitmask in future - // `${tab}this._v=(typeof keyman!="undefined"&&typeof keyman.version=="string")?parseInt(keyman.version,10):9;${nl}` + ? we probably don't need this, it's for back-compat - `${tab}this.KI="${sName}";${nl}` + - `${tab}this.KN=${JSON.stringify(source.keyboard3.info.name)};${nl}` + - `${tab}this.KMINVER=${JSON.stringify(MINIMUM_KMW_VERSION)};${nl}` + - `${tab}this.KV=${this.compileVisualKeyboard(source)};${nl}` + - `${tab}this.KDU=${displayUnderlying ? '1' : '0'};${nl}` + - `${tab}this.KH="";${nl}` + // TODO-LDML: help text not supported - `${tab}this.KM=0;${nl}` + // TODO-LDML: mnemonic layout not supported for LDML keyboards - `${tab}this.KBVER=${JSON.stringify(source.keyboard3.version?.number || '0.0')};${nl}` + - `${tab}this.KMBM=${modifierBitmask};${nl}`; - - if(isRTL) { - result += `${tab}this.KRTL=1;${nl}`; - } - - if(hasSupplementaryPlaneChars) { - result += `${tab}this.KS=1;${nl}`; - } - - if(vkDictionary != '') { - result += `${tab}this.KVKD=${JSON.stringify(vkDictionary)};${nl}`; - } - - let layoutFile = this.compileTouchLayout(source); - if(layoutFile != '') { - result += `${tab}this.KVKL=${layoutFile};${nl}`; - } - // TODO-LDML: KCSS not supported - - // TODO-LDML: embed binary keyboard for loading into Core - - // A LDML keyboard has a no-op for its gs() (begin Unicode) function, - // because the functionality is embedded in Keyman Core - result += `${tab}this.gs=function(t,e){${nl}`+ - `${tab}${tab}return 0;${nl}`+ // TODO-LDML: we will start by embedding call into Keyman Core here - `${tab}};${nl}`; - - result += `}${nl}`; - return result; - } -} diff --git a/developer/src/kmc-ldml/src/compiler/layr.ts b/developer/src/kmc-ldml/src/compiler/layr.ts index 8056144b25..bc7ee38e1e 100644 --- a/developer/src/kmc-ldml/src/compiler/layr.ts +++ b/developer/src/kmc-ldml/src/compiler/layr.ts @@ -63,7 +63,7 @@ export class LayrCompiler extends SectionCompiler { for (const layer of layers.layer) { const rows = layer.row.map((row) => { const erow: LayrRow = { - keys: row.keys.split(' ').map((id) => sections.strs.allocString(id)), + keys: row.keys.trim().split(/[ \t]+/).map((id) => sections.strs.allocString(id)), }; return erow; }); diff --git a/developer/src/kmc-ldml/src/compiler/messages.ts b/developer/src/kmc-ldml/src/compiler/messages.ts index 7ebf6bddc7..a87c4d7927 100644 --- a/developer/src/kmc-ldml/src/compiler/messages.ts +++ b/developer/src/kmc-ldml/src/compiler/messages.ts @@ -174,7 +174,13 @@ export class CompilerMessages { static Error_UnparseableReorderSet = (o: { from: string, set: string }) => m(this.ERROR_UnparseableReorderSet, `Illegal UnicodeSet "${def(o.set)}" in reorder "${def(o.from)}`); - // Available: 0x029 + static ERROR_InvalidVariableIdentifer = SevError | 0x0029; + static Error_InvalidVariableIdentifer = (o: { id: string }) => m( + this.ERROR_InvalidVariableIdentifer, + `Invalid variable identifier "\\u${def(o.id)}". Identifiers must be between 1 and 32 characters, and can use A-Z, a-z, 0-9, and _.`, + ); + + // Available: 0x02A-0x2F static ERROR_InvalidQuadEscape = SevError | 0x0030; static Error_InvalidQuadEscape = (o: { cp: number }) => @@ -189,9 +195,12 @@ export class CompilerMessages { m(this.ERROR_UnparseableTransformFrom, `Invalid transform from="${def(o.from)}": "${def(o.message)}"`); static ERROR_IllegalTransformDollarsign = SevErrorTransform | 0x01; - static Error_IllegalTransformDollarsign = (o: { from: string }) => - m(this.ERROR_IllegalTransformDollarsign, `Invalid transform from="${def(o.from)}": Unescaped dollar-sign ($) is not valid transform syntax.`, - '**Hint**: Use `\\$` to match a literal dollar-sign.'); + static Error_IllegalTransformDollarsign = (o: { from: string }) => m( + this.ERROR_IllegalTransformDollarsign, + `Invalid transform from="${def(o.from)}": Unescaped dollar-sign ($) is not valid transform syntax.`, + `**Hint**: Use \`\\$\` to match a literal dollar-sign. If this precedes a variable name, `+ + `the variable name may not be valid (A-Z, a-z, 0-9, _, 32 character maximum).` + ); static ERROR_TransformFromMatchesNothing = SevErrorTransform | 0x02; static Error_TransformFromMatchesNothing = (o: { from: string }) => diff --git a/developer/src/kmc-ldml/src/compiler/touch-layout-compiler.ts b/developer/src/kmc-ldml/src/compiler/touch-layout-compiler.ts deleted file mode 100644 index 5c5c7eb194..0000000000 --- a/developer/src/kmc-ldml/src/compiler/touch-layout-compiler.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { TouchLayout, LDMLKeyboard } from "@keymanapp/common-types"; - -export class TouchLayoutCompiler { - public compileToJavascript(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile): TouchLayout.TouchLayoutFile { - let result: TouchLayout.TouchLayoutFile = {}; - - // start with desktop to mimic vk emit - result.desktop = { - defaultHint: "none", // TODO-LDML this should be optional - layer: [] - }; - - for(let layers of source.keyboard3.layers) { - for(let layer of layers.layer) { - const resultLayer = this.compileHardwareLayer(source, result, layer); - result.desktop.layer.push(resultLayer); - } - } - return result; - } - - private compileHardwareLayer( - source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, - file: TouchLayout.TouchLayoutFile, - layer: LDMLKeyboard.LKLayer - ) { - // TODO-LDML: consider consolidation with keys.ts? - - let fileLayer: TouchLayout.TouchLayoutLayer = { - id: this.translateLayerIdToTouchLayoutLayerId(layer.id, layer.modifiers), - row: [] - }; - - let y = -1; - - for(let row of layer.row) { - y++; - - let fileRow: TouchLayout.TouchLayoutRow = {id: y, key: []}; - fileLayer.row.push(fileRow); - - const keys = row.keys.split(' '); - for(let key of keys) { - const keydef = source.keyboard3.keys?.key?.find(x => x.id == key); - if(keydef) { - const fileKey: TouchLayout.TouchLayoutKey = { - id: this.translateKeyIdentifierToTouch(keydef.id) as TouchLayout.TouchLayoutKeyId, - text: keydef.output || '', - // TODO-LDML: additional properties - }; - fileRow.key.push(fileKey); - } else { - // TODO-LDML: consider logging missing keys - } - } - } - - return fileLayer; - } - - private translateLayerIdToTouchLayoutLayerId(id: string, modifier: string): string { - // Touch layout layers have a set of reserved names that correspond to - // hardware modifiers. We want to map these identifiers first before falling - // back to the layer ids - - // The set of recognized layer identifiers is: - // - // touch | LDML - // ---------------+------------- - // default | none - // shift | shift - // caps | caps - // rightalt | altR - // rightalt-shift | altR shift - // - const map = { - none: 'default', - shift: 'shift', - caps: 'caps', - altR: 'rightalt', - "altR shift": 'rightalt-shift' - }; - - // canonicalize modifier string, alphabetical - // TODO-LDML: need to support multiple here - if (modifier && modifier.indexOf(',') !== -1) { - throw Error(`Internal error: TODO-LDML: multiple modifiers ${modifier} not yet supported.`); - } - modifier = (modifier||'').split(/\b/).sort().join(' ').trim(); - - if(Object.hasOwn(map, modifier)) { - return (map as any)[modifier]; - } - - // TODO-LDML: Other layer names will be used unmodified, is this sufficient? - return id; - } - - private translateKeyIdentifierToTouch(id: string): string { - // Note: keys identifiers in kmx were traditionally case-insensitive, but we - // are going to use them as case-insensitive for LDML keyboards. The set of - // standard key identifiers a-z, A-Z, 0-9 will be used, where possible, and - // all other keys will be mapped to `T_key`. - - if(id.match(/^[0-9a-zA-Z]$/)) { - return 'K_'+id; - } - - // Not a standard key - return 'T_'+id; - } -} diff --git a/developer/src/kmc-ldml/src/compiler/vars.ts b/developer/src/kmc-ldml/src/compiler/vars.ts index 34fa99831b..669aefe62f 100644 --- a/developer/src/kmc-ldml/src/compiler/vars.ts +++ b/developer/src/kmc-ldml/src/compiler/vars.ts @@ -40,6 +40,14 @@ export class VarsCompiler extends SectionCompiler { return valid; } + private validateIdentifier(id: string) { + if(!id.match(VariableParser.ID)) { // From DTD + this.callbacks.reportMessage(CompilerMessages.Error_InvalidVariableIdentifer({id})); + return false; + } + return true; + } + private validateVars(st: Substitutions): boolean { let valid = true; const variables = this.keyboard3?.variables; @@ -67,13 +75,28 @@ export class VarsCompiler extends SectionCompiler { if (variables) { // Strings for (const { id, value } of variables.string) { + if(!this.validateIdentifier(id)) { + valid = false; + continue; + } addId(id); - allStrings.add(id); const stringrefs = VariableParser.allStringReferences(value); + for(const ref of stringrefs) { + if(!allStrings.has(ref)) { + valid = false; + this.callbacks.reportMessage(CompilerMessages.Error_MissingStringVariable({id: ref})); + allStrings.add(ref); // avoids multiple reports of same missing variable + } + } st.string.add(SubstitutionUse.variable, stringrefs); + allStrings.add(id); } // Sets for (const { id, value } of variables.set) { + if(!this.validateIdentifier(id)) { + valid = false; + continue; + } addId(id); allSets.add(id); // check for illegal references, here. @@ -96,6 +119,10 @@ export class VarsCompiler extends SectionCompiler { } // UnicodeSets for (const { id, value } of variables.uset) { + if(!this.validateIdentifier(id)) { + valid = false; + continue; + } addId(id); allUnicodeSets.add(id); const stringrefs = VariableParser.allStringReferences(value); diff --git a/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts b/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts index bbb2d83abf..cf027043ba 100644 --- a/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts @@ -1,84 +1,164 @@ -import { VisualKeyboard, LDMLKeyboard, CompilerCallbacks } from "@keymanapp/common-types"; -import { KeysCompiler } from "./keys.js"; +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * Export LDML data (https://www.unicode.org/reports/tr35/tr35-keyboards.html) + * to .kvk format. This is an interim solution until Keyman Core supports + * interrogation of the KMX+ data for OSK. + */ +import { ModifierKeyConstants, KMXPlus } from "@keymanapp/common-types"; +import { VisualKeyboard, CompilerCallbacks } from "@keymanapp/common-types"; import { CompilerMessages } from "./messages.js"; +// This is a partial polyfill for findLast, so not polluting Array.prototype +// +// TODO: remove and replace with Array.prototype.findLast when it is +// well-supported +function findLast(arr: any, callback: any) { + if (!arr) { + return undefined; + } + const len = arr.length >>> 0; + for (let i = len - 1; i >= 0; i--) { + if (callback(arr[i], i, arr)) { + return arr[i]; + } + } + return undefined; +} + + +const LDML_MODIFIER_TO_KVK_MODIFIER = new Map(); +LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.LCTRLFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_LCTRL); +LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.RCTRLFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_RCTRL); +LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.LALTFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_LALT); +LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.RALTFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_RALT); +LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.K_SHIFTFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_SHIFT); +LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.K_CTRLFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_CTRL); +LDML_MODIFIER_TO_KVK_MODIFIER.set(ModifierKeyConstants.K_ALTFLAG, VisualKeyboard.VisualKeyboardShiftState.KVKS_ALT); + export class LdmlKeyboardVisualKeyboardCompiler { public constructor(private callbacks: CompilerCallbacks) { } - public compile(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile): VisualKeyboard.VisualKeyboard { + /** + * Generate a visual keyboard + * @param source Compiled KMX+ data; note that this is modified to add + * &VISUALKEYBOARD system store on success + * @param keyboardId Basename of keyboard, without file extension + * @returns Visual keyboard data on success, null on failure, or + * false if no VK was generated for this keyboard + */ + public compile(source: KMXPlus.KMXPlusData, keyboardId: string): VisualKeyboard.VisualKeyboard | boolean | null { let result = new VisualKeyboard.VisualKeyboard(); /* TODO-LDML: consider VisualKeyboardHeaderFlags.kvkhUseUnderlying kvkhDisplayUnderlying kvkhAltGr kvkh102 */ result.header.flags = 0; result.header.version = 0x0600; - - /* TODO-LDML: consider associatedKeyboard: this _must_ be set to id (aka basename sans ext) of keyboard .kmx file */ - result.header.associatedKeyboard = ''; + result.header.associatedKeyboard = keyboardId; result.header.ansiFont = {...VisualKeyboard.DEFAULT_KVK_FONT}; result.header.unicodeFont = {...VisualKeyboard.DEFAULT_KVK_FONT}; - for(let layers of source.keyboard3.layers) { - const { formId } = layers; - for(let layer of layers.layer) { - this.compileHardwareLayer(source, result, layer, formId); + let hasVisualKeyboard = false; + + for(let layersList of source.layr.lists) { + const formId = layersList.hardware.value; + if(formId == 'touch') { + continue; + } + + for(let layer of layersList.layers) { + const res = this.compileHardwareLayer(source, result, layer, formId); + if(res === false) { + // failed to compile the layer + return null; + } + if(res === null) { + // not a supported layer type, but not an error + continue; + } + hasVisualKeyboard = true; + } + } + + if(!hasVisualKeyboard) { + return false; + } + + return result; + } + + private compileHardwareLayer( + source: KMXPlus.KMXPlusData, + vk: VisualKeyboard.VisualKeyboard, + layer: KMXPlus.LayrEntry, + hardware: string, + ) { + const layerId = layer.id.value; + + hardware = 'us'; // TODO-LDML: US Only. We need to clean this up for other hardware forms + + const shift = this.translateLayerModifiersToVisualKeyboardShift(layer.mod); + if(shift === null) { + // Caps (num, scroll) is not a supported shift state in .kvk + return null; + } + + let result = true; + let y = -1; + for(let row of layer.rows) { + y++; + let x = -1; + for(let key of row.keys) { + x++; + + const keydef: KMXPlus.KeysKeys = findLast(source.keys?.keys, (kd: KMXPlus.KeysKeys) => kd.id.value == key.value); + const kmap = source.keys.kmap.find(k => k.key == keydef.id.value && k.mod == layer.mod); + const text = this.getDisplayFromKey(keydef, source) ?? null; + + if (!keydef || !kmap || text === null) { + this.callbacks.reportMessage( + CompilerMessages.Error_KeyNotFoundInKeyBag({ keyId: key.value, layer: layerId, row: y, col: x, form: hardware }) + ); + result = false; + } else { + vk.keys.push({ + flags: VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode, + shift, + text, + vkey: kmap.vkey + }); + } } } return result; } - private compileHardwareLayer( - source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, - vk: VisualKeyboard.VisualKeyboard, - layer: LDMLKeyboard.LKLayer, - hardware: string, - ) { - const layerId = layer.id; - if (hardware === 'touch') { - hardware = 'us'; // TODO-LDML: US Only. Do something different here? + private getDisplayFromKey(keydef: KMXPlus.KeysKeys, source: KMXPlus.KMXPlusData) { + const display = source.disp?.disps?.find(d => d.id.value == keydef.id.value || d.to.value == keydef.to.value); + return display?.display.value ?? keydef.to.value; + } + + private translateLayerModifiersToVisualKeyboardShift(modifiers: number): VisualKeyboard.VisualKeyboardShiftState { + + if(modifiers == 0) { + return VisualKeyboard.VisualKeyboardShiftState.KVKS_NORMAL; } - const keymap = KeysCompiler.getKeymapFromForms(source.keyboard3?.forms?.form, hardware); - if (!keymap) { - this.callbacks.reportMessage( - CompilerMessages.Error_InvalidHardware({ formId: hardware }) - ); - return; + + if(modifiers & + (ModifierKeyConstants.CAPITALFLAG | ModifierKeyConstants.NUMLOCKFLAG | ModifierKeyConstants.SCROLLFLAG) + ) { + // Caps/Num/Scroll are not supported in .kvk, in combination or alone + return null; } - const shift = this.translateLayerIdToVisualKeyboardShift(layer.id); - let y = -1; - for(let row of layer.row) { - y++; + let shift: VisualKeyboard.VisualKeyboardShiftState = 0; - const keys = row.keys.split(' '); - let x = -1; - for(let key of keys) { - const keyId = key; - x++; - - let keydef = source.keyboard3.keys?.key?.find(x => x.id == key); - - if (!keydef) { - this.callbacks.reportMessage( - CompilerMessages.Error_KeyNotFoundInKeyBag({ keyId, layer: layerId, row: y, col: x, form: hardware }) - ); - } else { - vk.keys.push({ - flags: VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode, - shift: shift, - text: keydef.output, // TODO-LDML: displays - vkey: keymap[y][x], - }); - } + for(const mod of LDML_MODIFIER_TO_KVK_MODIFIER.keys()) { + if(modifiers & mod) { + shift |= LDML_MODIFIER_TO_KVK_MODIFIER.get(mod); } } - } - private translateLayerIdToVisualKeyboardShift(id: string) { - if(id == 'base') { - return 0; - } - // TODO-LDML: other modifiers - return 0; + return shift; } } diff --git a/developer/src/kmc-ldml/test/fixtures/basic-kvk.txt b/developer/src/kmc-ldml/test/fixtures/basic-kvk.txt index 68666feacf..da8f5e6011 100644 --- a/developer/src/kmc-ldml/test/fixtures/basic-kvk.txt +++ b/developer/src/kmc-ldml/test/fixtures/basic-kvk.txt @@ -1,4 +1,6 @@ # +# Keyman is copyright (C) SIL International. MIT License. +# # basic-kvk.txt describes the expected output of running kmc against basic.xml to generate # a .kvk file. It is used in the end-to-end test test-visual-keyboard-compiler-e2e.ts. # @@ -10,8 +12,8 @@ block(kvkheader) 00 06 00 00 # version 0x0600 00 # flags block(associated_keyboard) - 01 00 # string len in UTF-16 code units incl zterm - 00 00 # '\0' + 06 00 # string len in UTF-16 code units incl zterm + 62 00 61 00 73 00 69 00 63 00 00 00 # 'basic\0' block(ansi_font) 06 00 # string len in UTF-16 code units incl zterm diff --git a/developer/src/kmc-ldml/test/fixtures/basic-no-debug.js b/developer/src/kmc-ldml/test/fixtures/basic-no-debug.js deleted file mode 100644 index 84168414cf..0000000000 --- a/developer/src/kmc-ldml/test/fixtures/basic-no-debug.js +++ /dev/null @@ -1 +0,0 @@ -if(typeof keyman === 'undefined') {console.error('Keyboard requires KeymanWeb 16.0 or later');} else {KeymanWeb.KR(new Keyboard_basic());}function Keyboard_basic() {this.KI="Keyboard_basic";this.KN="TestKbd";this.KMINVER="16.0";this.KV={F: ' 1em "Arial"', K102: 0};this.KV.KLS={TODO_LDML: 2};this.KDU=1;this.KH="";this.KM=0;this.KBVER="1.0.0";this.KMBM=0;this.KVKL={"desktop":{"defaultHint":"none","layer":[{"id":"base","row":[{"id":"0","key":[{"id":"T_hmaqtugha","text":"ħ"},{"id":"T_that","text":"ថា"}]}]}],"displayUnderlying":false}};this.gs=function(t,e){return 0;};} diff --git a/developer/src/kmc-ldml/test/fixtures/basic.js b/developer/src/kmc-ldml/test/fixtures/basic.js deleted file mode 100644 index c74798cfac..0000000000 --- a/developer/src/kmc-ldml/test/fixtures/basic.js +++ /dev/null @@ -1,48 +0,0 @@ -if(typeof keyman === 'undefined') { - console.error('Keyboard requires KeymanWeb 16.0 or later'); -} else { - KeymanWeb.KR(new Keyboard_basic()); -} -function Keyboard_basic() { - this.KI="Keyboard_basic"; - this.KN="TestKbd"; - this.KMINVER="16.0"; - this.KV={F: ' 1em "Arial"', K102: 0}; - this.KV.KLS={ - TODO_LDML: 2 - }; - this.KDU=1; - this.KH=""; - this.KM=0; - this.KBVER="1.0.0"; - this.KMBM=0; - this.KVKL={ - "desktop": { - "defaultHint": "none", - "layer": [ - { - "id": "base", - "row": [ - { - "id": "0", - "key": [ - { - "id": "T_hmaqtugha", - "text": "ħ" - }, - { - "id": "T_that", - "text": "ថា" - } - ] - } - ] - } - ], - "displayUnderlying": false - } -}; - this.gs=function(t,e){ - return 0; - }; -} diff --git a/developer/src/kmc-ldml/test/fixtures/basic.txt b/developer/src/kmc-ldml/test/fixtures/basic.txt index b471228a52..6302a5eb27 100644 --- a/developer/src/kmc-ldml/test/fixtures/basic.txt +++ b/developer/src/kmc-ldml/test/fixtures/basic.txt @@ -1,4 +1,6 @@ # +# Keyman is copyright (C) SIL International. MIT License. +# # basic.txt describes the expected output of running kmc against basic.xml. It is used in # the end-to-end test test-compiler-e2e.ts. # @@ -402,7 +404,7 @@ block(layr) # struct COMP_KMXPLUS_LAYR { 01 00 00 00 # count 7B 00 00 00 # KMX_DWORD minDeviceWidth; // 123 # layers 0 - index(strNull,strBase,2) # KMXPLUS_STR id; + 00 00 00 00 # KMXPLUS_STR id; 00 00 00 00 # KMX_DWORD mod 00 00 00 00 # KMX_DWORD row index 01 00 00 00 # KMX_DWORD count @@ -502,7 +504,6 @@ block(strs) # struct COMP_KMXPLUS_STRS { diff(strs,strSet) sizeof(strSet,2) diff(strs,strSet2) sizeof(strSet2,2) diff(strs,strTranTo) sizeof(strTranTo,2) - diff(strs,strBase) sizeof(strBase,2) diff(strs,strElemBkspFrom2) sizeof(strElemBkspFrom2,2) diff(strs,strGapReserved) sizeof(strGapReserved,2) diff(strs,strHmaqtugha) sizeof(strHmaqtugha,2) @@ -538,7 +539,6 @@ block(strs) # struct COMP_KMXPLUS_STRS { #str #0A block(strSet2) 61 00 62 00 63 00 block(x) 00 00 # 'abc' block(strTranTo) 61 00 02 03 block(x) 00 00 # 'â' (U+0061 U+0302) - block(strBase) 62 00 61 00 73 00 65 00 block(x) 00 00 # 'base' block(strElemBkspFrom2) 65 00 block(x) 00 00 # 'e' block(strGapReserved) 67 00 61 00 70 00 20 00 28 00 72 00 65 00 73 00 65 00 72 00 76 00 65 00 64 00 29 00 block(x) 00 00 # 'gap (reserved)' block(strHmaqtugha) 68 00 6d 00 61 00 71 00 74 00 75 00 67 00 68 00 61 00 block(x) 00 00 # 'hmaqtugha' diff --git a/developer/src/kmc-ldml/test/fixtures/basic.xml b/developer/src/kmc-ldml/test/fixtures/basic.xml index c8565271ed..168ee0ef9a 100644 --- a/developer/src/kmc-ldml/test/fixtures/basic.xml +++ b/developer/src/kmc-ldml/test/fixtures/basic.xml @@ -1,4 +1,5 @@ + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/layr/row-keys-whitespace.xml b/developer/src/kmc-ldml/test/fixtures/sections/layr/row-keys-whitespace.xml new file mode 100644 index 0000000000..f8d1a629d5 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/layr/row-keys-whitespace.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-7.xml b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-7.xml new file mode 100644 index 0000000000..25e7c46a1d --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-7.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-invalid-identifiers.xml b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-invalid-identifiers.xml new file mode 100644 index 0000000000..880864fd4e --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-invalid-identifiers.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/helpers/index.ts b/developer/src/kmc-ldml/test/helpers/index.ts index 2e9e16ff6a..0fd659fd1b 100644 --- a/developer/src/kmc-ldml/test/helpers/index.ts +++ b/developer/src/kmc-ldml/test/helpers/index.ts @@ -1,16 +1,17 @@ -/** +/* + * Keyman is copyright (C) SIL International. MIT License. + * * Helpers and utilities for the Mocha tests. */ import 'mocha'; import * as path from 'path'; import { fileURLToPath } from 'url'; import { SectionCompiler, SectionCompilerNew } from '../../src/compiler/section-compiler.js'; -import { util, KMXPlus, LDMLKeyboardXMLSourceFileReader, VisualKeyboard, CompilerEvent, LDMLKeyboardTestDataXMLSourceFile, compilerEventFormat, LDMLKeyboard, UnicodeSetParser, CompilerCallbacks } from '@keymanapp/common-types'; +import { util, KMXPlus, LDMLKeyboardXMLSourceFileReader, CompilerEvent, LDMLKeyboardTestDataXMLSourceFile, compilerEventFormat, LDMLKeyboard, UnicodeSetParser, CompilerCallbacks } from '@keymanapp/common-types'; import { LdmlKeyboardCompiler } from '../../src/main.js'; // make sure main.js compiles import { assert } from 'chai'; import { KMXPlusMetadataCompiler } from '../../src/compiler/metadata-compiler.js'; import { LdmlCompilerOptions } from '../../src/compiler/ldml-compiler-options.js'; -import { LdmlKeyboardVisualKeyboardCompiler } from '../../src/compiler/visual-keyboard-compiler.js'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import KMXPlusFile = KMXPlus.KMXPlusFile; @@ -20,7 +21,6 @@ import Section = KMXPlus.Section; import { ElemCompiler, ListCompiler, StrsCompiler } from '../../src/compiler/empty-compiler.js'; import { KmnCompiler } from '@keymanapp/kmc-kmn'; import { VarsCompiler } from '../../src/compiler/vars.js'; -// import Vars = KMXPlus.Vars; /** * Builds a path to the fixture with the given path components. @@ -47,7 +47,7 @@ beforeEach(function() { afterEach(function() { if (this.currentTest.state !== 'passed') { - compilerTestCallbacks.messages.forEach(message => console.log(message.message)); + compilerTestCallbacks.printMessages(); } }); @@ -160,28 +160,7 @@ export async function compileKeyboard(inputFilename: string, options: LdmlCompil return kmx; } -export async function compileVisualKeyboard(inputFilename: string, options: LdmlCompilerOptions): Promise { - const k = new LdmlKeyboardCompiler(); - assert.isTrue(await k.init(compilerTestCallbacks, options)); - const source = k.load(inputFilename); - checkMessages(); - assert.isNotNull(source, 'k.load should not have returned null'); - - const valid = await k.validate(source); - checkMessages(); - assert.isTrue(valid, 'k.validate should not have failed'); - - const vk = (new LdmlKeyboardVisualKeyboardCompiler(compilerTestCallbacks)).compile(source); - checkMessages(); - assert.isNotNull(vk, 'LdmlKeyboardVisualKeyboardCompiler.compile should not have returned null'); - - return vk; -} - export function checkMessages() { - if(compilerTestCallbacks.messages.length > 0) { - console.log(compilerTestCallbacks.messages); - } assert.isEmpty(compilerTestCallbacks.messages, compilerEventFormat(compilerTestCallbacks.messages)); } diff --git a/developer/src/kmc-ldml/test/test-keymanweb-compiler.ts b/developer/src/kmc-ldml/test/test-keymanweb-compiler.ts deleted file mode 100644 index 9a14b38c33..0000000000 --- a/developer/src/kmc-ldml/test/test-keymanweb-compiler.ts +++ /dev/null @@ -1,50 +0,0 @@ -import 'mocha'; -import { assert } from 'chai'; -import { checkMessages, compilerTestCallbacks, compilerTestOptions, makePathToFixture } from './helpers/index.js'; -import { LdmlKeyboardKeymanWebCompiler } from '../src/compiler/keymanweb-compiler.js'; -import { LdmlKeyboardCompiler } from '../src/compiler/compiler.js'; -import * as fs from 'fs'; - -describe('LdmlKeyboardKeymanWebCompiler', function() { - - it('should build a .js file', async function() { - // Let's build basic.xml - // It should generate content identical to basic.js - const inputFilename = makePathToFixture('basic.xml'); - const outputFilename = makePathToFixture('basic.js'); - const outputFilenameNoDebug = makePathToFixture('basic-no-debug.js'); - - // Load input data; we'll use the LDML keyboard compiler loader to save us - // effort here - const k = new LdmlKeyboardCompiler(); - await k.init(compilerTestCallbacks, {...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false}); - const source = k.load(inputFilename); - checkMessages(); - assert.isNotNull(source, 'k.load should not have returned null'); - - // Sanity check ... this is also checked in other tests - const valid = await k.validate(source); - checkMessages(); - assert.isTrue(valid, 'k.validate should not have failed'); - - // Actual test: compile to javascript - const jsCompiler = new LdmlKeyboardKeymanWebCompiler(compilerTestCallbacks, {...compilerTestOptions, saveDebug: true}); - const output = jsCompiler.compile('basic.xml', source); - assert.isNotNull(output); - - // Does the emitted js match? - const outputFixture = fs.readFileSync(outputFilename, 'utf-8').replaceAll(/\r\n/g, '\n'); - assert.strictEqual(output, outputFixture); - - // Second test: compile to javascript without debug formatting - const jsCompilerNoDebug = new LdmlKeyboardKeymanWebCompiler(compilerTestCallbacks, {...compilerTestOptions, saveDebug: false}); - const outputNoDebug = jsCompilerNoDebug.compile('basic.xml', source); - assert.isNotNull(outputNoDebug); - - // Does the emitted js match? The nodebug has no newline at end, but allow one in the fixture - const outputFixtureNoDebug = fs.readFileSync(outputFilenameNoDebug, 'utf-8').replaceAll(/\r\n/g, '\n').trim(); - assert.strictEqual(outputNoDebug, outputFixtureNoDebug); - - // TODO(lowpri): consider using Typescript parser to generate AST for further validation - }); -}); diff --git a/developer/src/kmc-ldml/test/test-layr.ts b/developer/src/kmc-ldml/test/test-layr.ts index 32d212a009..bc275d0ea1 100644 --- a/developer/src/kmc-ldml/test/test-layr.ts +++ b/developer/src/kmc-ldml/test/test-layr.ts @@ -129,5 +129,30 @@ describe('layr', function () { ]); }, }, + { + subpath: 'sections/layr/row-keys-whitespace.xml', + callback(sect) { + const layr = sect; + assert.ok(layr); + assert.equal(compilerTestCallbacks.messages.length, 0); + + assert.equal(layr.lists?.length, 1); + const list0 = layr.lists[0]; + assert.ok(list0); + assert.equal(list0.layers.length, 1); + assert.equal(list0.hardware.value, 'us'); + const layer0 = list0.layers[0]; + assert.ok(layer0); + assert.equal(layer0.rows.length, 2); + assert.equal(layer0.id.value, 'base'); + assert.equal(layer0.mod, constants.keys_mod_none); + for(const row of layer0.rows) { + assert.ok(row); + assert.equal(row.keys.length, 2); + assert.equal(row.keys[0]?.value, 'grave'); + assert.equal(row.keys[1]?.value, 'mistake'); + } + }, + }, ]); }); diff --git a/developer/src/kmc-ldml/test/test-vars.ts b/developer/src/kmc-ldml/test/test-vars.ts index e3ac53c4a9..05fb6d0b4a 100644 --- a/developer/src/kmc-ldml/test/test-vars.ts +++ b/developer/src/kmc-ldml/test/test-vars.ts @@ -121,6 +121,16 @@ describe('vars', function () { CompilerMessages.Error_DuplicateVariable({ids: 'upper, y'}) ], }, + { + subpath: 'sections/vars/fail-invalid-identifiers.xml', + errors: [ + CompilerMessages.Error_InvalidVariableIdentifer({id: 'invalid-string'}), + CompilerMessages.Error_InvalidVariableIdentifer({id: 'invalid-set'}), + CompilerMessages.Error_InvalidVariableIdentifer({id: 'invalid-uset'}), + CompilerMessages.Error_InvalidVariableIdentifer({id: 'a_marker_name_more_than_32_chars_long'}), + CompilerMessages.Error_InvalidVariableIdentifer({id: '😡'}), + ], + }, { subpath: 'sections/vars/fail-uset-props1.xml', errors: [ @@ -187,7 +197,14 @@ describe('vars', function () { CompilerMessages.Error_MissingStringVariable({id: 'missingStringInSet'}) ], }, - ], varsDependencies); + { + subpath: 'sections/vars/fail-badref-7.xml', + errors: [ + CompilerMessages.Error_MissingStringVariable({id: 'usedBeforeDefinition'}) + ], + strictErrors: true + }, +], varsDependencies); describe('should match some marker constants', () => { // neither of these live here, but, common/web/types does not import ldml-keyboard-constants otherwise. diff --git a/developer/src/kmc-ldml/test/test-visual-keyboard-compiler-e2e.ts b/developer/src/kmc-ldml/test/test-visual-keyboard-compiler-e2e.ts deleted file mode 100644 index df0aea0391..0000000000 --- a/developer/src/kmc-ldml/test/test-visual-keyboard-compiler-e2e.ts +++ /dev/null @@ -1,31 +0,0 @@ -import 'mocha'; -import {assert} from 'chai'; -import hextobin from '@keymanapp/hextobin'; -import { KvkFileWriter } from '@keymanapp/common-types'; -import {checkMessages, compilerTestOptions, compileVisualKeyboard, makePathToFixture} from './helpers/index.js'; - -describe('visual-keyboard-compiler', function() { - this.slow(500); // 0.5 sec -- json schema validation takes a while - - it('should build fixtures', async function() { - // Let's build basic.xml - // It should match basic.kvk (built from basic-kvk.txt) - - const inputFilename = makePathToFixture('basic.xml'); - const binaryFilename = makePathToFixture('basic-kvk.txt'); - - // Compile the visual keyboard - const vk = await compileVisualKeyboard(inputFilename, {...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false}); - assert.isNotNull(vk); - - // Use the builder to generate the binary output file - const writer = new KvkFileWriter(); - const code = writer.write(vk); - checkMessages(); - assert.isNotNull(code); - - // Compare output - let expected = await hextobin(binaryFilename, undefined, {silent:true}); - assert.deepEqual(code, expected); - }); -}); diff --git a/developer/src/kmc-ldml/test/test-visual-keyboard-compiler.ts b/developer/src/kmc-ldml/test/test-visual-keyboard-compiler.ts new file mode 100644 index 0000000000..ab412f1396 --- /dev/null +++ b/developer/src/kmc-ldml/test/test-visual-keyboard-compiler.ts @@ -0,0 +1,235 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + */ +import 'mocha'; +import * as path from 'path'; +import {assert} from 'chai'; +import { stripIndent } from 'common-tags'; + +import { KMX, KvkFileWriter, LDMLKeyboardXMLSourceFileReader, VisualKeyboard } from '@keymanapp/common-types'; +import hextobin from '@keymanapp/hextobin'; + +import { checkMessages, compilerTestCallbacks, compilerTestOptions, makePathToFixture } from './helpers/index.js'; + +import { LdmlKeyboardVisualKeyboardCompiler } from '../src/compiler/visual-keyboard-compiler.js'; +import { LdmlKeyboardCompiler } from '../src/main.js'; + +describe('visual-keyboard-compiler', function() { + this.slow(500); // 0.5 sec -- json schema validation takes a while + + it('should build fixtures', async function() { + // Let's build basic.xml + + // It should match basic.kvk (built from basic-kvk.txt) + const inputFilename = makePathToFixture('basic.xml'); + const binaryFilename = makePathToFixture('basic-kvk.txt'); + + // Compile the visual keyboard + const k = new LdmlKeyboardCompiler(); + assert.isTrue(await k.init(compilerTestCallbacks, {...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false})); + const source = k.load(inputFilename); + checkMessages(); + assert.isNotNull(source, 'k.load should not have returned null'); + + const valid = await k.validate(source); + checkMessages(); + assert.isTrue(valid, 'k.validate should not have failed'); + + let kmx = await k.compile(source); + assert(kmx, 'k.compile should not have failed'); + + const keyboardId = path.basename(inputFilename, '.xml'); + + const vk = (new LdmlKeyboardVisualKeyboardCompiler(compilerTestCallbacks)).compile(kmx.kmxplus, keyboardId); + checkMessages(); + assert.isNotNull(vk, 'LdmlKeyboardVisualKeyboardCompiler.compile should not have returned null'); + assert.isNotNull(kmx.keyboard.stores.find(store => + store.dwSystemID == KMX.KMXFile.TSS_VISUALKEYBOARD && + store.dpString == keyboardId + '.kvk' + )); + assert(typeof vk == 'object'); + + // Use the builder to generate the binary output file + const writer = new KvkFileWriter(); + const code = writer.write(vk); + assert.isEmpty(compilerTestCallbacks.messages); + assert.isNotNull(code); + + // Compare output + let expected = await hextobin(binaryFilename, undefined, {silent:true}); + + assert.deepEqual(code, expected); + }); + + it('should support various modifiers', async function() { + const xml = stripIndent` + + + + + + + + + + + + + + + `; + + const vk = await loadVisualKeyboardFromXml(xml, 'test'); + + assert.equal(vk.keys.length, 5); + assert.equal(vk.keys[0].shift, VisualKeyboard.VisualKeyboardShiftState.KVKS_NORMAL); + assert.equal(vk.keys[1].shift, VisualKeyboard.VisualKeyboardShiftState.KVKS_SHIFT); + assert.equal(vk.keys[2].shift, VisualKeyboard.VisualKeyboardShiftState.KVKS_RALT); + assert.equal(vk.keys[3].shift, VisualKeyboard.VisualKeyboardShiftState.KVKS_RCTRL | VisualKeyboard.VisualKeyboardShiftState.KVKS_RALT); + assert.equal(vk.keys[4].shift, VisualKeyboard.VisualKeyboardShiftState.KVKS_SHIFT | VisualKeyboard.VisualKeyboardShiftState.KVKS_RALT); + }); + + it('should emit the correct associated keyboard id', async function() { + const xml = stripIndent` + + + + + + + + + + + `; + + const vk = await loadVisualKeyboardFromXml(xml, 'test'); + + assert.equal(vk.header.associatedKeyboard, 'test'); + }); + + it('should support ', async function() { + const xml = stripIndent` + + + + + + + + + + + + + + + + `; + + const vk = await loadVisualKeyboardFromXml(xml, 'test'); + + assert.equal(vk.keys.length, 2); + assert.equal(vk.keys[0].text, 'C'); + assert.equal(vk.keys[1].text, 'D'); + }); + + it('should correctly decode \\u{xxxx}', async function() { + const xml = stripIndent` + + + + + + + + + + + + + + + `; + + const vk = await loadVisualKeyboardFromXml(xml, 'test'); + + assert.equal(vk.keys.length, 2); + assert.equal(vk.keys[0].text, '\u{0e80}'); + assert.equal(vk.keys[1].text, '\u{0e81}'); + }); + + it('should read string variables in key.output', async function() { + const xml = stripIndent` + + + + + + + + + + + + + + + `; + + const vk = await loadVisualKeyboardFromXml(xml, 'test'); + + assert.equal(vk.keys.length, 1); + assert.equal(vk.keys[0].text, '2'); + }); + + it('should read string variables in display.display', async function() { + const xml = stripIndent` + + + + + + + + + + + + + + + + + + `; + + const vk = await loadVisualKeyboardFromXml(xml, 'test'); + + assert.equal(vk.keys.length, 1); + assert.equal(vk.keys[0].text, '2'); + }); +}); + +async function loadVisualKeyboardFromXml(xml: string, id: string) { + const data = new TextEncoder().encode(xml); + assert.isOk(data); + + const reader = new LDMLKeyboardXMLSourceFileReader(compilerTestOptions.readerOptions, compilerTestCallbacks); + const source = reader.load(data); + assert.isEmpty(compilerTestCallbacks.messages); + assert.isOk(source); + + const k = new LdmlKeyboardCompiler(); + assert.isTrue(await k.init(compilerTestCallbacks, compilerTestOptions)); + + const kmx = await k.compile(source); + assert(kmx, 'k.compile should not have failed'); + + const vk = (new LdmlKeyboardVisualKeyboardCompiler(compilerTestCallbacks)).compile(kmx.kmxplus, id); + assert(typeof vk == 'object'); + assert.isEmpty(compilerTestCallbacks.messages); + assert.isOk(vk); + + return vk; +} diff --git a/developer/src/kmc/src/util/getLastGitCommitDate.ts b/developer/src/kmc/src/util/getLastGitCommitDate.ts index bc85edacce..955927c20d 100644 --- a/developer/src/kmc/src/util/getLastGitCommitDate.ts +++ b/developer/src/kmc/src/util/getLastGitCommitDate.ts @@ -10,21 +10,53 @@ export const expectedGitDateFormat = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d\d\ */ export function getLastGitCommitDate(path: string): string { // TZ=UTC0 git log -1 --no-merges --date=format:%Y-%m-%dT%H:%M:%SZ --format=%ad - let result = null; + let result = callGitLog([ + 'log', // git log + '-1', // one commit only + '--no-merges', // we're only interested in 'real' commits + '--format=%at', // emit only the commit date as a UNIX timestamp + '--', + path + ]); - try { - result = execFileSync('git', [ + if(typeof result != 'string') { + return null; + } + + if(result == '') { + // #12626: no history was found, but not an error. We may have only a merge + // commit in history for that file, allow merges in the log + result = callGitLog([ 'log', // git log '-1', // one commit only - '--no-merges', // we're only interested in 'real' commits '--format=%at', // emit only the commit date as a UNIX timestamp '--', path - ], { + ]); + + if(typeof result != 'string') { + return null; + } + } + + const sec = Number.parseInt(result); + if(Number.isNaN(sec)) { + // We received invalid data, perhaps a git error string so just ignore + return null; + } + + // We receive a timestamp in seconds but we need milliseconds + return new Date(sec * 1000).toISOString(); +} + +function callGitLog(params: string[]) { + try { + const result = execFileSync('git', params, { encoding: 'utf-8', // force a string result rather than Buffer windowsHide: true, // on windows, we may need this to suppress a console window popup stdio: ['pipe', 'pipe', 'pipe'] // all output via pipe, so we don't get git errors on console }); + return result.trim(); } catch (e) { // If git is not available, or the file is not in-repo, then it is probably // fine to just silently return null, as the only machines where this is @@ -34,9 +66,4 @@ export function getLastGitCommitDate(path: string): string { // seems low. return null; } - - // We receive a timestamp in seconds but we need milliseconds - const msec = Number.parseInt(result.trim()) * 1000; - - return new Date(msec).toISOString(); } diff --git a/developer/src/kmcmplib/src/CompilerInterfacesWasm.cpp b/developer/src/kmcmplib/src/CompilerInterfacesWasm.cpp index 977b3b7443..3a0351f80f 100644 --- a/developer/src/kmcmplib/src/CompilerInterfacesWasm.cpp +++ b/developer/src/kmcmplib/src/CompilerInterfacesWasm.cpp @@ -90,9 +90,16 @@ struct BindingType> { using ValBinding = BindingType; using WireType = ValBinding::WireType; +#if __EMSCRIPTEN_major__ == 3 && __EMSCRIPTEN_minor__ == 1 && __EMSCRIPTEN_tiny__ >= 60 + // emscripten-core/emscripten#21692 + static WireType toWireType(const std::vector &vec, rvp::default_tag) { + return ValBinding::toWireType(val::array(vec), rvp::default_tag{}); + } +#else static WireType toWireType(const std::vector &vec) { return ValBinding::toWireType(val::array(vec)); } +#endif static std::vector fromWireType(WireType value) { return vecFromJSArray(ValBinding::fromWireType(value)); diff --git a/developer/src/kmcmplib/wasm.build.linux.in b/developer/src/kmcmplib/wasm.build.linux.in index c5e61a9bda..27cb7ba68f 100644 --- a/developer/src/kmcmplib/wasm.build.linux.in +++ b/developer/src/kmcmplib/wasm.build.linux.in @@ -1,4 +1,4 @@ [binaries] -c = ['$EMSCRIPTEN_BASE/emcc.py'] -cpp = ['$EMSCRIPTEN_BASE/em++.py'] -ar = ['$EMSCRIPTEN_BASE/emar.py'] +c = ['$EMSCRIPTEN_BASE/emcc'] +cpp = ['$EMSCRIPTEN_BASE/em++'] +ar = ['$EMSCRIPTEN_BASE/emar'] diff --git a/developer/src/kmcmplib/wasm.build.mac.in b/developer/src/kmcmplib/wasm.build.mac.in index c5e61a9bda..27cb7ba68f 100644 --- a/developer/src/kmcmplib/wasm.build.mac.in +++ b/developer/src/kmcmplib/wasm.build.mac.in @@ -1,4 +1,4 @@ [binaries] -c = ['$EMSCRIPTEN_BASE/emcc.py'] -cpp = ['$EMSCRIPTEN_BASE/em++.py'] -ar = ['$EMSCRIPTEN_BASE/emar.py'] +c = ['$EMSCRIPTEN_BASE/emcc'] +cpp = ['$EMSCRIPTEN_BASE/em++'] +ar = ['$EMSCRIPTEN_BASE/emar'] diff --git a/developer/src/tike/main/KeymanDeveloperOptions.pas b/developer/src/tike/main/KeymanDeveloperOptions.pas index 6eb0573096..5f8613499f 100644 --- a/developer/src/tike/main/KeymanDeveloperOptions.pas +++ b/developer/src/tike/main/KeymanDeveloperOptions.pas @@ -83,6 +83,7 @@ type procedure optWriteInt(const nm: string; value: Integer); procedure WriteServerConfigurationJson; class function Default_DefaultProjectPath: string; static; + function BackOffAndSaveJson(const Filename: string; const JSON: TJSONObject): Boolean; public procedure Read; procedure Write; @@ -406,8 +407,6 @@ begin end; procedure TKeymanDeveloperOptions.Write; -var - count: Integer; begin // If the output file does not exist, this creates it. Note that we load // from the existing file before writing out our set of options, which is @@ -464,33 +463,7 @@ begin ForceDirectories(TKeymanDeveloperPaths.OptionsPath); try - count := 0; - repeat - try - SaveJSONToFile(TKeymanDeveloperPaths.OptionsPath + TKeymanDeveloperPaths.S_OptionsJson, json); - Break; - except - on E:EOSError do - begin - if E.ErrorCode = ERROR_ACCESS_DENIED then - begin - // Back off and retry 0.5 sec later, it's probably another - // instance happening to read or write the settings file - Inc(count); - Sleep(500); - end - else - raise; - end - else - raise; - end; - until count >= Max_Retries; - if count >= Max_Retries then - begin - if TKeymanSentryClient.Instance <> nil then - TKeymanSentryClient.Instance.ReportMessage('TKeymanDeveloperOptions: Failed to write settings after 5 tries'); - end; + BackOffAndSaveJson(TKeymanDeveloperPaths.OptionsPath + TKeymanDeveloperPaths.S_OptionsJson, json); except on E:Exception do begin @@ -507,6 +480,39 @@ begin WriteServerConfigurationJson; end; +function TKeymanDeveloperOptions.BackOffAndSaveJson(const Filename: string; const JSON: TJSONObject): Boolean; +var + count: Integer; +begin + count := 0; + repeat + try + SaveJSONToFile(Filename, JSON); + Exit(True); + except + on E:EOSError do + begin + if E.ErrorCode = ERROR_ACCESS_DENIED then + begin + // Back off and retry 0.5 sec later, it's probably another + // instance happening to read or write the settings file + Inc(count); + Sleep(500); + end + else + raise; + end + else + raise; + end; + until count >= Max_Retries; + + if TKeymanSentryClient.Instance <> nil then + TKeymanSentryClient.Instance.ReportMessage('TKeymanDeveloperOptions: Failed to write '+Filename+' after 5 tries'); + + Result := False; +end; + procedure TKeymanDeveloperOptions.WriteServerConfigurationJson; var o: TJSONObject; @@ -517,7 +523,8 @@ begin o.AddPair('ngrokToken', FServerNgrokToken); o.AddPair('useNgrok', TJSONBool.Create(FServerUseNgrok)); o.AddPair('ngrokVisible', TJSONBool.Create(FServerServerShowConsoleWindow)); - SaveJSONToFile(TKeymanDeveloperPaths.ServerDataPath + TKeymanDeveloperPaths.S_ServerConfigJson, o); + ForceDirectories(TKeymanDeveloperPaths.ServerDataPath); + BackOffAndSaveJSON(TKeymanDeveloperPaths.ServerDataPath + TKeymanDeveloperPaths.S_ServerConfigJson, o); finally o.Free; end; diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift index 5b77f79307..d1d134b249 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift @@ -831,12 +831,22 @@ public class Manager: NSObject, UIGestureRecognizerDelegate { shared.copyUserDefaults(to: nonShared, withKeys: keysToCopy, shouldOverwrite: true) do { try shared.copyFiles(to: nonShared) - FontManager.shared.registerCustomFonts() } catch { let message = ("Failed to copy from shared container: \(error)") os_log("%{public}s", log:KeymanEngineLogger.settings, type: .error, message) SentryManager.capture(error, message:message) } + + // This operation has a surprisingly high cost and isn't critical + // for getting the keyboard loaded and available to the OS. + // + // We certainly want it done _soon_... but we don't want it to cause + // the primary, synchronous initialization call from the OS to run so + // long that the keyboard fails to start due to its enforced time + // constraints. + DispatchQueue.main.async { + FontManager.shared.registerCustomFonts() + } } } diff --git a/ios/exportAppStore.plist b/ios/exportAppStore.plist index 48ef58f532..bb7315750e 100644 --- a/ios/exportAppStore.plist +++ b/ios/exportAppStore.plist @@ -7,9 +7,9 @@ teamID 3YE4W86L3G signingCertificate - BB842A0081298BBA6D034C85451750B08FC9EE8A + 683E4A3CDF5277C13004E84D63E31D2F720AB063 installerSigningCertificate - BB842A0081298BBA6D034C85451750B08FC9EE8A + 683E4A3CDF5277C13004E84D63E31D2F720AB063 provisioningProfiles Tavultesoft.Keyman diff --git a/linux/Dockerfile b/linux/Dockerfile index ea07ec699a..fac302b474 100644 --- a/linux/Dockerfile +++ b/linux/Dockerfile @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2023 SIL International. All rights reserved. +# Copyright (c) 2022-2023 SIL Global. All rights reserved. # # builder image for a linux build # see ../docs/build/linux-ubuntu.md @@ -7,7 +7,7 @@ ARG OS_VERSION=latest ARG OS_PLATFORM=amd64 FROM --platform=${OS_PLATFORM} ubuntu:${OS_VERSION} -LABEL org.opencontainers.image.authors="SIL International." +LABEL org.opencontainers.image.authors="SIL Global." LABEL org.opencontainers.image.url="https://github.com/keymanapp/keyman.git" LABEL org.opencontainers.image.title="Keyman Linux Build Image" diff --git a/linux/debian/changelog b/linux/debian/changelog index ddd4d3611d..22fd40caad 100644 --- a/linux/debian/changelog +++ b/linux/debian/changelog @@ -1,3 +1,18 @@ +keyman (17.0.333-1) unstable; urgency=medium + + * New upstream release. + + -- Eberhard Beilharz Thu, 16 Jan 2025 16:29:44 +0100 + +keyman (17.0.332-1) unstable; urgency=medium + + * set environment variable for rendering of downloads dialog (#12617) + * add `keymanFacename` to .ldml file (#12283) + * ignore exceptions trying to install cache (#11885) + * New upstream release. + + -- Eberhard Beilharz Wed, 11 Dec 2024 15:53:59 +0100 + keyman (17.0.326-1) unstable; urgency=medium * New upstream release diff --git a/linux/debian/com.keyman.config.appdata.xml b/linux/debian/com.keyman.config.appdata.xml index 9df49a37dc..e7f7152422 100644 --- a/linux/debian/com.keyman.config.appdata.xml +++ b/linux/debian/com.keyman.config.appdata.xml @@ -5,10 +5,15 @@ MIT MIT Keyman - SIL International + + SIL Global + https://keyman.com/cdn/dev/img/keyman-logo.png Keyman for Linux Keyman makes it possible for you to type in over 2,000 languages + + mild +

diff --git a/linux/debian/com.keyman.ibus_keyman.metainfo.xml b/linux/debian/com.keyman.ibus_keyman.metainfo.xml index 1f021d2fce..7b109278a1 100644 --- a/linux/debian/com.keyman.ibus_keyman.metainfo.xml +++ b/linux/debian/com.keyman.ibus_keyman.metainfo.xml @@ -2,9 +2,11 @@ com.keyman.ibus_keyman MIT - GPL-2+ + GPL-2.0+ Keyman - SIL International + + SIL Global + https://keyman.com/cdn/dev/img/keyman-logo.png ibus-keyman diff --git a/linux/debian/control b/linux/debian/control index 611d586225..66a410f1e9 100644 --- a/linux/debian/control +++ b/linux/debian/control @@ -26,6 +26,7 @@ Build-Depends: pkgconf, python3-all (>= 3.5), python3-dbus, + python3-fonttools, python3-gi, python3-lxml, python3-magic, @@ -90,6 +91,7 @@ Depends: gir1.2-webkit2-4.1 | gir1.2-webkit2-4.0, keyman-engine, python3-bs4, + python3-fonttools, python3-gi, python3-packaging, python3-sentry-sdk (>= 1.1), diff --git a/linux/debian/copyright b/linux/debian/copyright index ee6603bded..46f156e19b 100644 --- a/linux/debian/copyright +++ b/linux/debian/copyright @@ -4,24 +4,24 @@ Upstream-Contact: Keyman team Source: https://github.com/keymanapp/keyman Files: * -Copyright: 2018-2024 SIL International +Copyright: 2018-2025 SIL Global License: MIT Files: linux/ibus-keyman/* -Copyright: 2004-2024 SIL International +Copyright: 2004-2025 SIL Global License: GPL-2+ Files: linux/ibus-keyman/src/keymanutil.c linux/ibus-keyman/src/keymanutil.h linux/ibus-keyman/src/kmpdetails.c linux/ibus-keyman/src/kmpdetails.h -Copyright: 2009-2024 SIL International +Copyright: 2009-2025 SIL Global License: GPL-2+ or MIT Files: linux/ibus-keyman/src/keyman-service.c linux/ibus-keyman/src/keyman-service.h linux/keyman-config/buildtools/help2md -Copyright: 2018-2024 SIL International +Copyright: 2018-2025 SIL Global License: GPL-3+ Files: linux/ibus-keyman/tests/ibusimcontext.c @@ -31,7 +31,7 @@ Copyright: 2008-2010, Peng Huang 2008-2013, Peng Huang 2008-2021, Red Hat, Inc. 2015-2021, Takao Fujiwara - 2021-2024, SIL International + 2021-2025, SIL Global License: LGPL-2.1+ Files: linux/keyman-config/buildtools/help2man @@ -41,7 +41,7 @@ License: GPL-3+ Files: debian/com.keyman.config.appdata.xml debian/com.keyman.ibus_keyman.metainfo.xml Copyright: 2019 Daniel Glassey - 2022-2024 SIL International + 2022-2025 SIL Global License: MIT License: MIT diff --git a/linux/debian/libkeymancore1.symbols b/linux/debian/libkeymancore1.symbols index 99a3be74a4..236801261b 100644 --- a/linux/debian/libkeymancore1.symbols +++ b/linux/debian/libkeymancore1.symbols @@ -1,7 +1,8 @@ libkeymancore.so.1 libkeymancore1 #MINVER# * Build-Depends-Package: libkeymancore-dev - (c++|optional)"typeinfo name for std::codecvt_utf8_utf16@Base" 17.0.244 +# We only use a C API, so ignore all C++ symbols in the std namespace that bleed through + (c++|regex|optional)"std::" 17.0.332 km_core_context_clear@Base 17.0.195 km_core_context_get@Base 17.0.195 km_core_context_item_list_size@Base 17.0.195 diff --git a/linux/ibus-keyman/src/engine.c b/linux/ibus-keyman/src/engine.c index edd6db0720..d35f49c7e1 100644 --- a/linux/ibus-keyman/src/engine.c +++ b/linux/ibus-keyman/src/engine.c @@ -3,7 +3,7 @@ /* * Keyman Input Method for IBUS (The Input Bus) * - * Copyright (C) 2009-2023 SIL International + * Copyright (C) 2009-2023 SIL Global * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public diff --git a/linux/ibus-keyman/src/engine.h b/linux/ibus-keyman/src/engine.h index 5aaa6303a7..21a99314fb 100644 --- a/linux/ibus-keyman/src/engine.h +++ b/linux/ibus-keyman/src/engine.h @@ -3,7 +3,7 @@ /* * Keyman Input Method for IBUS (The Input Bus) * - * Copyright (C) 2018 SIL International + * Copyright (C) 2018 SIL Global * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public diff --git a/linux/ibus-keyman/src/keyman-service.c b/linux/ibus-keyman/src/keyman-service.c index b5c4afc75f..fa2bdb702b 100644 --- a/linux/ibus-keyman/src/keyman-service.c +++ b/linux/ibus-keyman/src/keyman-service.c @@ -3,7 +3,7 @@ /* * Keyman Input Method for IBUS (The Input Bus) * - * Copyright (C) 2018-2023 SIL International + * Copyright (C) 2018-2023 SIL Global * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public diff --git a/linux/ibus-keyman/src/keyman-service.h b/linux/ibus-keyman/src/keyman-service.h index 0f7221478a..a989d6b513 100644 --- a/linux/ibus-keyman/src/keyman-service.h +++ b/linux/ibus-keyman/src/keyman-service.h @@ -3,7 +3,7 @@ /* * Keyman Input Method for IBUS (The Input Bus) * - * Copyright (C) 2018-2023 SIL International + * Copyright (C) 2018-2023 SIL Global * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public diff --git a/linux/ibus-keyman/src/keymanutil.c b/linux/ibus-keyman/src/keymanutil.c index 26bfcf1d55..be4c66c56d 100644 --- a/linux/ibus-keyman/src/keymanutil.c +++ b/linux/ibus-keyman/src/keymanutil.c @@ -3,7 +3,7 @@ /* * Keyman Input Method for IBUS (The Input Bus) * - * Copyright (C) 2018 SIL International + * Copyright (C) 2018 SIL Global * * keymanutil is dual licensed under the MIT or GPL licenses as described below. * diff --git a/linux/ibus-keyman/src/keymanutil.h b/linux/ibus-keyman/src/keymanutil.h index 8a3f4506fa..c4a10687f5 100644 --- a/linux/ibus-keyman/src/keymanutil.h +++ b/linux/ibus-keyman/src/keymanutil.h @@ -3,7 +3,7 @@ /* * Keyman Input Method for IBUS (The Input Bus) * - * Copyright (C) 2018-2023 SIL International + * Copyright (C) 2018-2023 SIL Global * * keymanutil is dual licensed under the MIT or GPL licenses as described below. * diff --git a/linux/ibus-keyman/src/kmpdetails.c b/linux/ibus-keyman/src/kmpdetails.c index ba6e26faea..5d98c3bf34 100644 --- a/linux/ibus-keyman/src/kmpdetails.c +++ b/linux/ibus-keyman/src/kmpdetails.c @@ -3,7 +3,7 @@ /* * Keyman Input Method for IBUS (The Input Bus) * - * Copyright (C) 2018 SIL International + * Copyright (C) 2018 SIL Global * * kmpdetails is dual licensed under the MIT or GPL licenses as described below. * diff --git a/linux/ibus-keyman/src/main.c b/linux/ibus-keyman/src/main.c index 477d744d9b..b7da28e97d 100644 --- a/linux/ibus-keyman/src/main.c +++ b/linux/ibus-keyman/src/main.c @@ -3,7 +3,7 @@ /* * Keyman Input Method for IBUS (The Input Bus) * - * Copyright (C) 2018 SIL International + * Copyright (C) 2018 SIL Global * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public diff --git a/linux/ibus-keyman/src/test/testdata/kmp.json b/linux/ibus-keyman/src/test/testdata/kmp.json index d2ee4fa9e1..4320826b2f 100644 --- a/linux/ibus-keyman/src/test/testdata/kmp.json +++ b/linux/ibus-keyman/src/test/testdata/kmp.json @@ -14,7 +14,7 @@ "description": "LIBTRALO" }, "copyright": { - "description": "\u00A9 2008-2018 SIL International" + "description": "\u00A9 2008-2018 SIL Global" }, "author": { "description": "support@keyman.com", diff --git a/linux/ibus-keyman/src/test/testdata/kmp1.json b/linux/ibus-keyman/src/test/testdata/kmp1.json index 5d9954c330..5ca7bd87d0 100644 --- a/linux/ibus-keyman/src/test/testdata/kmp1.json +++ b/linux/ibus-keyman/src/test/testdata/kmp1.json @@ -14,7 +14,7 @@ "description": "LIBTRALO" }, "copyright": { - "description": "\u00A9 2008-2018 SIL International" + "description": "\u00A9 2008-2018 SIL Global" }, "author": { "description": "support@keyman.com", diff --git a/linux/ibus-keyman/src/test/testdata/kmp2.json b/linux/ibus-keyman/src/test/testdata/kmp2.json index 5e889091f7..187f2dbbb6 100644 --- a/linux/ibus-keyman/src/test/testdata/kmp2.json +++ b/linux/ibus-keyman/src/test/testdata/kmp2.json @@ -14,7 +14,7 @@ "description": "LIBTRALO" }, "copyright": { - "description": "\u00A9 2008-2018 SIL International" + "description": "\u00A9 2008-2018 SIL Global" }, "author": { "description": "support@keyman.com", diff --git a/linux/ibus-keyman/tests/ibusimcontext.c b/linux/ibus-keyman/tests/ibusimcontext.c index 8cf389d749..c6d7af1e98 100644 --- a/linux/ibus-keyman/tests/ibusimcontext.c +++ b/linux/ibus-keyman/tests/ibusimcontext.c @@ -4,7 +4,7 @@ * Copyright (C) 2008-2013 Peng Huang * Copyright (C) 2015-2022 Takao Fujiwara * Copyright (C) 2008-2022 Red Hat, Inc. - * Copyright (C) 2021-2022 SIL International + * Copyright (C) 2021-2022 SIL Global * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public diff --git a/linux/ibus-keyman/tests/ibusimcontext.h b/linux/ibus-keyman/tests/ibusimcontext.h index f45b404a26..94e8687bde 100644 --- a/linux/ibus-keyman/tests/ibusimcontext.h +++ b/linux/ibus-keyman/tests/ibusimcontext.h @@ -3,7 +3,7 @@ /* ibus - The Input Bus * Copyright (C) 2008-2010 Peng Huang * Copyright (C) 2008-2010 Red Hat, Inc. - * Copyright (C) 2021-2022 SIL International + * Copyright (C) 2021-2022 SIL Global * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public diff --git a/linux/keyman-config/COPYING b/linux/keyman-config/COPYING index 5260aa6d9e..485c2c057c 100644 --- a/linux/keyman-config/COPYING +++ b/linux/keyman-config/COPYING @@ -1,4 +1,4 @@ -Copyright (c) 2018 SIL International +Copyright (c) 2018 SIL Global Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/linux/keyman-config/Makefile b/linux/keyman-config/Makefile index f057dcad0c..d36bf16a0f 100644 --- a/linux/keyman-config/Makefile +++ b/linux/keyman-config/Makefile @@ -48,7 +48,7 @@ MOFILES := $(POFILES:.po=/LC_MESSAGES/keyman-config.mo) update-template: version xgettext --package-name "keyman" --package-version "$(VERSION)" \ - --msgid-bugs-address "" --copyright-holder "SIL International" \ + --msgid-bugs-address "" --copyright-holder "SIL Global" \ --language=Python --directory=. --output-dir=locale --output=keyman-config.pot \ --add-comments=i18n: --sort-by-file --width=98 \ keyman_config/*.py km-* diff --git a/linux/keyman-config/buildtools/help2md b/linux/keyman-config/buildtools/help2md index 23a3f5b26b..a32685d1de 100755 --- a/linux/keyman-config/buildtools/help2md +++ b/linux/keyman-config/buildtools/help2md @@ -5,7 +5,7 @@ # Copyright (C) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2009, # 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2020, 2021 Free Software # Foundation, Inc. -# Copyright (C) 2021 SIL International +# Copyright (C) 2021 SIL Global # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -76,7 +76,7 @@ my $version_info = enc_user sprintf _(<<'EOT'), $this_program, $this_version; Copyright (C) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2020, 2021 Free Software Foundation, Inc. -Copyright (C) 2021 SIL International +Copyright (C) 2021 SIL Global This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. diff --git a/linux/keyman-config/keyman_config/__init__.py b/linux/keyman-config/keyman_config/__init__.py index 2bb3cdbaa2..26fbeddf7d 100644 --- a/linux/keyman-config/keyman_config/__init__.py +++ b/linux/keyman-config/keyman_config/__init__.py @@ -1,5 +1,6 @@ import atexit import gettext +import importlib import logging import os import pathlib @@ -53,6 +54,14 @@ def initialize_sentry(): SentryErrorHandling().initialize_sentry() +def are_requirements_missing(): + try: + ttLib = importlib.import_module('fontTools.ttLib') + except ImportError: + return True + return False + + class FileCleanup(): """ Allow to register files that will be deleted when the process exits diff --git a/linux/keyman-config/keyman_config/install_kmp.py b/linux/keyman-config/keyman_config/install_kmp.py index 1ae05d905a..21520a10c2 100755 --- a/linux/keyman-config/keyman_config/install_kmp.py +++ b/linux/keyman-config/keyman_config/install_kmp.py @@ -177,8 +177,8 @@ class InstallKmp(): # Special handling to convert kvk into LDML logging.info("Converting %s to LDML and installing both as as keyman file", f['name']) - ldml = convert_kvk_to_ldml(fpath) name, ext = os.path.splitext(f['name']) + ldml = convert_kvk_to_ldml(name, fpath) ldmlfile = os.path.join(self.packageDir, f"{name}.ldml") output_ldml(ldmlfile, ldml) elif ftype == KMFileTypes.KM_ICON: diff --git a/linux/keyman-config/keyman_config/kvk2ldml.py b/linux/keyman-config/keyman_config/kvk2ldml.py index 5b3ab85ae8..875ab7b10c 100755 --- a/linux/keyman-config/keyman_config/kvk2ldml.py +++ b/linux/keyman-config/keyman_config/kvk2ldml.py @@ -1,11 +1,15 @@ #!/usr/bin/python3 import logging +import os import struct import sys +from fontTools import ttLib from lxml import etree +from keyman_config.kmpmetadata import parsemetadata + # .kvk file format # KVK files are variable length files with variable sized structures. @@ -311,7 +315,18 @@ def get_modifer(key): return modifier -def convert_ldml(kvkData): +def _fontFacename(fontFilename): + """Get the facename from fontFilename's TTF tables""" + # From https://github.com/mcfletch/ttfquery/blob/master/ttfquery/describe.py + FONT_SPECIFIER_NAME_ID = 4 + font = ttLib.TTFont(fontFilename) + for record in font['name'].names: + if record.nameID == FONT_SPECIFIER_NAME_ID: + return record.toUnicode() + return None + + +def convert_ldml(keyboardName, kvkData, kmpJsonFilename): keymaps = {} for key in kvkData.Keys: @@ -349,7 +364,19 @@ def convert_ldml(kvkData): else: keymaps["shift"] = (uskey,) + info, system, options, keyboards, files = parsemetadata(kmpJsonFilename) + ldml = etree.Element("keyboard", locale="zzz-keyman") + for keyboard in keyboards: + if keyboard['id'] != keyboardName: + continue + if 'oskFont' in keyboard: + fontFile = os.path.join(os.path.dirname(kmpJsonFilename), keyboard['oskFont']) + font = _fontFacename(fontFile) + if font: + ldml.set('keymanFacename', font) + break + etree.SubElement(ldml, "version", platform="11") names = etree.SubElement(ldml, "names") names.append(etree.Element("name", value="ZZZ")) @@ -402,6 +429,7 @@ def parse_kvk_file(kvkfile): return kvkData -def convert_kvk_to_ldml(kvkfile): +def convert_kvk_to_ldml(name, kvkfile): kvkData = parse_kvk_file(kvkfile) - return convert_ldml(kvkData) + kmpJsonFilename = os.path.join(os.path.dirname(kvkfile), 'kmp.json') + return convert_ldml(name, kvkData, kmpJsonFilename) diff --git a/linux/keyman-config/keyman_config/sentry_handling.py b/linux/keyman-config/keyman_config/sentry_handling.py index 7585f88e43..7d76843a22 100644 --- a/linux/keyman-config/keyman_config/sentry_handling.py +++ b/linux/keyman-config/keyman_config/sentry_handling.py @@ -1,4 +1,9 @@ #!/usr/bin/python3 +''' +Keyman is copyright (C) SIL Global. MIT License. + +Implements the Sentry error handling +''' import getpass import hashlib import importlib diff --git a/linux/keyman-config/km-config b/linux/keyman-config/km-config index f40ebeebb4..e099c59eda 100755 --- a/linux/keyman-config/km-config +++ b/linux/keyman-config/km-config @@ -2,6 +2,7 @@ import argparse import logging +import os import sys import gi @@ -9,7 +10,10 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk -from keyman_config import __versionwithtag__, __pkgversion__, add_standard_arguments, initialize_logging, initialize_sentry, verify_dbus_running +from keyman_config import ( + _, __versionwithtag__, __pkgversion__, add_standard_arguments, + are_requirements_missing, initialize_logging, initialize_sentry, + verify_dbus_running) from keyman_config.handle_install import download_and_install_package from keyman_config.ibus_util import verify_ibus_daemon from keyman_config.view_installed import ViewInstalledWindow @@ -42,6 +46,18 @@ if __name__ == '__main__': initialize_sentry() verify_dbus_running() + if are_requirements_missing(): + if args.install or args.url: + logging.error('Missing requirements. Please install python3-fonttools.') + else: + dialog = Gtk.MessageDialog( + None, 0, Gtk.MessageType.ERROR, + Gtk.ButtonsType.OK, _("Missing requirements. Please install python3-fonttools.")) + dialog.run() + dialog.destroy() + sys.exit(1) + + logging.info('Keyman version %s %s', __versionwithtag__, __pkgversion__) verify_ibus_daemon(False) @@ -51,6 +67,10 @@ if __name__ == '__main__': elif args.url: download_and_install_package(args.url) else: + # Workaround for bug in webkit2gtk (#12587) + if not 'WEBKIT_DISABLE_DMABUF_RENDERER' in os.environ: + os.environ['WEBKIT_DISABLE_DMABUF_RENDERER'] = '1' + w = ViewInstalledWindow() try: w.run() diff --git a/linux/keyman-config/km-kvk2ldml b/linux/keyman-config/km-kvk2ldml index 8a9adbb322..6974ac7ebb 100755 --- a/linux/keyman-config/km-kvk2ldml +++ b/linux/keyman-config/km-kvk2ldml @@ -5,7 +5,9 @@ import logging import os import sys -from keyman_config import add_standard_arguments, initialize_logging, initialize_sentry, verify_dbus_running +from keyman_config import ( + are_requirements_missing, add_standard_arguments, initialize_logging, + initialize_sentry, verify_dbus_running) from keyman_config.kvk2ldml import parse_kvk_file, print_kvk, convert_ldml, output_ldml @@ -25,6 +27,10 @@ def main(): initialize_sentry() verify_dbus_running() + if are_requirements_missing(): + logging.error('km-kvk2ldml: Missing requirements. Please install python3-fonttools.') + sys.exit(1) + name, ext = os.path.splitext(args.kvkfile) # Check if input file extension is kvk if ext != ".kvk": @@ -54,7 +60,8 @@ def main(): try: with open(outputfile, 'wb') as ldmlfile: - ldml = convert_ldml(kvkData) + kmpJsonFilename = os.path.join(os.path.dirname(args.kvkfile), 'kmp.json') + ldml = convert_ldml(name, kvkData, kmpJsonFilename) output_ldml(ldmlfile, ldml) except PermissionError: logging.error(f'km-kvk2ldml: error, permission denied writing file `{outputfile}`') diff --git a/linux/keyman-config/locale/keyman-config.pot b/linux/keyman-config/locale/keyman-config.pot index 50b0d4d8ea..ed377151f7 100644 --- a/linux/keyman-config/locale/keyman-config.pot +++ b/linux/keyman-config/locale/keyman-config.pot @@ -1,5 +1,5 @@ # SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR SIL International +# Copyright (C) YEAR SIL Global # This file is distributed under the same license as the keyman package. # FIRST AUTHOR , YEAR. # diff --git a/linux/keyman-config/tests/test_kvk2ldml.py b/linux/keyman-config/tests/test_kvk2ldml.py new file mode 100644 index 0000000000..9f4c072874 --- /dev/null +++ b/linux/keyman-config/tests/test_kvk2ldml.py @@ -0,0 +1,57 @@ +import os +import shutil +import sys +import tempfile +import unittest + +from keyman_config.kvk2ldml import KVKData, NFont, convert_ldml + +class Kvk2LdmlTests(unittest.TestCase): + def _createKmpJson(self, packagedir): + kmpJsonFilename = os.path.join(packagedir, 'kmp.json') + with open(kmpJsonFilename, 'w') as file: + file.write('''{ + "system": { + "keymanDeveloperVersion": "18.0", + "fileVersion": "7.0" + }, + "files": [ { + "name": "khmer_angkor.kmx", + "description": "Keyboard Khmer Angkor" + }, { + "name": "kmp.json", + "description": "Package information (JSON)" + } ], + "keyboards": [ { + "name": "Khmer Angkor", + "id": "khmer_angkor", + "version": "1.5", + "oskFont": "keymanweb-osk.ttf", + "languages": [ { + "name": "Central Khmer (Khmer, Cambodia)", + "id": "km" + } ]} + ]}''') + return kmpJsonFilename + + def test_convert_ldml__adds_keymanFacename(self): + # Setup + keyboardName = 'khmer_angkor' + kvkData = KVKData() + kvkData.AssociatedKeyboard = keyboardName + kvkData.UnicodeFont = NFont() + kvkData.UnicodeFont.name='DontUseThis!' + + workdir = tempfile.TemporaryDirectory() + kmpJsonFilename = self._createKmpJson(workdir.name) + shutil.copy2(os.path.join(sys.path[0], '../../../common/resources/fonts/keymanweb-osk.ttf'), + workdir.name) + + # Execute + ldml = convert_ldml(keyboardName, kvkData, kmpJsonFilename) + + # Verify + self.assertEqual(ldml.get('locale'), "zzz-keyman") + self.assertEqual(ldml.get('keymanFacename'), 'SymChar') + + workdir.cleanup() diff --git a/linux/scripts/debian.sh b/linux/scripts/debian.sh index 66cc7ba157..3a896c10b2 100755 --- a/linux/scripts/debian.sh +++ b/linux/scripts/debian.sh @@ -36,7 +36,7 @@ for proj in ${projects}; do EXTRA_ARGS="--distribution ${DIST} --force-distribution" fi # shellcheck disable=SC2086 - dch --newversion "${version}-${DEBREVISION-1}" ${EXTRA_ARGS} "Re-release to Debian" + dch --newversion "${version}-${DEBREVISION-1}" ${EXTRA_ARGS} "" debuild -d -S -sa -Zxz cd "${BASEDIR}" done diff --git a/linux/scripts/upload-to-debian.sh b/linux/scripts/upload-to-debian.sh index 0d6d3252b8..ead2943983 100755 --- a/linux/scripts/upload-to-debian.sh +++ b/linux/scripts/upload-to-debian.sh @@ -92,21 +92,28 @@ function get_latest_stable_branch_name() { echo "${stable_branch##* }" } +# Push the changelog changes to GitHub and create a PR. Returns the PR# +# in the environment variable PR_NUMBER. function push_to_github_and_create_pr() { - local BRANCH=$1 - local BASE=$2 - local COMMIT_MSG=$3 - local PR_MSG=$4 - local pr_number + local BRANCH=$1 # `chore/linux/changelog` or `chore/linux/cherry-pick/changelog` + local BASE=$2 # stable branch, `beta` or `master` + local PR_TITLE=$3 # `Update debian changelog` + local PR_BODY=$4 # `@keymanapp-test-bot skip` if [[ -n "${PUSH}" ]]; then - ${NOOP} git push --force-with-lease origin "${BRANCH}" - pr_number=$(gh pr list --draft --search "${COMMIT_MSG}" --base "${BASE}" --json number --jq '.[].number') - if [[ -n ${pr_number} ]]; then - builder_echo "PR #${pr_number} already exists" - else - ${NOOP} gh pr create --draft --base "${BASE}" --title "${PR_MSG}" --body "@keymanapp-test-bot skip" - fi + # Push to origin. We force push to reset the branch the commit we just made. + # There shouldn't be any other commits on ${BRANCH} except the one we want to replace + # (if any). + ${NOOP} git push --force origin "${BRANCH}" + PR_NUMBER=$(gh pr list --draft --search "${PR_TITLE}" --base "${BASE}" --json number --jq '.[].number') + if [[ -n ${PR_NUMBER} ]]; then + builder_echo "PR #${PR_NUMBER} already exists" + else + ${NOOP} gh pr create --draft --base "${BASE}" --title "${PR_TITLE}" --body "${PR_BODY}" + PR_NUMBER=$(gh pr list --draft --search "${PR_TITLE}" --base "${BASE}" --json number --jq '.[].number') + fi + else + PR_NUMBER="" fi } @@ -142,12 +149,14 @@ cp debianpackage/keyman-*/debian/changelog debian/ git add debian/changelog COMMIT_MESSAGE="chore(linux): Update debian changelog" git commit -m "${COMMIT_MESSAGE}" -push_to_github_and_create_pr chore/linux/changelog "${DEPLOY_BRANCH#origin/}" "${COMMIT_MESSAGE}" "${COMMIT_MESSAGE}" +push_to_github_and_create_pr chore/linux/changelog "${DEPLOY_BRANCH#origin/}" "${COMMIT_MESSAGE}" "@keymanapp-test-bot skip" # Create cherry-pick on master branch git checkout -B chore/linux/cherry-pick/changelog origin/master git cherry-pick -x chore/linux/changelog -push_to_github_and_create_pr chore/linux/cherry-pick/changelog master "${COMMIT_MESSAGE}" "${COMMIT_MESSAGE} 🍒" +push_to_github_and_create_pr chore/linux/cherry-pick/changelog master "${COMMIT_MESSAGE} 🍒" \ +"Cherry-pick-of: #${PR_NUMBER} +@keymanapp-test-bot skip" builder_heading "Finishing" git checkout "${CURRENT_BRANCH}" diff --git a/mac/Keyman4MacIM/Keyman4MacIM/Privacy/PrivacyConsent.m b/mac/Keyman4MacIM/Keyman4MacIM/Privacy/PrivacyConsent.m index 7e3fffdb0a..992916862b 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/Privacy/PrivacyConsent.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/Privacy/PrivacyConsent.m @@ -1,4 +1,4 @@ -/** +/* * Keyman is copyright (C) SIL International. MIT License. * * PrivacyConsent.m @@ -7,8 +7,8 @@ * Created by Shawn Schantz on 2022-09-22. * * Used to determine if the user has provided consent to the services it needs - * and, if not, to request that consent. For versions of macOS prior to 10.15, - * Keyman requires Accessibility access. For 10.15 and later, Keyman requires + * and, if not, to request that consent. For versions of macOS prior to 11.0, + * Keyman requires Accessibility access. For 11.0 and later, Keyman requires * PostEvent access. Both are presented to the user as a need for Accessibility * when prompted by the system and are listed under Accessibility in the Privacy * tab in System Preferences, Security & Privacy. Both Accessibility and @@ -16,7 +16,11 @@ * that access is not explicitly needed as long as one of the others is provided * first. * - * Access this functionality through the shared instance. Call + * Note that Apple's documentation states that the PostEvent access APIs are + * available with macOS 10.15 (Catalina) but testing showed that it was only + * available with macOS 11.0 (Big Sur). This is documented with issue #12295. + * + * To access this functionality, used the shared instance. Call * requestPrivacyAccess which will check whether access has been granted. If * not, then a dialog will be presented to the user to inform them that * Accessibility is required. Dismissing this dialog will trigger the API call @@ -24,7 +28,7 @@ * only happen once. If the user was prompted earlier and did not grant access, * they can still go to System Preferences and enable it there. So it is useful * to prompt with a dialog from Keyman at startup. Without Accessibility, the - * shift layer and the OSK do not work -- so Keyman is basically useless. + * shift layer and the OSK do not work. */ #import "PrivacyConsent.h" @@ -42,7 +46,7 @@ } /** - * For macOS earlier than 10.15: check for Accessibility access. + * For macOS earlier than 11.0: check for Accessibility access. */ - (BOOL)checkAccessibility { @@ -65,7 +69,7 @@ BOOL hasAccessibility = NO; // check if we already have accessibility - if (@available(macOS 10.15, *)) { + if (@available(macOS 11.0, *)) { hasAccessibility = [self checkPostEventAccess]; } else { hasAccessibility = [self checkAccessibility]; @@ -84,7 +88,7 @@ } /** - * For macOS earlier than 10.15: request Accessibility access. + * For macOS earlier than 11.0: request Accessibility access. */ - (void)requestAccessibility { @@ -111,19 +115,18 @@ * Initialize privacy alert appropriately as determined by macOS version. */ - (void)configureDialogForOsVersion { - - if (@available(macOS 10.15, *)) { - [self configureDialogForCatalinaAndLater]; + if (@available(macOS 11.0, *)) { + [self configureDialogForBigSurAndLater]; } else { - [self configureDialogForPreCatalina]; + [self configureDialogForPreBigSur]; } } /** - * Initialize privacy dialog for macOS versions prior to Catalina (10.15). + * Initialize privacy dialog for macOS versions prior to Big Sur (11.0). * In this case, request Accessibility access, not PostEvent. */ -- (void)configureDialogForPreCatalina { +- (void)configureDialogForPreBigSur { void (^consentPrompt)(void) = ^(void) { [self requestAccessibility]; @@ -135,10 +138,10 @@ } /** - * Initialize privacy dialog for macOS versions of Catalina (10.15) or later. + * Initialize privacy dialog for macOS versions of Big Sur (11.0) or later. * In this case, request PostEvent access, not Accessibility. */ -- (void)configureDialogForCatalinaAndLater { +- (void)configureDialogForBigSurAndLater { void (^consentPrompt)(void) = ^(void) { [self requestPostEventAccess]; @@ -159,7 +162,7 @@ /** * Check whether the user has allowed ListenEvent access. - * Only available with macOS Catalina (10.15) or later. + * Only available with macOS Big Sur (11.0) or later. * * If user has granted Accessibility or PostEvent access, then * ListenEvent access is also granted. @@ -169,18 +172,18 @@ BOOL hasAccess = NO; // below checks for ListenEvent access - if (@available(macOS 10.15, *)) { + if (@available(macOS 11.0, *)) { hasAccess = CGPreflightListenEventAccess(); NSLog(@"CGPreflightListenEventAccess() returned %@", hasAccess ? @"YES" : @"NO"); } else { - NSLog(@"CGPreflightListenEventAccess not available before macOS version 10.15"); + NSLog(@"CGPreflightListenEventAccess not available before macOS version 11.0"); } return hasAccess; } /** * Check whether the user has allowed PostEvent access. - * Only available with macOS Catalina (10.15) or later. + * Only available with macOS Big Sur (11.0) or later. * * If user has granted Accessibility, then PostEvent access is also granted. */ @@ -189,46 +192,46 @@ BOOL hasAccess = NO; // below checks for PostEvent access - if (@available(macOS 10.15, *)) { + if (@available(macOS 11.0, *)) { hasAccess = CGPreflightPostEventAccess(); NSLog(@"CGPreflightPostEventAccess() returned %@", hasAccess ? @"YES" : @"NO"); } else { - NSLog(@"CGPreflightPostEventAccess not available before macOS version 10.15"); + NSLog(@"CGPreflightPostEventAccess not available before macOS version 11.0"); } return hasAccess; } /** * Request ListenEvent access. - * Only available with macOS Catalina (10.15) or later. + * Only available with macOS Big Sur (11.0) or later. */ - (BOOL)requestListenEventAccess { BOOL granted = NO; - if (@available(macOS 10.15, *)) { + if (@available(macOS 11.0, *)) { granted = CGRequestListenEventAccess(); NSLog(@"CGRequestListenEventAccess() returned %@", granted ? @"YES" : @"NO"); } else { - NSLog(@"CGRequestListenEventAccess not available before macOS version 10.15"); + NSLog(@"CGRequestListenEventAccess not available before macOS version 11.0"); } return granted; } /** * Request PostEvent access. - * Only available with macOS Catalina (10.15) or later. + * Only available with macOS Big Sur (11.0) or later. */ - (BOOL)requestPostEventAccess { BOOL granted = NO; - if (@available(macOS 10.15, *)) { + if (@available(macOS 11.0, *)) { granted = CGRequestPostEventAccess(); NSLog(@"CGRequestPostEventAccess() returned %@", granted ? @"YES" : @"NO"); } else { - NSLog(@"CGRequestPostEventAccess not available before macOS version 10.15"); + NSLog(@"CGRequestPostEventAccess not available before macOS version 11.0"); } return granted; } diff --git a/oem/firstvoices/android/app/build.gradle b/oem/firstvoices/android/app/build.gradle index 43adc17265..62064f3899 100644 --- a/oem/firstvoices/android/app/build.gradle +++ b/oem/firstvoices/android/app/build.gradle @@ -73,12 +73,17 @@ java { } // how to configure the sentry android gradle plugin -sentry { - // Disables or enables the automatic configuration of Native symbols - uploadNativeSymbols = true +task publishSentry { + doLast { + println 'Publishing FirstVoices symbols to Sentry' + sentry { + // Disables or enables the automatic configuration of Native symbols + uploadNativeSymbols = true - // Does or doesn't include the source code of native code for Sentry - includeNativeSources = true + // Does or doesn't include the source code of native code for Sentry + includeNativeSources = true + } + } } String env_keys_json_file = System.getenv("oem_firstvoices_keys_json_file") diff --git a/oem/firstvoices/android/build.sh b/oem/firstvoices/android/build.sh index 75bcfc672a..5500555acd 100755 --- a/oem/firstvoices/android/build.sh +++ b/oem/firstvoices/android/build.sh @@ -32,6 +32,7 @@ builder_describe "Builds FirstVoices for Android app." \ "configure" \ "build" \ "test Runs lint and tests." \ + "publish Publishes symbols to Sentry and the APK to the Play Store." \ "--ci Don't start the Gradle daemon. For CI" \ "--upload-sentry Upload to sentry" @@ -61,15 +62,10 @@ fi #### Build action definitions #### -function makeLocalSentryRelease() { - echo "Placeholder for uploading symbols to Sentry" -} - -#### Build action definitions #### - # Check about cleaning artifact paths if builder_start_action clean; then rm -rf "$KEYMAN_ROOT/oem/firstvoices/android/app/build/outputs" + rm -rf "$KEYMAN_ROOT/oem/firstvoices/android/app/build/tmp" builder_finish_action success clean fi @@ -102,3 +98,5 @@ if builder_start_action test; then builder_finish_action success test fi + +builder_run_action publish ./gradlew $DAEMON_FLAG publishSentry publishReleaseApk diff --git a/oem/firstvoices/keyboards.csv b/oem/firstvoices/keyboards.csv index cbede23edc..ed13eb5aaa 100644 --- a/oem/firstvoices/keyboards.csv +++ b/oem/firstvoices/keyboards.csv @@ -7,61 +7,62 @@ fv,fv_uummarmiutun,Uummarmiutun,Arctic,fv_uummarmiutun_kmw-9.0.js,9.1.1,ikt-Latn fv,fv_eastern_canadian_inuktitut,ᐃᓄᒃᑎᑐᑦ (Eastern Canadian Inuktitut),Arctic,fv_eastern_canadian_inuktitut_kmw-9.0.js,9.2.1,ike-Cans,Eastern Canadian Inuktitut (Unified Canadian Aboriginal Syllabics) fv,fv_migmaq,Mi'gmawi'simg / Mi'kmawi'simk,Atlantic,fv_migmaq_kmw-9.0.js,9.1.2,mic-Latn,Mi'kmaq (Latin) fv,fv_skicinuwatuwewakon,Skicinuwatuwewakon,Atlantic,fv_skicinuwatuwewakon_kmw-9.0.js,9.1.1,pqm-Latn,Malecite-Passamaquoddy (Latin) -fv,fv_uwikala,'Uwik̓ala,BC Coast,fv_uwikala_kmw-9.0.js,9.3,hei,Heiltsuk +fv,fv_uwikala,’Wuìk̓ala,BC Coast,fv_uwikala_kmw-9.0.js,9.4,hei,Heiltsuk fv,fv_dexwlesucid,Dəxʷləšucid,BC Coast,fv_dexwlesucid_kmw-9.0.js,9.2.1,lut-Latn,Lushootseed (Latin) fv,fv_diitiidatx,Diidiitidq,BC Coast,fv_diitiidatx_kmw-9.0.js,9.2.1,nuk-Latn,Nuu-chah-nulth (Latin) -fv,fv_gitsenimx,Gitsenimx̱,BC Coast,fv_gitsenimx_kmw-9.0.js,10.1.1,git,Gitxsan (Latin) -fv,fv_hailzaqvla,Haiɫzaqvla,BC Coast,fv_hailzaqvla_kmw-9.0.js,9.5.2,hei,Heiltsuk (Latin) -fv,fv_haisla,Haisla,BC Coast,fv_haisla.js,2.1,has-Latn,Haisla (Latin) -fv,fv_halqemeylem,Halq'eméylem,BC Coast,fv_halqemeylem_kmw-9.0.js,9.2,hur-Latn,Halkomelem (Latin) -fv,fv_henqeminem,Hǝn̓q̓ǝmin̓ǝm,BC Coast,fv_henqeminem_kmw-9.0.js,10.1,hur-Latn,Halkomelem (Latin) -fv,fv_klahoose,Homalco-Klahoose-Sliammon,BC Coast,fv_klahoose_kmw-9.0.js,10.1,coo,Comox -fv,fv_hulquminum,Hul’q’umi’num’,BC Coast,fv_hulquminum_kmw-9.0.js,9.1,hur-Latn,Halkomelem (Latin) +fv,fv_gitsenimx,Gitsenimx̱,BC Coast,fv_gitsenimx_kmw-9.0.js,10.1.2,git,Gitxsan (Latin) +fv,fv_hailzaqvla,Haíɫzaqvḷa,BC Coast,fv_hailzaqvla_kmw-9.0.js,10.0,hei,Heiltsuk (Latin) +fv,fv_haisla,Haisla,BC Coast,fv_haisla.js,2.1.3,has-Latn,Haisla (Latin) +fv,fv_halqemeylem,Halq'eméylem,BC Coast,fv_halqemeylem_kmw-9.0.js,9.2.1,hur-Latn,Halkomelem (Latin) +fv,fv_henqeminem,hǝn̓q̓ǝmin̓ǝm̓,BC Coast,fv_henqeminem_kmw-9.0.js,10.2.1,hur-Latn,Halkomelem (Latin) +fv,fv_klahoose,Éy7á7juuthem,BC Coast,fv_klahoose_kmw-9.0.js,10.2,coo,Comox +fv,fv_hulquminum,Hul’q’umi’num’,BC Coast,fv_hulquminum_kmw-9.0.js,9.2,hur,Halkomelem fv,fv_hulquminum_combine,Hul̓q̓umin̓um̓,BC Coast,fv_hulquminum_combine_kmw-9.0.js,2.0.1,hur-Latn,Halkomelem (Latin) -fv,fv_kwakwala_liqwala,Kʷak̓ʷala,BC Coast,fv_kwakwala_liqwala_kmw-9.0.js,9.2.5,kwk-Latn,Kwakiutl (Latin) -fv,fv_kwakwala,Kwak̕wala,BC Coast,fv_kwakwala_kmw-9.0.js,9.1.2,kwk-Latn,Kwakiutl (Latin) -fv,fv_nexwslayemucen,Nəxʷsƛ̓ay̓əmúcən,BC Coast,fv_nexwslayemucen_kmw-9.0.js,9.2.1,clm-Latn,Clallam (Latin) -fv,fv_nisgaa,Nisg̱a'a,BC Coast,fv_nisgaa_kmw-9.0.js,9.1.2,ncg-Latn,Nisga'a (Latin) -fv,fv_nuucaanul,Nuučaan̓uł,BC Coast,fv_nuucaanul_kmw-9.0.js,9.1.4,nuk-Latn,Nuu-chah-nulth (Latin) -fv,fv_nuxalk,Nuxalk,BC Coast,fv_nuxalk_kmw-9.0.js,10.0,blc-Latn,Bella Coola (Latin) -fv,fv_sencoten,SENĆOŦEN,BC Coast,fv_sencoten_kmw-9.0.js,9.2.2,str,Straits Salish -fv,fv_sguuxs,Sgüüx̱s,BC Coast,fv_sguuxs.js,1.0.1,tsi,Tsimshian -fv,fv_shashishalhem,Shashishalhem,BC Coast,fv_shashishalhem_kmw-9.0.js,9.2,sec-Latn,Sechelt (Latin) -fv,fv_skwxwumesh_snichim,Sḵwx̱wúmesh sníchim,BC Coast,fv_skwxwumesh_snichim_kmw-9.0.js,9.3,squ-Latn,Squamish (Latin) -fv,fv_smalgyax,Sm'algya̱x,BC Coast,fv_smalgyax_kmw-9.0.js,9.2,tsi-Latn,Tsimshian (Latin) -fv,fv_xaislakala,X̄a'ʼislak̓ala,BC Coast,fv_xaislakala_kmw-9.0.js,9.1.1,has-Latn,Haisla (Latin) -fv,fv_hlgaagilda_xaayda_kil,X̱aayda-X̱aad Kil,BC Coast,fv_hlgaagilda_xaayda_kil_kmw-9.0.js,9.3,hax,Southern Haida -fv,fv_dakelh,Dakelh,BC Interior,fv_dakelh_kmw-9.0.js,9.2,caf-Latn,Southern Carrier (Latin) +fv,fv_kwakwala_liqwala,Kʷak̓ʷala,BC Coast,fv_kwakwala_liqwala_kmw-9.0.js,9.3,kwk-Latn,Kwakiutl (Latin) +fv,fv_kwakwala,Kwak̕wala,BC Coast,fv_kwakwala_kmw-9.0.js,9.2,kwk-Latn,Kwakiutl (Latin) +fv,fv_lekwungen,Lekwungen (Lək̓ʷəŋən),BC Coast,fv_lekwungen_kmw-9.0.js,1.0.1,str,Straits Salish +fv,fv_nexwslayemucen,Nəxʷsƛ̓ay̓əmúcən,BC Coast,fv_nexwslayemucen_kmw-9.0.js,11.0,clm,Clallam +fv,fv_nisgaa,Nisg̱a'a,BC Coast,fv_nisgaa_kmw-9.0.js,9.2.1,ncg-Latn,Nisga'a (Latin) +fv,fv_nuucaanul,Nuučaan̓uł,BC Coast,fv_nuucaanul_kmw-9.0.js,9.2,nuk-Latn,Nuu-chah-nulth (Latin) +fv,fv_nuxalk,Nuxalk,BC Coast,fv_nuxalk_kmw-9.0.js,10.0.1,blc-Latn,Bella Coola (Latin) +fv,fv_sencoten,SENĆOŦEN,BC Coast,fv_sencoten_kmw-9.0.js,9.2.3,str,Straits Salish +fv,fv_sguuxs,Sgüüx̱s,BC Coast,fv_sguuxs.js,1.1,tsi,Tsimshian +fv,fv_shashishalhem,Shashishalhem,BC Coast,fv_shashishalhem_kmw-9.0.js,9.2.1,sec-Latn,Sechelt (Latin) +fv,fv_skwxwumesh_snichim,Sḵwx̱wú7mesh sníchim,BC Coast,fv_skwxwumesh_snichim_kmw-9.0.js,10.0,squ-Latn,Squamish (Latin) +fv,fv_smalgyax,Sm'algya̱x,BC Coast,fv_smalgyax_kmw-9.0.js,9.3,tsi-Latn,Tsimshian (Latin) +fv,fv_xaislakala,X̄aʼislak̓ala,BC Coast,fv_xaislakala_kmw-9.0.js,10.0,has-Latn,Haisla (Latin) +fv,fv_hlgaagilda_xaayda_kil,X̱aayda-X̱aad Kil,BC Coast,fv_hlgaagilda_xaayda_kil_kmw-9.0.js,9.4,hax,Southern Haida +fv,fv_dakelh,Dakelh,BC Interior,fv_dakelh_kmw-9.0.js,9.2.1,caf-Latn,Southern Carrier (Latin) fv,fv_ktunaxa,Ktunaxa,BC Interior,fv_ktunaxa_kmw-9.0.js,10.0,kut-Latn,Kutenai (Latin) -fv,fv_kwadacha_tsekene,Kwadacha Tsek’ene,BC Interior,fv_kwadacha_tsekene_kmw-9.0.js,1.0,sek-Latn,Sekani -fv,fv_natwits,Nedut’en-Witsuwit'en,BC Interior,fv_natwits_kmw-9.0.js,9.1.3,caf-Latn,Southern Carrier (Latin) -fv,fv_nlekepmxcin,Nłeʔkepmxcin,BC Interior,fv_nlekepmxcin_kmw-9.0.js,9.4,thp-Latn,Thompson (Latin) -fv,fv_nlha7kapmxtsin,Nlha7kapmxtsin,BC Interior,fv_nlha7kapmxtsin_kmw-9.0.js,10.0,thp-Latn,Thompson (Latin) -fv,fv_nlakapamuxcheen,Nlakapamuxcheen,BC Interior,fv_nlakapamuxcheen_kmw-9.0.js,1.0.2,thp,Thompson -fv,fv_nsilxcen,Nsilxcən,BC Interior,fv_nsilxcen_kmw-9.0.js,9.3,oka,Okanagan -fv,fv_secwepemctsin,Secwepemctsín,BC Interior,fv_secwepemctsin_kmw-9.0.js,9.2,shs-Latn,Shuswap (Latin) -fv,fv_stlatlimxec,Sƛ̓aƛ̓imxəc,BC Interior,fv_stlatlimxec_kmw-9.0.js,9.3,lil-Latn,Lillooet (Latin) -fv,fv_statimcets,St̓át̓imcets,BC Interior,fv_statimcets_kmw-9.0.js,9.1.4,lil-Latn,Lillooet (Latin) -fv,fv_taltan,Tāłtān,BC Interior,fv_taltan_kmw-9.0.js,9.1.5,tht-Latn,Tahltan (Latin) -fv,fv_tsekehne,Tsek'ehne,BC Interior,fv_tsekehne_kmw-9.0.js,9.1.2,sek-Latn,Sekani (Latin) -fv,fv_tsilhqotin,Tŝilhqot'in,BC Interior,fv_tsilhqotin_kmw-9.0.js,9.1.3,clc-Latn,Chilcotin (Latin) -fv,fv_southern_carrier,ᑐᑊᘁᗕᑋᗸ (Southern Carrier),BC Interior,fv_southern_carrier_kmw-9.0.js,10.0.1,caf-Cans,Southern Carrier (Unified Canadian Aboriginal Syllabics) +fv,fv_kwadacha_tsekene,Kwadacha Tsek’ene,BC Interior,fv_kwadacha_tsekene_kmw-9.0.js,1.1,sek-Latn,Sekani +fv,fv_natwits,Nedut’en-Witsuwit'en,BC Interior,fv_natwits_kmw-9.0.js,9.2,caf-Latn,Southern Carrier (Latin) +fv,fv_nlekepmxcin,Nłeʔkepmxcin,BC Interior,fv_nlekepmxcin_kmw-9.0.js,9.5.1,thp-Latn,Thompson (Latin) +fv,fv_nlha7kapmxtsin,Nlha7kapmxtsin,BC Interior,fv_nlha7kapmxtsin_kmw-9.0.js,10.1.2,thp,Thompson +fv,fv_nlakapamuxcheen,Nlakapamuxcheen,BC Interior,fv_nlakapamuxcheen_kmw-9.0.js,1.1,thp,Thompson +fv,fv_nsilxcen,Nsyilxcən,BC Interior,fv_nsilxcen_kmw-9.0.js,10.0.1,oka,Okanagan +fv,fv_secwepemctsin,Secwepemctsín,BC Interior,fv_secwepemctsin_kmw-9.0.js,9.3,shs,Shuswap +fv,fv_stlatlimxec,Sƛ̓aƛ̓imxəc,BC Interior,fv_stlatlimxec_kmw-9.0.js,9.4,lil-Latn,Lillooet (Latin) +fv,fv_statimcets,St̓át̓imcets,BC Interior,fv_statimcets_kmw-9.0.js,9.2,lil-Latn,Lillooet (Latin) +fv,fv_taltan,Tāłtān,BC Interior,fv_taltan_kmw-9.0.js,9.2,tht-Latn,Tahltan (Latin) +fv,fv_tsekehne,Tsek'ehne,BC Interior,fv_tsekehne_kmw-9.0.js,9.2,sek-Latn,Sekani (Latin) +fv,fv_tsilhqotin,Tŝilhqot’in,BC Interior,fv_tsilhqotin_kmw-9.0.js,9.3,clc-Latn,Chilcotin (Latin) +fv,fv_southern_carrier,ᑐᑊᘁᗕᑋᗸ (Southern Carrier),BC Interior,fv_southern_carrier_kmw-9.0.js,10.1,caf-Cans,Southern Carrier (Unified Canadian Aboriginal Syllabics) fv,fv_anicinapemi8in,Anicinapemi8in/Anishinàbemiwin,Eastern Subarctic,fv_anicinapemi8in_kmw-9.0.js,9.1.1,alq-Latn,Algonquin (Latin) fv,fv_atikamekw,Atikamekw,Eastern Subarctic,fv_atikamekw_kmw-9.0.js,9.1.1,atj-Latn,Atikamekw (Latin) fv,fv_ilnu_innu_aimun,Ilnu-Innu Aimun,Eastern Subarctic,fv_ilnu_innu_aimun_kmw-9.0.js,9.1.1,moe-Latn,Montagnais (Latin) fv,fv_swampy_cree,ᐃᓂᓂᒧᐎᐣ (Swampy Cree),Eastern Subarctic,fv_swampy_cree_kmw-9.0.js,9.2.1,csw-Cans,Swampy Cree (Unified Canadian Aboriginal Syllabics) fv,fv_moose_cree,ᐃᓕᓖᒧᐎᓐ (Moose Cree),Eastern Subarctic,fv_moose_cree_kmw-9.0.js,9.2.1,crm-Cans,Moose Cree (Unified Canadian Aboriginal Syllabics) fv,fv_northern_east_cree,ᐄᔨᔫ-ᐄᓅ ᐊᔨᒨᓐ (Northern East Cree),Eastern Subarctic,fv_northern_east_cree_kmw-9.0.js,9.2.1,crl-Cans,Northern East Cree (Unified Canadian Aboriginal Syllabics) -fv,fv_severn_ojibwa,ᐊᓂᔑᓂᓂᒧᐎᐣ (Severn Ojibwa),Eastern Subarctic,fv_severn_ojibwa_kmw-9.0.js,10.0.1,ojs-Cans,Severn Ojibwa (Unified Canadian Aboriginal Syllabics) -fv,fv_severn_ojibwa_rdot,ᐊᓂᔑᓂᓂᒧᐏᐣ (Severn Ojibwa right w-dot),Eastern Subarctic,fv_severn_ojibwa_kmw-9.0.js,1.0.1,ojs,Oji-Cree -fv,fv_ojibwa,ᐊᓂᔑᓇᐯᒧᐎᓐ (a-finals),Eastern Subarctic,fv_ojibwa_kmw-9.0.js,10.0.1,ojb-Cans,Northwestern Ojibwa (Unified Canadian Aboriginal Syllabics) -fv,fv_ojibwa_rdot,ᐁᓂᔑᓇᐯᒧᐏᓐ (a-finals right w-dot),Eastern Subarctic,fv_ojibwa_rdot_kmw-9.0.js,1.0.1,oj,Ojibwa -fv,fv_ojibwa_ifinal,ᐊᓂᔑᓇᐯᒧᐎᣙ (i-finals),Eastern Subarctic,fv_ojibwa_ifinal_kmw-9.0.js,1.0,oj,Ojibwa -fv,fv_ojibwa_ifinal_rdot,ᐊᓂᔑᓇᐯᒧᐏᣙ (i-finals right w-dot),Eastern Subarctic,fv_ojibwa_ifinal_rdot_kmw-9.0.js,1.0,oj,Ojibwa +fv,fv_severn_ojibwa,ᐊᓂᔑᓂᓂᒧᐎᐣ (Severn Ojibwa),Eastern Subarctic,fv_severn_ojibwa_kmw-9.0.js,10.1.1,ojs-Cans,Severn Ojibwa (Unified Canadian Aboriginal Syllabics) +fv,fv_severn_ojibwa_rdot,ᐊᓂᔑᓂᓂᒧᐏᐣ (Severn Ojibwa right w-dot),Eastern Subarctic,fv_severn_ojibwa_kmw-9.0.js,1.1.1,ojs,Oji-Cree +fv,fv_ojibwa,ᐊᓂᔑᓇᐯᒧᐎᓐ (a-finals),Eastern Subarctic,fv_ojibwa_kmw-9.0.js,10.1,ojb-Cans,Northwestern Ojibwa (Unified Canadian Aboriginal Syllabics) +fv,fv_ojibwa_rdot,ᐁᓂᔑᓇᐯᒧᐏᓐ (a-finals right w-dot),Eastern Subarctic,fv_ojibwa_rdot_kmw-9.0.js,1.0.3,oj,Ojibwa +fv,fv_ojibwa_ifinal,ᐊᓂᔑᓇᐯᒧᐎᣙ (i-finals),Eastern Subarctic,fv_ojibwa_ifinal_kmw-9.0.js,1.0.2,oj,Ojibwa +fv,fv_ojibwa_ifinal_rdot,ᐊᓂᔑᓇᐯᒧᐏᣙ (i-finals right w-dot),Eastern Subarctic,fv_ojibwa_ifinal_rdot_kmw-9.0.js,1.0.2,oj,Ojibwa fv,fv_naskapi,ᓇᔅᑲᐱ (Naskapi),Eastern Subarctic,fv_naskapi_kmw-9.0.js,9.3.1,nsk-Cans,Naskapi (Unified Canadian Aboriginal Syllabics) sil,sil_euro_latin,English,European,european2-1.6.js,3.0.2,en,English basic,basic_kbdcan,Français,European,canadian_french-1.0.js,1.1.1,fr-CA,French (Canada) -fv,fv_anishinaabemowin,Anishinaabemowin,Great Lakes - St. Lawrence,fv_anishinaabemowin_kmw-9.0.js,10.1,oj,Ojibwa +fv,fv_anishinaabemowin,Anishinaabemowin,Great Lakes - St. Lawrence,fv_anishinaabemowin_kmw-9.0.js,10.2,oj,Ojibwa fv,fv_bodewadminwen,Bodéwadminwen-Nishnabémwen,Great Lakes - St. Lawrence,fv_bodewadminwen_kmw-9.0.js,9.1.1,pot-Latn,Potawatomi (Latin) fv,fv_goyogohono,Goyogo̱hó:nǫ',Great Lakes - St. Lawrence,fv_goyogohono_kmw-9.0.js,9.2.1,cay-Latn,Cayuga (Latin) fv,fv_kanienkeha_e,Kanien'kéha-Kanyen'kéha,Great Lakes - St. Lawrence,fv_kanienkeha_e_kmw-9.0.js,9.1.3,moh-Latn,Mohawk (Latin) @@ -82,24 +83,24 @@ fv,fv_isga_iabi,Isga Iʔabi,Prairies,fv_isga_iabi_kmw-9.0.js,9.1.1,sto-Latn,Ston fv,fv_lakota,Lak̇ot̄a,Prairies,fv_lakota-9.0.js,9.1.1,lkt-Latn,Lakota (Latin) fv,fv_nakoda,Nakoda,Prairies,fv_nakoda_kmw-9.0.js,9.1.1,asb-Latn,Assiniboine (Latin) fv,fv_tsuutina,Tsúùt'ínà,Prairies,fv_tsuutina_kmw-9.0.js,9.1.1,srs-Latn,Sarsi (Latin) -fv,fv_plains_cree,ᓀᐦᐃᔭᐍᐏᐣ (Plains Cree),Prairies,fv_plains_cree_kmw-9.0.js,11.0.2,crk-Cans,ᓀᐦᐃᔭᐍᐏᐣ (Cree syllabics) +fv,fv_plains_cree,ᓀᐦᐃᔭᐍᐏᐣ (Plains Cree),Prairies,fv_plains_cree_kmw-9.0.js,11.1,crk-Cans,ᓀᐦᐃᔭᐍᐏᐣ (Cree syllabics) fv,fv_dine_bizaad,Diné Bizaad,South West,fv_dine_bizaad_kmw-9.0.js,9.1.1,nv-Latn,Navajo (Latin) -fv,fv_dane_zaa_zaage,Dane-Z̲aa Z̲áágéʔ,Western Subarctic,fv_dane_zaa_zaage_kmw-9.0.js,9.4,bea,Beaver -fv,fv_dene_dzage,Dene Dzage,Western Subarctic,fv_dene_dzage_kmw-9.0.js,11.0.1,kkz-Latn,Kaska (Latin) -fv,fv_dene_zhatie,Dene Zhatié,Western Subarctic,fv_dene_zhatie_kmw-9.0.js,10.2,den,Dene Zhatıé -fv,fv_denesuline_epsilon,Dënesųłıné,Western Subarctic,fv_denesuline_epsilon_kmw-9.0.js,10.0.1,chp,Chipewyan -fv,fv_denesuline,Dɛnɛsųłiné,Western Subarctic,fv_denesuline_kmw-9.0.js,10.0.1,chp,Chipewyan (Latin) +fv,fv_dane_zaa_zaage,Dane-Z̲aa Z̲áágéʔ,Western Subarctic,fv_dane_zaa_zaage_kmw-9.0.js,9.4.1,bea,Beaver +fv,fv_dene_dzage,Danezāgéʼ,Western Subarctic,fv_dene_dzage_kmw-9.0.js,11.0.1,kkz-Latn,Kaska (Latin) +fv,fv_dene_zhatie,Dene Zhatıé,Western Subarctic,fv_dene_zhatie_kmw-9.0.js,10.2.1,den,Dene Zhatıé +fv,fv_denesuline_epsilon,Dɛnɛsųłįnɛ,Western Subarctic,fv_denesuline_epsilon_kmw-9.0.js,10.0.2,chp,Chipewyan +fv,fv_denesuline,Dënesųłıné,Western Subarctic,fv_denesuline_kmw-9.0.js,10.0.1,chp,Chipewyan (Latin) fv,fv_gwichin,Gwich'in,Western Subarctic,fv_gwichin_kmw-9.0.js,9.2.1,gwi-Latn,Gwichʼin (Latin) fv,fv_han,Hän,Western Subarctic,fv_han_kmw-9.0.js,9.2,haa-Latn,Han (Latin) -fv,fv_kashogotine_yati,K'áshogot'ı̨nę́ Yatı̨́,Western Subarctic,fv_kashogotine_yati_kmw-9.0.js,10.0,scs,North Slavey -fv,fv_tlingit,Łingít,Western Subarctic,fv_tlingit_kmw-9.0.js,10.0.1,tli,Tlingit (Latin) +fv,fv_kashogotine_yati,K'áshogot'ı̨nę́ Yatı̨́,Western Subarctic,fv_kashogotine_yati_kmw-9.0.js,10.1,scs,North Slavey +fv,fv_tlingit,Łingít,Western Subarctic,fv_tlingit_kmw-9.0.js,10.1,tli,Tlingit (Latin) fv,fv_neeaandeg,Nee'aanděg',Western Subarctic,fv_neeaandeg_kmw-9.0.js,9.1.1,tcb-Latn,Tanacross (Latin) fv,fv_neeaaneegn,Nee'aaneegn',Western Subarctic,fv_neeaaneegn_kmw-9.0.js,9.1,tau-Latn,Upper Tanana (Latin) fv,fv_northern_tutchone,Northern Tutchone,Western Subarctic,fv_northern_tutchone_kmw-9.0.js,9.2,ttm-Latn,Northern Tutchone (Latin) fv,fv_sahugotine_yati,Sahtúgot'ı̨nę́ Yatı̨́,Western Subarctic,fv_sahugotine_yati_kmw-9.0.js,9.1.1,scs-Latn,North Slavey (Latin) fv,fv_shihgotine_yati,Shıhgot'ı̨nę́ Yatı̨́,Western Subarctic,fv_shihgotine_yati_kmw-9.0.js,9.1.1,scs-Latn,North Slavey (Latin) -fv,fv_southern_tutchone,Southern Tutchone,Western Subarctic,fv_southern_tutchone_kmw-9.0.js,9.2.2,tce-Latn,Southern Tutchone (Latin) -fv,fv_tagizi_dene,Tāgizi Dene,Western Subarctic,fv_tagizi_dene_kmw-9.0.js,9.3,tgx-Latn,Tagish (Latin) +fv,fv_southern_tutchone,Southern Tutchone,Western Subarctic,fv_southern_tutchone_kmw-9.0.js,9.3,tce-Latn,Southern Tutchone (Latin) +fv,fv_tagizi_dene,Tāgizi Dene,Western Subarctic,fv_tagizi_dene_kmw-9.0.js,9.4,tgx-Latn,Tagish (Latin) fv,fv_tlicho_yatii,Tłı̨chǫ Yatıı̀,Western Subarctic,fv_tlicho_yatii_kmw-9.0.js,9.1.1,dgr-Latn,Dogrib (Latin) fv,fv_dene_mb,ᑌᓀ ᔭᕠᐁ (Dene MB),Western Subarctic,fv_dene_mb_kmw-9.0.js,9.2.1,chp-Cans,Chipewyan (Unified Canadian Aboriginal Syllabics) fv,fv_dene_nt,ᑌᓀ ᔭᕱᐁ (Dene NT),Western Subarctic,fv_dene_nt_kmw-9.0.js,9.2.1,chp-Cans,Chipewyan (Unified Canadian Aboriginal Syllabics) diff --git a/package-lock.json b/package-lock.json index 3125817985..8b2b1c816d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -510,6 +510,7 @@ "license": "MIT", "dependencies": { "@keymanapp/keyman-version": "*", + "@keymanapp/ldml-keyboard-constants": "*", "restructure": "git+https://github.com/keymanapp/dependency-restructure.git#7a188a1e26f8f36a175d95b67ffece8702363dfc", "semver": "^7.5.2", "xml2js": "git+https://github.com/keymanapp/dependency-node-xml2js#535fe732dc408d697e0f847c944cc45f0baf0829" @@ -1794,12 +1795,14 @@ "@keymanapp/developer-test-helpers": "*", "@keymanapp/resources-gosh": "*", "@types/chai": "^4.1.7", + "@types/common-tags": "^1.8.4", "@types/mocha": "^5.2.7", "@types/node": "^20.4.1", "@types/semver": "^7.3.12", "c8": "^7.12.0", "chai": "^4.3.4", "chalk": "^2.4.2", + "common-tags": "^1.8.2", "mocha": "^8.4.0", "ts-node": "^9.1.1", "typescript": "^4.9.5" @@ -4196,6 +4199,13 @@ "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==", "dev": true }, + "node_modules/@types/common-tags": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/@types/common-tags/-/common-tags-1.8.4.tgz", + "integrity": "sha512-S+1hLDJPjWNDhcGxsxEbepzaxWqURP/o+3cP4aa2w7yBXgdcmKGQtZzP8JbyfOd0m+33nh+8+kvxYE2UJtBDkg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/component-emitter": { "version": "1.2.11", "dev": true, @@ -5611,6 +5621,16 @@ "node": ">=14" } }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/component-emitter": { "version": "1.3.0", "dev": true, diff --git a/resources/build/increment-version.sh b/resources/build/increment-version.sh index 1a577b480f..da459776eb 100755 --- a/resources/build/increment-version.sh +++ b/resources/build/increment-version.sh @@ -104,9 +104,9 @@ echo "increment-version.sh: running resources/build/version" pushd "$KEYMAN_ROOT" ABORT=0 if [[ -z "$fromversion" ]]; then - node resources/build/version/build/src/index.js history version -t "$GITHUB_TOKEN" -b "$base" $HISTORY_FORCE || ABORT=$? + node resources/build/version/build/src/index.js history version -t "$GITHUB_TOKEN" -b "$base" $HISTORY_FORCE --github-pr || ABORT=$? else - node resources/build/version/build/src/index.js history version -t "$GITHUB_TOKEN" -b "$base" $HISTORY_FORCE --from "$fromversion" --to "$toversion" || ABORT=$? + node resources/build/version/build/src/index.js history version -t "$GITHUB_TOKEN" -b "$base" $HISTORY_FORCE --github-pr --from "$fromversion" --to "$toversion" || ABORT=$? fi if [[ $ABORT = 50 ]]; then @@ -179,7 +179,7 @@ if [ "$action" == "commit" ]; then # In order to avoid potential git conflicts, we run the history collater # again on the master HISTORY.md. Note that the script always exits 1 to # indicate it hasn't updated VERSION.md. We could tweak that in the future. - node resources/build/version/lib/index.js history --no-write-github-comment -t "$GITHUB_TOKEN" -b "$base" || true + node resources/build/version/lib/index.js history --no-write-github-comment --github-pr -t "$GITHUB_TOKEN" -b "$base" || true # If HISTORY.md has been updated, then we want to create a branch and push # it for review diff --git a/resources/build/version/src/fixupHistory.ts b/resources/build/version/src/fixupHistory.ts index 2183b11f70..a8ceda3c50 100644 --- a/resources/build/version/src/fixupHistory.ts +++ b/resources/build/version/src/fixupHistory.ts @@ -184,7 +184,7 @@ export const sendCommentToPullRequestAndRelatedIssues = async ( */ export const fixupHistory = async ( - octokit: GitHub, base: string, force: boolean, writeGithubComment: boolean, from?: string, to?: string + octokit: GitHub, base: string, force: boolean, writeGithubComment: boolean, useGitHubPRInfo: boolean, from?: string, to?: string ): Promise => { // @@ -194,7 +194,7 @@ export const fixupHistory = async ( let pulls: PRInformation[] = []; try { - pulls = await reportHistory(octokit, base, force, false, from, to); + pulls = await reportHistory(octokit, base, force, useGitHubPRInfo, from, to); } catch(e) { logWarning(String(e)); return -1; diff --git a/resources/build/version/src/index.ts b/resources/build/version/src/index.ts index 16f1583e39..ea0b12e7e7 100644 --- a/resources/build/version/src/index.ts +++ b/resources/build/version/src/index.ts @@ -108,7 +108,7 @@ const main = async (): Promise => { if(argv._.includes('history')) { logInfo(`# Validating history for ${version}`); - changeCount = await fixupHistory(octokit, argv.base, argv.force, argv['write-github-comment'], argv.from, argv.to); + changeCount = await fixupHistory(octokit, argv.base, argv.force, argv['write-github-comment'], argv['github-pr'], argv.from, argv.to); logInfo(`# ${changeCount} change(s) found for ${version}\n`); } diff --git a/resources/locate_emscripten.inc.sh b/resources/locate_emscripten.inc.sh index be66adfa32..36a78113f7 100644 --- a/resources/locate_emscripten.inc.sh +++ b/resources/locate_emscripten.inc.sh @@ -1,31 +1,73 @@ # shellcheck shell=bash # no hashbang for .inc.sh +KEYMAN_MIN_VERSION_EMSCRIPTEN=3.1.58 + # -# We don't want to rely on emcc.py being on the path, because Emscripten puts far +# We don't want to rely on emcc being on the path, because Emscripten puts far # too many things onto the path (in particular for us, node). # -# The following comment suggests that we don't need emcc.py on the path. +# The following comment suggests that we don't need emcc on the path. # https://github.com/emscripten-core/emscripten/issues/4848#issuecomment-1097357775 # -# So we try and locate emcc.py in common locations ourselves. The search pattern +# So we try and locate in common locations ourselves. The search pattern # is: # # 1. Look for $EMSCRIPTEN_BASE (our primary emscripten variable), which should -# point to the folder that emcc.py is located in -# 2. Look for $EMCC which should point to the emcc.py executable -# 3. Look for emcc.py on the path +# point to the folder that emcc is located in +# 2. Look for $EMCC which should point to the emcc executable +# 3. Look for emcc on the path # locate_emscripten() { + local EMCC_EXECUTABLE + if [[ "${BUILDER_OS}" == "win" ]]; then + EMCC_EXECUTABLE="emcc.py" + else + EMCC_EXECUTABLE="emcc" + fi if [[ -z ${EMSCRIPTEN_BASE+x} ]]; then if [[ -z ${EMCC+x} ]]; then - local EMCC=`which emcc.py` - [[ -z $EMCC ]] && builder_die "locate_emscripten: Could not locate emscripten (emcc.py) on the path or with \$EMCC or \$EMSCRIPTEN_BASE" + local EMCC=`which ${EMCC_EXECUTABLE}` + [[ -z $EMCC ]] && builder_die "locate_emscripten: Could not locate emscripten (emcc) on the path or with \$EMCC or \$EMSCRIPTEN_BASE" fi - [[ -f $EMCC && ! -x $EMCC ]] && builder_die "locate_emscripten: Variable EMCC ($EMCC) points to emcc.py but it is not executable" - [[ -x $EMCC ]] || builder_die "locate_emscripten: Variable EMCC ($EMCC) does not point to a valid executable emcc.py" + [[ -f $EMCC && ! -x $EMCC ]] && builder_die "locate_emscripten: Variable EMCC ($EMCC) points to emcc but it is not executable" + [[ -x $EMCC ]] || builder_die "locate_emscripten: Variable EMCC ($EMCC) does not point to a valid executable emcc" EMSCRIPTEN_BASE="$(dirname "$EMCC")" fi - [[ -f ${EMSCRIPTEN_BASE}/emcc.py && ! -x ${EMSCRIPTEN_BASE}/emcc.py ]] && builder_die "locate_emscripten: Variable EMSCRIPTEN_BASE ($EMSCRIPTEN_BASE) contains emcc.py but it is not executable" - [[ -x ${EMSCRIPTEN_BASE}/emcc.py ]] || builder_die "locate_emscripten: Variable EMSCRIPTEN_BASE ($EMSCRIPTEN_BASE) does not point to emcc.py's folder" + [[ -f ${EMSCRIPTEN_BASE}/${EMCC_EXECUTABLE} && ! -x ${EMSCRIPTEN_BASE}/${EMCC_EXECUTABLE} ]] && builder_die "locate_emscripten: Variable EMSCRIPTEN_BASE ($EMSCRIPTEN_BASE) contains ${EMCC_EXECUTABLE} but it is not executable" + [[ -x ${EMSCRIPTEN_BASE}/${EMCC_EXECUTABLE} ]] || builder_die "locate_emscripten: Variable EMSCRIPTEN_BASE ($EMSCRIPTEN_BASE) does not point to ${EMCC_EXECUTABLE}'s folder" + + verify_emscripten_version +} + +# Ensure that we use correct version of emsdk on build agents. +# For developers, define KEYMAN_USE_SDK to do this on your +# build machine. +verify_emscripten_version() { + if [[ "$VERSION_ENVIRONMENT" != local || ! -z "${KEYMAN_USE_EMSDK+x}" ]]; then + _select_emscripten_version_with_emsdk + fi +} + +# Use emsdk to select the appropriate version of Emscripten +# according to minimum-versions.inc.sh +_select_emscripten_version_with_emsdk() { + if [[ -z "${EMSCRIPTEN_BASE+x}" ]]; then + builder_die "Variable EMSCRIPTEN_BASE must be set" + fi + + if [[ -z "${KEYMAN_MIN_VERSION_EMSCRIPTEN+x}" ]]; then + builder_die "Variable KEYMAN_MIN_VERSION_EMSCRIPTEN must be set" + fi + + pushd "${EMSCRIPTEN_BASE}/../.." > /dev/null + if [[ ! -f emsdk ]]; then + builder_die "emsdk[.bat] should be in $(pwd)" + fi + + export EMSDK_KEEP_DOWNLOADS=1 + git pull + ./emsdk install "$KEYMAN_MIN_VERSION_EMSCRIPTEN" + ./emsdk activate "$KEYMAN_MIN_VERSION_EMSCRIPTEN" + popd > /dev/null } diff --git a/web/src/engine/osk/src/visualKeyboard.ts b/web/src/engine/osk/src/visualKeyboard.ts index d4c234cde6..2ac7a06432 100644 --- a/web/src/engine/osk/src/visualKeyboard.ts +++ b/web/src/engine/osk/src/visualKeyboard.ts @@ -1260,6 +1260,7 @@ export default class VisualKeyboard extends EventEmitter implements Ke const groupStyle = getComputedStyle(this.layerGroup.element); const isInDOM = computedStyle.height != '' && computedStyle.height != 'auto'; + const isGroupInDOM = groupStyle.height != '' && groupStyle.height != 'auto'; if (computedStyle.border) { this._borderWidth = new ParsedLengthStyle(computedStyle.borderWidth).val; @@ -1272,10 +1273,11 @@ export default class VisualKeyboard extends EventEmitter implements Ke this._computedHeight = this.height; } else if (isInDOM) { this._computedWidth = parseInt(computedStyle.width, 10); - if (!this._computedWidth) { - this._computedWidth = parseInt(groupStyle.width, 10); - } this._computedHeight = parseInt(computedStyle.height, 10); + } else if (isGroupInDOM) { + // May occur for documentation-keyboards, which are detached from their VisualKeyboard base. + this._computedWidth = parseInt(groupStyle.width, 10); + this._computedHeight = parseInt(groupStyle.height, 10); } else { // Cannot perform layout operations! return; @@ -1612,6 +1614,16 @@ export default class VisualKeyboard extends EventEmitter implements Ke // the page. kbdObj.appendStyleSheet(); + // Unset the width + height we used thus far; this method's consumer may choose to rescale + // the returned element. If so, we don't want to use our outdated value by mistake. + // + // While `kbdObj.setSize()` could be used in theory, it _also_ unsets the element styling. + // We actually wish to _leave_ this styling in place - one of our parameters is `height`, and + // it should remain in place in the styling on the output element as the default in case + // the consumer _doesn't_ add styling afterward. + delete kbdObj._width; + delete kbdObj._height; + return classWrapper; }